Skip to content

Commit 0060d5f

Browse files
committed
feat: programmatic shell completions
These are missing some of the features of the current hand-rolled completions, but: 1. Are less buggy. 2. Cover _all_ commands. 3. Don't need to be manually maintained (which we never do anyways). fixes ipfs#4551 fixes ipfs#8033
1 parent fcfe793 commit 0060d5f

File tree

7 files changed

+213
-982
lines changed

7 files changed

+213
-982
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ dependencies as well.
220220
We strongly recommend you use the [latest version of OSX FUSE](http://osxfuse.github.io/).
221221
(See https://github.com/ipfs/go-ipfs/issues/177)
222222
- Read [docs/fuse.md](docs/fuse.md) for more details on setting up FUSE (so that you can mount the filesystem).
223-
- Shell command completion is available in `misc/completion/ipfs-completion.bash`. Read [docs/command-completion.md](docs/command-completion.md) to learn how to install it.
223+
- Shell command completions can be generated with one of the `ipfs commands completion` subcommands. Read [docs/command-completion.md](docs/command-completion.md) to learn more.
224224
- See the [misc folder](https://github.com/ipfs/go-ipfs/tree/master/misc) for how to connect IPFS to systemd or whatever init system your distro uses.
225225

226226
### Updating go-ipfs

core/commands/commands.go

+42
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package commands
77

88
import (
9+
"bytes"
910
"fmt"
1011
"io"
1112
"os"
@@ -63,6 +64,9 @@ func CommandsCmd(root *cmds.Command) *cmds.Command {
6364
Tagline: "List all available commands.",
6465
ShortDescription: `Lists all available commands (and subcommands) and exits.`,
6566
},
67+
Subcommands: map[string]*cmds.Command{
68+
"completion": CompletionCmd(root),
69+
},
6670
Options: []cmds.Option{
6771
cmds.BoolOption(flagsOptionName, "f", "Show command flags"),
6872
},
@@ -131,6 +135,44 @@ func cmdPathStrings(cmd *Command, showOptions bool) []string {
131135
return cmds
132136
}
133137

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+
},
173+
}
174+
}
175+
134176
type nonFatalError string
135177

136178
// streamResult is a helper function to stream results that possibly

core/commands/commands_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ func TestROCommands(t *testing.T) {
2222
"/block/stat",
2323
"/cat",
2424
"/commands",
25+
"/commands/completion",
26+
"/commands/completion/bash",
2527
"/dag",
2628
"/dag/get",
2729
"/dag/resolve",
@@ -89,6 +91,8 @@ func TestCommands(t *testing.T) {
8991
"/bootstrap/rm/all",
9092
"/cat",
9193
"/commands",
94+
"/commands/completion",
95+
"/commands/completion/bash",
9296
"/config",
9397
"/config/edit",
9498
"/config/replace",

core/commands/completion.go

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package commands
2+
3+
import (
4+
"io"
5+
"sort"
6+
"text/template"
7+
8+
cmds "github.com/ipfs/go-ipfs-cmds"
9+
)
10+
11+
type completionCommand struct {
12+
Name string
13+
Subcommands []*completionCommand
14+
ShortFlags []string
15+
ShortOptions []string
16+
LongFlags []string
17+
LongOptions []string
18+
}
19+
20+
func commandToCompletions(name string, cmd *cmds.Command) *completionCommand {
21+
parsed := &completionCommand{
22+
Name: name,
23+
}
24+
for name, subCmd := range cmd.Subcommands {
25+
parsed.Subcommands = append(parsed.Subcommands, commandToCompletions(name, subCmd))
26+
}
27+
sort.Slice(parsed.Subcommands, func(i, j int) bool {
28+
return parsed.Subcommands[i].Name < parsed.Subcommands[j].Name
29+
})
30+
31+
for _, opt := range cmd.Options {
32+
if opt.Type() == cmds.Bool {
33+
parsed.LongFlags = append(parsed.LongFlags, opt.Name())
34+
for _, name := range opt.Names() {
35+
if len(name) == 1 {
36+
parsed.ShortFlags = append(parsed.ShortFlags, name)
37+
break
38+
}
39+
}
40+
} else {
41+
parsed.LongOptions = append(parsed.LongOptions, opt.Name())
42+
for _, name := range opt.Names() {
43+
if len(name) == 1 {
44+
parsed.ShortOptions = append(parsed.ShortOptions, name)
45+
break
46+
}
47+
}
48+
}
49+
}
50+
sort.Slice(parsed.LongFlags, func(i, j int) bool {
51+
return parsed.LongFlags[i] < parsed.LongFlags[j]
52+
})
53+
sort.Slice(parsed.ShortFlags, func(i, j int) bool {
54+
return parsed.ShortFlags[i] < parsed.ShortFlags[j]
55+
})
56+
sort.Slice(parsed.LongOptions, func(i, j int) bool {
57+
return parsed.LongOptions[i] < parsed.LongOptions[j]
58+
})
59+
sort.Slice(parsed.ShortOptions, func(i, j int) bool {
60+
return parsed.ShortOptions[i] < parsed.ShortOptions[j]
61+
})
62+
return parsed
63+
}
64+
65+
var bashCompletionTemplate *template.Template
66+
67+
func init() {
68+
commandTemplate := template.Must(template.New("command").Parse(`
69+
while [[ ${index} -lt ${COMP_CWORD} ]]; do
70+
case "${COMP_WORDS[index]}" in
71+
-*)
72+
let index++
73+
continue
74+
;;
75+
{{ range .Subcommands }}
76+
"{{ .Name }}")
77+
let index++
78+
{{ template "command" . }}
79+
return 0
80+
;;
81+
{{ end }}
82+
esac
83+
break
84+
done
85+
86+
if [[ "${word}" == -* ]]; then
87+
{{ if .ShortFlags -}}
88+
_ipfs_compgen -W $'{{ range .ShortFlags }}-{{.}} \n{{end}}' -- "${word}"
89+
{{ end -}}
90+
{{- if .ShortOptions -}}
91+
_ipfs_compgen -S = -W $'{{ range .ShortOptions }}-{{.}}\n{{end}}' -- "${word}"
92+
{{ end -}}
93+
{{- if .LongFlags -}}
94+
_ipfs_compgen -W $'{{ range .LongFlags }}--{{.}} \n{{end}}' -- "${word}"
95+
{{ end -}}
96+
{{- if .LongOptions -}}
97+
_ipfs_compgen -S = -W $'{{ range .LongOptions }}--{{.}}\n{{end}}' -- "${word}"
98+
{{ end -}}
99+
return 0
100+
fi
101+
102+
while [[ ${index} -lt ${COMP_CWORD} ]]; do
103+
if [[ "${COMP_WORDS[index]}" != -* ]]; then
104+
let argidx++
105+
fi
106+
let index++
107+
done
108+
109+
{{- if .Subcommands }}
110+
if [[ "${argidx}" -eq 0 ]]; then
111+
_ipfs_compgen -W $'{{ range .Subcommands }}{{.Name}} \n{{end}}' -- "${word}"
112+
fi
113+
{{ end -}}
114+
`))
115+
116+
bashCompletionTemplate = template.Must(commandTemplate.New("root").Parse(`#!/bin/bash
117+
118+
_ipfs_compgen() {
119+
local oldifs="$IFS"
120+
IFS=$'\n'
121+
while read -r line; do
122+
COMPREPLY+=("$line")
123+
done < <(compgen "$@")
124+
IFS="$oldifs"
125+
}
126+
127+
_ipfs() {
128+
COMPREPLY=()
129+
local index=1
130+
local argidx=0
131+
local word="${COMP_WORDS[COMP_CWORD]}"
132+
{{ template "command" . }}
133+
}
134+
complete -o nosort -o nospace -o default -F _ipfs ipfs
135+
`))
136+
}
137+
138+
// writeBashCompletions generates a bash completion script for the given command tree.
139+
func writeBashCompletions(cmd *cmds.Command, out io.Writer) error {
140+
cmds := commandToCompletions("ipfs", cmd)
141+
return bashCompletionTemplate.Execute(out, cmds)
142+
}

docs/command-completion.md

+9-23
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,15 @@
1-
Command Completion
2-
==================
1+
# Command Completion
32

4-
Shell command completion is provided by the script at
5-
[/misc/completion/ipfs-completion.bash](../misc/completion/ipfs-completion.bash).
3+
Shell command completions can be generated by running one of the `ipfs commands completions`
4+
sub-commands.
65

6+
The simplest way to see it working is write the completions
7+
to a file and then source it:
78

8-
Installation
9-
------------
10-
The simplest way to see it working is to run
11-
`source misc/completion/ipfs-completion.bash` straight from your shell. This
12-
is only temporary and to fully enable it, you'll have to follow one of the steps
13-
below.
14-
15-
### Bash on Linux
16-
For bash, completion can be enabled in a couple of ways. One is to copy the
17-
completion script to the directory `~/.ipfs/` and then in the file
18-
`~/.bash_completion` add
199
```bash
20-
source ~/.ipfs/ipfs-completion.bash
10+
> ipfs commands completion bash > ipfs-completion.bash
11+
> source ./ipfs-completion.bash
2112
```
22-
It will automatically be loaded the next time bash is loaded.
23-
To enable ipfs command completion globally on your system you may also
24-
copy the completion script to `/etc/bash_completion.d/`.
25-
2613

27-
Additional References
28-
---------------------
29-
* https://www.debian-administration.org/article/316/An_introduction_to_bash_completion_part_1
14+
To install the completions permanently, they can be moved to
15+
`/etc/bash_completion.d` or sourced from your `~/.bashrc` file.

0 commit comments

Comments
 (0)