master
go 267 lines 6.68 KB
Raw
1 // Package commands implements the ipfs command interface
2 //
3 // Using github.com/ipfs/kubo/commands to define the command line and HTTP
4 // APIs. This is the interface available to folks using IPFS from outside of
5 // the Go language.
6 package commands
7
8 import (
9 "bytes"
10 "fmt"
11 "io"
12 "os"
13 "slices"
14 "strings"
15
16 cmds "github.com/ipfs/go-ipfs-cmds"
17 )
18
19 type commandEncoder struct {
20 w io.Writer
21 }
22
23 func (e *commandEncoder) Encode(v any) error {
24 var (
25 cmd *Command
26 ok bool
27 )
28
29 if cmd, ok = v.(*Command); !ok {
30 return fmt.Errorf(`core/commands: unexpected type %T, expected *"core/commands".Command`, v)
31 }
32
33 for _, s := range cmdPathStrings(cmd, cmd.showOpts) {
34 _, err := e.w.Write([]byte(s + "\n"))
35 if err != nil {
36 return err
37 }
38 }
39
40 return nil
41 }
42
43 type Command struct {
44 Name string
45 Subcommands []Command
46 Options []Option
47
48 showOpts bool
49 }
50
51 type Option struct {
52 Names []string
53 }
54
55 const (
56 flagsOptionName = "flags"
57 )
58
59 // CommandsCmd takes in a root command,
60 // and returns a command that lists the subcommands in that root
61 func CommandsCmd(root *cmds.Command) *cmds.Command {
62 return &cmds.Command{
63 Helptext: cmds.HelpText{
64 Tagline: "List all available commands.",
65 ShortDescription: `Lists all available commands (and subcommands) and exits.`,
66 },
67 Subcommands: map[string]*cmds.Command{
68 "completion": CompletionCmd(root),
69 },
70 Options: []cmds.Option{
71 cmds.BoolOption(flagsOptionName, "f", "Show command flags"),
72 },
73 Extra: CreateCmdExtras(SetDoesNotUseRepo(true)),
74 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
75 rootCmd := cmd2outputCmd("ipfs", root)
76 rootCmd.showOpts, _ = req.Options[flagsOptionName].(bool)
77 return cmds.EmitOnce(res, &rootCmd)
78 },
79 Encoders: cmds.EncoderMap{
80 cmds.Text: func(req *cmds.Request) func(io.Writer) cmds.Encoder {
81 return func(w io.Writer) cmds.Encoder { return &commandEncoder{w} }
82 },
83 },
84 Type: Command{},
85 }
86 }
87
88 func cmd2outputCmd(name string, cmd *cmds.Command) Command {
89 opts := make([]Option, len(cmd.Options))
90 for i, opt := range cmd.Options {
91 opts[i] = Option{opt.Names()}
92 }
93
94 output := Command{
95 Name: name,
96 Subcommands: make([]Command, 0, len(cmd.Subcommands)),
97 Options: opts,
98 }
99
100 for name, sub := range cmd.Subcommands {
101 output.Subcommands = append(output.Subcommands, cmd2outputCmd(name, sub))
102 }
103
104 return output
105 }
106
107 func cmdPathStrings(cmd *Command, showOptions bool) []string {
108 var cmds []string
109
110 var recurse func(prefix string, cmd *Command)
111 recurse = func(prefix string, cmd *Command) {
112 newPrefix := prefix + cmd.Name
113 cmds = append(cmds, newPrefix)
114 if prefix != "" && showOptions {
115 for _, options := range cmd.Options {
116 var cmdOpts []string
117 for _, flag := range options.Names {
118 if len(flag) == 1 {
119 flag = "-" + flag
120 } else {
121 flag = "--" + flag
122 }
123 cmdOpts = append(cmdOpts, newPrefix+" "+flag)
124 }
125 cmds = append(cmds, strings.Join(cmdOpts, " / "))
126 }
127 }
128 for _, sub := range cmd.Subcommands {
129 recurse(newPrefix+" ", &sub)
130 }
131 }
132
133 recurse("", cmd)
134 slices.Sort(cmds)
135 return cmds
136 }
137
138 func CompletionCmd(root *cmds.Command) *cmds.Command {
139 return &cmds.Command{
140 Helptext: cmds.HelpText{
141 Tagline: "Generate shell completions.",
142 },
143 NoRemote: true,
144 Subcommands: map[string]*cmds.Command{
145 "bash": {
146 Helptext: cmds.HelpText{
147 Tagline: "Generate bash shell completions.",
148 ShortDescription: "Generates command completions for the bash shell.",
149 LongDescription: `
150 Generates command completions for the bash shell.
151
152 The simplest way to see it working is write the completions
153 to a file and then source it:
154
155 > ipfs commands completion bash > ipfs-completion.bash
156 > source ./ipfs-completion.bash
157
158 To install the completions permanently, they can be moved to
159 /etc/bash_completion.d or sourced from your ~/.bashrc file.
160 `,
161 },
162 NoRemote: true,
163 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
164 var buf bytes.Buffer
165 if err := writeBashCompletions(root, &buf); err != nil {
166 return err
167 }
168 res.SetLength(uint64(buf.Len()))
169 return res.Emit(&buf)
170 },
171 },
172 "zsh": {
173 Helptext: cmds.HelpText{
174 Tagline: "Generate zsh shell completions.",
175 ShortDescription: "Generates command completions for the zsh shell.",
176 LongDescription: `
177 Generates command completions for the zsh shell.
178
179 The simplest way to see it working is write the completions
180 to a file and then source it:
181
182 > ipfs commands completion zsh > ipfs-completion.zsh
183 > source ./ipfs-completion.zsh
184
185 To install the completions permanently, they can be moved to
186 /etc/zsh/completions or sourced from your ~/.zshrc file.
187 `,
188 },
189 NoRemote: true,
190 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
191 var buf bytes.Buffer
192 if err := writeZshCompletions(root, &buf); err != nil {
193 return err
194 }
195 res.SetLength(uint64(buf.Len()))
196 return res.Emit(&buf)
197 },
198 },
199 "fish": {
200 Helptext: cmds.HelpText{
201 Tagline: "Generate fish shell completions.",
202 ShortDescription: "Generates command completions for the fish shell.",
203 LongDescription: `
204 Generates command completions for the fish shell.
205
206 The simplest way to see it working is write the completions
207 to a file and then source it:
208
209 > ipfs commands completion fish > ipfs-completion.fish
210 > source ./ipfs-completion.fish
211
212 To install the completions permanently, they can be moved to
213 /etc/fish/completions or ~/.config/fish/completions or sourced from your ~/.config/fish/config.fish file.
214 `,
215 },
216 NoRemote: true,
217 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
218 var buf bytes.Buffer
219 if err := writeFishCompletions(root, &buf); err != nil {
220 return err
221 }
222 res.SetLength(uint64(buf.Len()))
223 return res.Emit(&buf)
224 },
225 },
226 },
227 }
228 }
229
230 type nonFatalError string
231
232 // streamResult is a helper function to stream results that possibly
233 // contain non-fatal errors. The helper function is allowed to panic
234 // on internal errors.
235 func streamResult(procVal func(any, io.Writer) nonFatalError) func(cmds.Response, cmds.ResponseEmitter) error {
236 return func(res cmds.Response, re cmds.ResponseEmitter) (rerr error) {
237 defer func() {
238 if r := recover(); r != nil {
239 rerr = fmt.Errorf("internal error: %v", r)
240 }
241 }()
242
243 var errors bool
244 for {
245 v, err := res.Next()
246 if err != nil {
247 if err == io.EOF {
248 break
249 }
250 rerr = err
251 return
252 }
253
254 errorMsg := procVal(v, os.Stdout)
255
256 if errorMsg != "" {
257 errors = true
258 fmt.Fprintf(os.Stderr, "%s\n", errorMsg)
259 }
260 }
261
262 if errors {
263 rerr = fmt.Errorf("errors while displaying some entries")
264 }
265 return
266 }
267 }