Skip to content

Commit ced3483

Browse files
feat: add zsh completions (#10040)
Co-authored-by: Henrique Dias <[email protected]>
1 parent cc79eeb commit ced3483

File tree

6 files changed

+99
-2
lines changed

6 files changed

+99
-2
lines changed

.github/workflows/gotest.yml

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ jobs:
3535
go-version: 1.19.x
3636
- name: Check out Kubo
3737
uses: actions/checkout@v3
38+
- name: Install missing tools
39+
run: sudo apt update && sudo apt install -y zsh
3840
- name: Restore Go cache
3941
uses: protocol/cache-go-action@v1
4042
with:

core/commands/commands.go

+28-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
"sort"
1414
"strings"
1515

16-
"github.com/ipfs/go-ipfs-cmds"
16+
cmds "github.com/ipfs/go-ipfs-cmds"
1717
)
1818

1919
type commandEncoder struct {
@@ -169,6 +169,33 @@ To install the completions permanently, they can be moved to
169169
return res.Emit(&buf)
170170
},
171171
},
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+
},
172199
"fish": {
173200
Helptext: cmds.HelpText{
174201
Tagline: "Generate fish shell completions.",

core/commands/commands_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func TestROCommands(t *testing.T) {
2525
"/commands/completion",
2626
"/commands/completion/bash",
2727
"/commands/completion/fish",
28+
"/commands/completion/zsh",
2829
"/dag",
2930
"/dag/get",
3031
"/dag/resolve",
@@ -102,6 +103,7 @@ func TestCommands(t *testing.T) {
102103
"/commands/completion",
103104
"/commands/completion/bash",
104105
"/commands/completion/fish",
106+
"/commands/completion/zsh",
105107
"/config",
106108
"/config/edit",
107109
"/config/profile",

core/commands/completion.go

+28-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func commandToCompletions(name string, fullName string, cmd *cmds.Command) *comp
8383
return parsed
8484
}
8585

86-
var bashCompletionTemplate, fishCompletionTemplate *template.Template
86+
var bashCompletionTemplate, fishCompletionTemplate, zshCompletionTemplate *template.Template
8787

8888
func init() {
8989
commandTemplate := template.Must(template.New("command").Parse(`
@@ -153,6 +153,28 @@ _ipfs() {
153153
{{ template "command" . }}
154154
}
155155
complete -o nosort -o nospace -o default -F _ipfs ipfs
156+
`))
157+
158+
zshCompletionTemplate = template.Must(commandTemplate.New("root").Parse(`#!bin/zsh
159+
autoload bashcompinit
160+
bashcompinit
161+
_ipfs_compgen() {
162+
local oldifs="$IFS"
163+
IFS=$'\n'
164+
while read -r line; do
165+
COMPREPLY+=("$line")
166+
done < <(compgen "$@")
167+
IFS="$oldifs"
168+
}
169+
170+
_ipfs() {
171+
COMPREPLY=()
172+
local index=1
173+
local argidx=0
174+
local word="${COMP_WORDS[COMP_CWORD]}"
175+
{{ template "command" . }}
176+
}
177+
complete -o nosort -o nospace -o default -F _ipfs ipfs
156178
`))
157179

158180
fishCommandTemplate := template.Must(template.New("command").Parse(`
@@ -221,3 +243,8 @@ func writeFishCompletions(cmd *cmds.Command, out io.Writer) error {
221243
cmds := commandToCompletions("ipfs", "", cmd)
222244
return fishCompletionTemplate.Execute(out, cmds)
223245
}
246+
247+
func writeZshCompletions(cmd *cmds.Command, out io.Writer) error {
248+
cmds := commandToCompletions("ipfs", "", cmd)
249+
return zshCompletionTemplate.Execute(out, cmds)
250+
}

docs/command-completion.md

+13
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,16 @@ The simplest way to use the completions logic:
2424

2525
To install the completions permanently, they can be moved to
2626
`/etc/fish/completions` or `~/.config/fish/completions` or sourced from your `~/.config/fish/config.fish` file.
27+
28+
## ZSH
29+
30+
The zsh shell is also supported:
31+
32+
The simplest way to "eval" the completions logic:
33+
34+
```bash
35+
> eval "$(ipfs commands completion zsh)"
36+
```
37+
38+
To install the completions permanently, they can be moved to
39+
`/etc/bash_completion.d` or sourced from your `~/.zshrc` file.

test/cli/completion_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,29 @@ func TestBashCompletion(t *testing.T) {
2929
assert.NoError(t, res.Err)
3030
})
3131
}
32+
33+
func TestZshCompletion(t *testing.T) {
34+
t.Parallel()
35+
h := harness.NewT(t)
36+
node := h.NewNode()
37+
38+
res := node.IPFS("commands", "completion", "zsh")
39+
40+
length := len(res.Stdout.String())
41+
if length < 100 {
42+
t.Fatalf("expected a long Bash completion file, but got one of length %d", length)
43+
}
44+
45+
t.Run("completion file can be loaded in bash", func(t *testing.T) {
46+
RequiresLinux(t)
47+
48+
completionFile := h.WriteToTemp(res.Stdout.String())
49+
res = h.Runner.Run(harness.RunRequest{
50+
Path: "zsh",
51+
Args: []string{"-c", fmt.Sprintf("autoload -Uz compinit && compinit && source %s && echo -E $_comps[ipfs]", completionFile)},
52+
})
53+
54+
assert.NoError(t, res.Err)
55+
assert.NotEmpty(t, res.Stdout.String())
56+
})
57+
}

0 commit comments

Comments
 (0)