@cryptotaxi247 / kubo / commits / 2fd4e197a

feat: Add command line completion for fish

Andreas Källberg committed Oct 23, 2022 at 03:31 UTC 2fd4e197a032c439eed25efec3ce3995d2e1ef04
6 files changed +146 -6
.circleci/main.yml
+1 -1
@@ -160,7 +160,7 @@ jobs:
160 tar xfz go1.19.1.linux-amd64.tar.gz
161 echo "export PATH=$(pwd)/go/bin:\$PATH" >> ~/.bashrc
162 - run: go version
163 - - run: sudo apt install socat net-tools
163 + - run: sudo apt install socat net-tools fish
164 - checkout
165
166 - run:
core/commands/commands.go
+27
@@ -169,6 +169,33 @@ To install the completions permanently, they can be moved to
169 return res.Emit(&buf)
170 },
171 },
172 + "fish": {
173 + Helptext: cmds.HelpText{
174 + Tagline: "Generate fish shell completions.",
175 + ShortDescription: "Generates command completions for the fish shell.",
176 + LongDescription: `
177 +Generates command completions for the fish 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 fish > ipfs-completion.fish
183 + > source ./ipfs-completion.fish
184 +
185 +To install the completions permanently, they can be moved to
186 +/etc/fish/completions or ~/.config/fish/completions or sourced from your ~/.config/fish/config.fish 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 := writeFishCompletions(root, &buf); err != nil {
193 + return err
194 + }
195 + res.SetLength(uint64(buf.Len()))
196 + return res.Emit(&buf)
197 + },
198 + },
199 },
200 }
201 }
core/commands/commands_test.go
+2
@@ -24,6 +24,7 @@ func TestROCommands(t *testing.T) {
24 "/commands",
25 "/commands/completion",
26 "/commands/completion/bash",
27 + "/commands/completion/fish",
28 "/dag",
29 "/dag/get",
30 "/dag/resolve",
@@ -99,6 +100,7 @@ func TestCommands(t *testing.T) {
100 "/commands",
101 "/commands/completion",
102 "/commands/completion/bash",
103 + "/commands/completion/fish",
104 "/config",
105 "/config/edit",
106 "/config/profile",
core/commands/completion.go
+87 -5
@@ -10,41 +10,62 @@ import (
10
11 type completionCommand struct {
12 Name string
13 + FullName string
14 + Description string
15 Subcommands []*completionCommand
16 + Flags []*singleOption
17 + Options []*singleOption
18 ShortFlags []string
19 ShortOptions []string
20 LongFlags []string
21 LongOptions []string
22 + IsFinal bool
23 }
24
20 -func commandToCompletions(name string, cmd *cmds.Command) *completionCommand {
25 +type singleOption struct {
26 + LongNames []string
27 + ShortNames []string
28 + Description string
29 +}
30 +
31 +func commandToCompletions(name string, fullName string, cmd *cmds.Command) *completionCommand {
32 parsed := &completionCommand{
22 - Name: name,
33 + Name: name,
34 + FullName: fullName,
35 + Description: cmd.Helptext.Tagline,
36 + IsFinal: len(cmd.Subcommands) == 0,
37 }
38 for name, subCmd := range cmd.Subcommands {
25 - parsed.Subcommands = append(parsed.Subcommands, commandToCompletions(name, subCmd))
39 + parsed.Subcommands = append(parsed.Subcommands,
40 + commandToCompletions(name, fullName+" "+name, subCmd))
41 }
42 sort.Slice(parsed.Subcommands, func(i, j int) bool {
43 return parsed.Subcommands[i].Name < parsed.Subcommands[j].Name
44 })
45
46 for _, opt := range cmd.Options {
47 + flag := &singleOption{Description: opt.Description()}
48 + flag.LongNames = append(flag.LongNames, opt.Name())
49 if opt.Type() == cmds.Bool {
50 parsed.LongFlags = append(parsed.LongFlags, opt.Name())
51 for _, name := range opt.Names() {
52 if len(name) == 1 {
53 parsed.ShortFlags = append(parsed.ShortFlags, name)
54 + flag.ShortNames = append(flag.ShortNames, name)
55 break
56 }
57 }
58 + parsed.Flags = append(parsed.Flags, flag)
59 } else {
60 parsed.LongOptions = append(parsed.LongOptions, opt.Name())
61 for _, name := range opt.Names() {
62 if len(name) == 1 {
63 parsed.ShortOptions = append(parsed.ShortOptions, name)
64 + flag.ShortNames = append(flag.ShortNames, name)
65 break
66 }
67 }
68 + parsed.Options = append(parsed.Options, flag)
69 }
70 }
71 sort.Slice(parsed.LongFlags, func(i, j int) bool {
@@ -62,7 +83,7 @@ func commandToCompletions(name string, cmd *cmds.Command) *completionCommand {
83 return parsed
84 }
85
65 -var bashCompletionTemplate *template.Template
86 +var bashCompletionTemplate, fishCompletionTemplate *template.Template
87
88 func init() {
89 commandTemplate := template.Must(template.New("command").Parse(`
@@ -133,10 +154,71 @@ _ipfs() {
154 }
155 complete -o nosort -o nospace -o default -F _ipfs ipfs
156 `))
157 +
158 + fishCommandTemplate := template.Must(template.New("command").Parse(`
159 +{{- if .IsFinal -}}
160 +complete -c ipfs -n '__fish_ipfs_seen_all_subcommands_from{{ .FullName }}' -F
161 +{{ end -}}
162 +{{- range .Flags -}}
163 + complete -c ipfs -n '__fish_ipfs_seen_all_subcommands_from{{ $.FullName }}' {{ range .ShortNames }}-s {{.}} {{end}}{{ range .LongNames }}-l {{.}} {{end}}-d "{{ .Description }}"
164 +{{ end -}}
165 +{{- range .Options -}}
166 + complete -c ipfs -n '__fish_ipfs_seen_all_subcommands_from{{ $.FullName }}' -r {{ range .ShortNames }}-s {{.}} {{end}}{{ range .LongNames }}-l {{.}} {{end}}-d "{{ .Description }}"
167 +{{ end -}}
168 +
169 +{{- range .Subcommands }}
170 +#{{ .FullName }}
171 +complete -c ipfs -n '__fish_ipfs_use_subcommand{{ .FullName }}' -a {{ .Name }} -d "{{ .Description }}"
172 +{{ template "command" . }}
173 +{{ end -}}
174 + `))
175 + fishCompletionTemplate = template.Must(fishCommandTemplate.New("root").Parse(`#!/usr/bin/env fish
176 +function __fish_ipfs_seen_all_subcommands_from
177 + set -l cmd (commandline -poc)
178 + set -e cmd[1]
179 + for c in $argv
180 + if not contains -- $c $cmd
181 + return 1
182 + end
183 + end
184 + return 0
185 +end
186 +
187 +function __fish_ipfs_use_subcommand
188 + set -e argv[-1]
189 + set -l cmd (commandline -poc)
190 + set -e cmd[1]
191 + for i in $cmd
192 + switch $i
193 + case '-*'
194 + continue
195 + case $argv[1]
196 + set argv $argv[2..]
197 + continue
198 + case '*'
199 + return 1
200 + end
201 + end
202 + test -z "$argv"
203 +end
204 +
205 +complete -c ipfs -l help -d "Show the full command help text."
206 +
207 +complete -c ipfs --keep-order --no-files
208 +
209 +{{ template "command" . }}
210 +`))
211 +
212 }
213
214 // writeBashCompletions generates a bash completion script for the given command tree.
215 func writeBashCompletions(cmd *cmds.Command, out io.Writer) error {
140 - cmds := commandToCompletions("ipfs", cmd)
216 + cmds := commandToCompletions("ipfs", "", cmd)
217 return bashCompletionTemplate.Execute(out, cmds)
218 }
219 +
220 +// writeFishCompletions generates a fish completion script for the given command tree.
221 +func writeFishCompletions(cmd *cmds.Command, out io.Writer) error {
222 + cmds := commandToCompletions("ipfs", "", cmd)
223 + return fishCompletionTemplate.Execute(out, cmds)
224 +}
docs/command-completion.md
+13
@@ -11,3 +11,16 @@ The simplest way to "eval" the completions logic:
11
12 To install the completions permanently, they can be moved to
13 `/etc/bash_completion.d` or sourced from your `~/.bashrc` file.
14 +
15 +## Fish
16 +
17 +The fish shell is also supported:
18 +
19 +The simplest way to use the completions logic:
20 +
21 +```bash
22 +> ipfs commands completion fish | source
23 +```
24 +
25 +To install the completions permanently, they can be moved to
26 +`/etc/fish/completions` or `~/.config/fish/completions` or sourced from your `~/.config/fish/config.fish` file.
test/sharness/t0012-completion-fish.sh new
+16
@@ -0,0 +1,16 @@
1 +#!/usr/bin/env bash
2 +
3 +test_description="Test generated fish completions"
4 +
5 +. lib/test-lib.sh
6 +
7 +test_expect_success "'ipfs commands completion fish' succeeds" '
8 + ipfs commands completion fish > completions.fish
9 +'
10 +
11 +test_expect_success "generated completions completes 'ipfs version'" '
12 + fish -c "source completions.fish && complete -C \"ipfs ver\" | grep -q \"version.Show IPFS version information.\" "
13 +'
14 +
15 +test_done
16 +