Skip to content

Commit 4181598

Browse files
authored
Add Fish Shell Completions (#601)
* Add fish completion script * Add support for fish completions * Add documentation for use of fish shell completions * Fix fish completion empty value check * Fix isOwnCommand when command descriptions are present * Fix restic command forwarding with profile prefix
1 parent b6693a3 commit 4181598

8 files changed

Lines changed: 137 additions & 11 deletions

File tree

commands.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ func getOwnCommands() []ownCommand {
7979
"--json-schema [--version 0.15] [v1|v2]": "generate a JSON schema that validates resticprofile configuration files in YAML or JSON format",
8080
"--bash-completion": "generate a shell completion script for bash",
8181
"--zsh-completion": "generate a shell completion script for zsh",
82+
"--fish-completion": "generate a shell completion script for fish",
8283
},
8384
},
8485
// commands that need the configuration
@@ -200,7 +201,7 @@ func completeCommand(output io.Writer, ctx commandContext) error {
200201

201202
// Parse requester as first argument. Format "[kind]:v[version]", e.g. "bash:v1"
202203
if len(args) > 0 {
203-
matcher := regexp.MustCompile(`^(bash|zsh):v(\d+)$`)
204+
matcher := regexp.MustCompile(`^(bash|zsh|fish):v(\d+)$`)
204205
if matches := matcher.FindStringSubmatch(args[0]); matches != nil {
205206
requester = matches[1]
206207
if v, err := strconv.Atoi(matches[2]); err == nil {
@@ -220,7 +221,12 @@ func completeCommand(output io.Writer, ctx commandContext) error {
220221
return nil
221222
}
222223

223-
completions := NewCompleter(ctx.ownCommands.All(), DefaultFlagsLoader).Complete(args)
224+
includeDescription := false
225+
if requester == "fish" {
226+
includeDescription = true
227+
}
228+
229+
completions := NewCompleter(ctx.ownCommands.All(), DefaultFlagsLoader, includeDescription).Complete(args)
224230
if len(completions) > 0 {
225231
for _, completion := range completions {
226232
fmt.Fprintln(output, completion)

commands_generate.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ var bashCompletionScript string
2626
//go:embed contrib/completion/zsh-completion.sh
2727
var zshCompletionScript string
2828

29+
//go:embed contrib/completion/fish-completion.fish
30+
var fishCompletionScript string
31+
2932
func generateCommand(output io.Writer, ctx commandContext) (err error) {
3033
args := ctx.request.arguments
3134
// enforce no-log
@@ -44,6 +47,8 @@ func generateCommand(output io.Writer, ctx commandContext) (err error) {
4447
err = randomKey(output, ctx)
4548
} else if slices.Contains(args, "--zsh-completion") {
4649
_, err = fmt.Fprintln(output, zshCompletionScript)
50+
} else if slices.Contains(args, "--fish-completion") {
51+
_, err = fmt.Fprintln(output, fishCompletionScript)
4752
} else {
4853
err = fmt.Errorf("nothing to generate for: %s", strings.Join(args, ", "))
4954
}

commands_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ _ = 0
168168
}
169169

170170
func TestCompleteCall(t *testing.T) {
171-
completer := NewCompleter(ownCommands.All(), DefaultFlagsLoader)
171+
completer := NewCompleter(ownCommands.All(), DefaultFlagsLoader, false)
172172
completer.init(nil)
173173
newline := fmt.Sprintln("")
174174
expectedFlags := strings.Join(completer.completeFlagSet(""), newline) + newline
@@ -180,13 +180,18 @@ func TestCompleteCall(t *testing.T) {
180180
expectedCommands := strings.Join(commandNames, newline) + newline +
181181
RequestResticCompletion + newline
182182

183+
completerWithDescriptions := NewCompleter(ownCommands.All(), DefaultFlagsLoader, true)
184+
completerWithDescriptions.init(nil)
185+
expectedFlagsWithDescriptions := strings.Join(completerWithDescriptions.completeFlagSet(""), newline) + newline
186+
183187
testTable := []struct {
184188
args []string
185189
expected string
186190
}{
187191
{args: []string{"--"}, expected: expectedFlags},
188192
{args: []string{"--config=does-not-exist", ""}, expected: expectedCommands},
189193
{args: []string{"bash:v1", "--"}, expected: expectedFlags},
194+
{args: []string{"fish:v1", "--"}, expected: expectedFlagsWithDescriptions},
190195
{args: []string{"bash:v10", "--"}, expected: ""},
191196
{args: []string{"zsh:v1", "--"}, expected: ""},
192197
}
@@ -225,6 +230,13 @@ func TestGenerateCommand(t *testing.T) {
225230
assert.Contains(t, zshCompletionScript, "#!/usr/bin/env zsh")
226231
})
227232

233+
t.Run("--fish-completion", func(t *testing.T) {
234+
buffer.Reset()
235+
assert.Nil(t, generateCommand(buffer, contextWithArguments([]string{"--fish-completion"})))
236+
assert.Equal(t, strings.TrimSpace(fishCompletionScript), strings.TrimSpace(buffer.String()))
237+
assert.Contains(t, fishCompletionScript, "#!/usr/bin/env fish")
238+
})
239+
228240
t.Run("--config-reference", func(t *testing.T) {
229241
buffer.Reset()
230242
assert.NoError(t, generateCommand(buffer, contextWithArguments([]string{"--config-reference", "--to", t.TempDir()})))

complete.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type Completer struct {
2929
ownCommands []ownCommand
3030
profiles []string
3131
enableProfilePrefixes bool
32+
includeDescription bool
3233
}
3334

3435
type FlagsLoader func(args []string) *pflag.FlagSet
@@ -38,8 +39,8 @@ func DefaultFlagsLoader(args []string) (flags *pflag.FlagSet) {
3839
return
3940
}
4041

41-
func NewCompleter(commands []ownCommand, loader FlagsLoader) *Completer {
42-
return &Completer{ownCommands: commands, loader: loader}
42+
func NewCompleter(commands []ownCommand, loader FlagsLoader, includeDescription bool) *Completer {
43+
return &Completer{ownCommands: commands, loader: loader, includeDescription: includeDescription}
4344
}
4445

4546
func (c *Completer) init(args []string) {
@@ -63,10 +64,15 @@ func (c *Completer) init(args []string) {
6364
}
6465

6566
func (c *Completer) formatFlag(flag *pflag.Flag, shorthand bool) string {
67+
description := ""
68+
if c.includeDescription {
69+
description = fmt.Sprintf("\t%s", flag.Usage)
70+
}
71+
6672
if shorthand {
67-
return fmt.Sprintf("-%s", flag.Shorthand)
73+
return fmt.Sprintf("-%s%s", flag.Shorthand, description)
6874
} else {
69-
return fmt.Sprintf("--%s", flag.Name)
75+
return fmt.Sprintf("--%s%s", flag.Name, description)
7076
}
7177
}
7278

@@ -169,7 +175,10 @@ func (c *Completer) completeProfileNamePrefixes(word string) (completions []stri
169175
}
170176

171177
func (c *Completer) formatOwnCommand(command ownCommand) string {
172-
return command.name
178+
if !c.includeDescription {
179+
return command.name
180+
}
181+
return fmt.Sprintf("%s\t%s", command.name, command.description)
173182
}
174183

175184
func (c *Completer) completeOwnCommands(word string) (completions []string) {
@@ -258,8 +267,11 @@ func (c *Completer) toCompletionsWithProfilePrefix(completions []string, profile
258267
}
259268

260269
func (c *Completer) isOwnCommand(command string, configurationLoaded bool) bool {
270+
//strip description, if any
271+
commandName := strings.SplitN(command, "\t", 2)[0]
272+
261273
for _, commandDef := range c.ownCommands {
262-
if commandDef.name == command && commandDef.needConfiguration == configurationLoaded {
274+
if commandDef.name == commandName && commandDef.needConfiguration == configurationLoaded {
263275
return true
264276
}
265277
}

complete_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
)
1414

1515
func TestCompleter(t *testing.T) {
16-
completer := NewCompleter(ownCommands.All(), DefaultFlagsLoader)
16+
completer := NewCompleter(ownCommands.All(), DefaultFlagsLoader, false)
1717
completer.init(nil)
1818

1919
expectedProfiles := func() []string {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/usr/bin/env fish
2+
#
3+
# resticprofile fish completion script
4+
# Usage: see https://fishshell.com/docs/current/completions.html#where-to-put-completions
5+
6+
function __resticprofile_completion
7+
set --local full_cmdline (commandline -x)
8+
set --local cmdline_to_cursor \
9+
(string split -- " " (string escape -- (commandline -cx; commandline -ct)))
10+
set --local current_token_pos (math (count $cmdline_to_cursor) - 1)
11+
12+
#send commandline to 'resticprofile complete' in the format it expects
13+
set --local completions ("$full_cmdline[1]" complete "fish:v1" "__POS:$current_token_pos" "$full_cmdline[2..]")
14+
15+
if test (count $completions) = 0
16+
return
17+
end
18+
19+
#handle the directive returned from resticprofile
20+
switch $completions[-1]
21+
case "__complete_file"
22+
set --erase completions[-1]
23+
24+
set --local file $full_cmdline[-1]
25+
#if file starts with '-', remove it
26+
test (string sub --length 1 -- "$file") = "-"; and set file ""
27+
28+
#do path completion
29+
set --append completions (__fish_complete_path "$file")
30+
31+
case "*__complete_restic"
32+
#string match --regex returns list where first element is the whole string
33+
#and the rest are capture group matches.
34+
set --local prefix (string match --regex '^(.*)\.' -- "$completions[-1]")[2]
35+
set --local suffix (string match --regex '\.([^.]+)$' -- "$completions[-1]")[2]
36+
set --erase completions[-1]
37+
38+
#This removes profile prefixes before forwarding the completion to restic
39+
set --local restic_words
40+
for word in $cmdline_to_cursor[2..]
41+
if test (string match --regex ".*\/.*" -- "$word")
42+
set --append restic_words (string unescape -- "$word")
43+
else
44+
#take everything after last dot, if there is one
45+
set --append restic_words (string match --regex '([^.]+)$' -- $word)[2]
46+
end
47+
end
48+
49+
#Add a space if the cursor is past the last token so that 'complete' doesn't
50+
#return a command that's already fully typed out
51+
if test "$restic_words[-1]" = "''"
52+
set restic_words[-1] " "
53+
end
54+
55+
#get restic completions
56+
set --local restic_values (complete --do-complete "restic $restic_words")
57+
58+
if test (count $restic_values) = 0
59+
for x in $completions
60+
echo $x
61+
end
62+
return
63+
end
64+
65+
for value in $restic_values
66+
#remove empty values
67+
string match --quiet --regex '^[\r\n\t ]+$' -- "$value"; and continue
68+
69+
#add prefix back to completion if applicable
70+
if test -n $prefix && test "$prefix" != "$suffix"
71+
set --append completions "$prefix.$value"
72+
else
73+
set --append completions "$value"
74+
end
75+
end
76+
end
77+
78+
for x in $completions
79+
echo $x
80+
end
81+
82+
end
83+
84+
complete --command resticprofile --no-files --arguments "(__resticprofile_completion)"

docs/content/configuration/getting_started/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ Flags:
344344
--json-schema [--version 0.15] [v1|v2] generate a JSON schema that validates resticprofile configuration files in YAML or JSON format
345345
--random-key [size] generate a cryptographically secure random key to use as a restic keyfile (size defaults to 1024 when omitted)
346346
--zsh-completion generate a shell completion script for zsh
347+
--fish-completion generate a shell completion script for fish
347348
348349
```
349350

docs/content/installation/shell.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ weight: 100
44
---
55

66

7-
Shell command line completions are provided for `bash` and `zsh`.
7+
Shell command line completions are provided for `bash`, `fish`, and `zsh`.
88

99
To load the command completions in shell, use:
1010

1111
```shell
1212
# bash
1313
eval "$(resticprofile generate --bash-completion)"
1414

15+
# fish
16+
resticprofile generate --fish-completion | source
17+
1518
# zsh
1619
eval "$(resticprofile generate --zsh-completion)"
1720
```
@@ -21,4 +24,7 @@ To install them permanently:
2124
```shell
2225
resticprofile generate --bash-completion > /etc/bash_completion.d/resticprofile
2326
chmod +x /etc/bash_completion.d/resticprofile
27+
28+
resticprofile generate --fish-completion > /etc/fish/completions/resticprofile.fish
29+
chmod +x /etc/fish/completions/resticprofile.fish
2430
```

0 commit comments

Comments
 (0)