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
+