From 2fd4e197a032c439eed25efec3ce3995d2e1ef04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20K=C3=A4llberg?= Date: Sun, 23 Oct 2022 03:31:47 +0200 Subject: [PATCH] feat: Add command line completion for fish --- .circleci/main.yml | 2 +- core/commands/commands.go | 27 ++++++++ core/commands/commands_test.go | 2 + core/commands/completion.go | 92 ++++++++++++++++++++++++-- docs/command-completion.md | 13 ++++ test/sharness/t0012-completion-fish.sh | 16 +++++ 6 files changed, 146 insertions(+), 6 deletions(-) create mode 100755 test/sharness/t0012-completion-fish.sh diff --git a/.circleci/main.yml b/.circleci/main.yml index efae6d945..67f3e711b 100644 --- a/.circleci/main.yml +++ b/.circleci/main.yml @@ -160,7 +160,7 @@ jobs: tar xfz go1.19.1.linux-amd64.tar.gz echo "export PATH=$(pwd)/go/bin:\$PATH" >> ~/.bashrc - run: go version - - run: sudo apt install socat net-tools + - run: sudo apt install socat net-tools fish - checkout - run: diff --git a/core/commands/commands.go b/core/commands/commands.go index 5f49c8b0d..794e1a592 100644 --- a/core/commands/commands.go +++ b/core/commands/commands.go @@ -169,6 +169,33 @@ To install the completions permanently, they can be moved to return res.Emit(&buf) }, }, + "fish": { + Helptext: cmds.HelpText{ + Tagline: "Generate fish shell completions.", + ShortDescription: "Generates command completions for the fish shell.", + LongDescription: ` +Generates command completions for the fish shell. + +The simplest way to see it working is write the completions +to a file and then source it: + + > ipfs commands completion fish > ipfs-completion.fish + > source ./ipfs-completion.fish + +To install the completions permanently, they can be moved to +/etc/fish/completions or ~/.config/fish/completions or sourced from your ~/.config/fish/config.fish file. +`, + }, + NoRemote: true, + Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error { + var buf bytes.Buffer + if err := writeFishCompletions(root, &buf); err != nil { + return err + } + res.SetLength(uint64(buf.Len())) + return res.Emit(&buf) + }, + }, }, } } diff --git a/core/commands/commands_test.go b/core/commands/commands_test.go index e40a146d6..ec4cc1bb6 100644 --- a/core/commands/commands_test.go +++ b/core/commands/commands_test.go @@ -24,6 +24,7 @@ func TestROCommands(t *testing.T) { "/commands", "/commands/completion", "/commands/completion/bash", + "/commands/completion/fish", "/dag", "/dag/get", "/dag/resolve", @@ -99,6 +100,7 @@ func TestCommands(t *testing.T) { "/commands", "/commands/completion", "/commands/completion/bash", + "/commands/completion/fish", "/config", "/config/edit", "/config/profile", diff --git a/core/commands/completion.go b/core/commands/completion.go index ed1865d26..c4fb1ba41 100644 --- a/core/commands/completion.go +++ b/core/commands/completion.go @@ -10,41 +10,62 @@ import ( type completionCommand struct { Name string + FullName string + Description string Subcommands []*completionCommand + Flags []*singleOption + Options []*singleOption ShortFlags []string ShortOptions []string LongFlags []string LongOptions []string + IsFinal bool } -func commandToCompletions(name string, cmd *cmds.Command) *completionCommand { +type singleOption struct { + LongNames []string + ShortNames []string + Description string +} + +func commandToCompletions(name string, fullName string, cmd *cmds.Command) *completionCommand { parsed := &completionCommand{ - Name: name, + Name: name, + FullName: fullName, + Description: cmd.Helptext.Tagline, + IsFinal: len(cmd.Subcommands) == 0, } for name, subCmd := range cmd.Subcommands { - parsed.Subcommands = append(parsed.Subcommands, commandToCompletions(name, subCmd)) + parsed.Subcommands = append(parsed.Subcommands, + commandToCompletions(name, fullName+" "+name, subCmd)) } sort.Slice(parsed.Subcommands, func(i, j int) bool { return parsed.Subcommands[i].Name < parsed.Subcommands[j].Name }) for _, opt := range cmd.Options { + flag := &singleOption{Description: opt.Description()} + flag.LongNames = append(flag.LongNames, opt.Name()) if opt.Type() == cmds.Bool { parsed.LongFlags = append(parsed.LongFlags, opt.Name()) for _, name := range opt.Names() { if len(name) == 1 { parsed.ShortFlags = append(parsed.ShortFlags, name) + flag.ShortNames = append(flag.ShortNames, name) break } } + parsed.Flags = append(parsed.Flags, flag) } else { parsed.LongOptions = append(parsed.LongOptions, opt.Name()) for _, name := range opt.Names() { if len(name) == 1 { parsed.ShortOptions = append(parsed.ShortOptions, name) + flag.ShortNames = append(flag.ShortNames, name) break } } + parsed.Options = append(parsed.Options, flag) } } sort.Slice(parsed.LongFlags, func(i, j int) bool { @@ -62,7 +83,7 @@ func commandToCompletions(name string, cmd *cmds.Command) *completionCommand { return parsed } -var bashCompletionTemplate *template.Template +var bashCompletionTemplate, fishCompletionTemplate *template.Template func init() { commandTemplate := template.Must(template.New("command").Parse(` @@ -133,10 +154,71 @@ _ipfs() { } complete -o nosort -o nospace -o default -F _ipfs ipfs `)) + + fishCommandTemplate := template.Must(template.New("command").Parse(` +{{- if .IsFinal -}} +complete -c ipfs -n '__fish_ipfs_seen_all_subcommands_from{{ .FullName }}' -F +{{ end -}} +{{- range .Flags -}} + complete -c ipfs -n '__fish_ipfs_seen_all_subcommands_from{{ $.FullName }}' {{ range .ShortNames }}-s {{.}} {{end}}{{ range .LongNames }}-l {{.}} {{end}}-d "{{ .Description }}" +{{ end -}} +{{- range .Options -}} + complete -c ipfs -n '__fish_ipfs_seen_all_subcommands_from{{ $.FullName }}' -r {{ range .ShortNames }}-s {{.}} {{end}}{{ range .LongNames }}-l {{.}} {{end}}-d "{{ .Description }}" +{{ end -}} + +{{- range .Subcommands }} +#{{ .FullName }} +complete -c ipfs -n '__fish_ipfs_use_subcommand{{ .FullName }}' -a {{ .Name }} -d "{{ .Description }}" +{{ template "command" . }} +{{ end -}} + `)) + fishCompletionTemplate = template.Must(fishCommandTemplate.New("root").Parse(`#!/usr/bin/env fish +function __fish_ipfs_seen_all_subcommands_from + set -l cmd (commandline -poc) + set -e cmd[1] + for c in $argv + if not contains -- $c $cmd + return 1 + end + end + return 0 +end + +function __fish_ipfs_use_subcommand + set -e argv[-1] + set -l cmd (commandline -poc) + set -e cmd[1] + for i in $cmd + switch $i + case '-*' + continue + case $argv[1] + set argv $argv[2..] + continue + case '*' + return 1 + end + end + test -z "$argv" +end + +complete -c ipfs -l help -d "Show the full command help text." + +complete -c ipfs --keep-order --no-files + +{{ template "command" . }} +`)) + } // writeBashCompletions generates a bash completion script for the given command tree. func writeBashCompletions(cmd *cmds.Command, out io.Writer) error { - cmds := commandToCompletions("ipfs", cmd) + cmds := commandToCompletions("ipfs", "", cmd) return bashCompletionTemplate.Execute(out, cmds) } + +// writeFishCompletions generates a fish completion script for the given command tree. +func writeFishCompletions(cmd *cmds.Command, out io.Writer) error { + cmds := commandToCompletions("ipfs", "", cmd) + return fishCompletionTemplate.Execute(out, cmds) +} diff --git a/docs/command-completion.md b/docs/command-completion.md index 5ca1edd16..b84483e61 100644 --- a/docs/command-completion.md +++ b/docs/command-completion.md @@ -11,3 +11,16 @@ The simplest way to "eval" the completions logic: To install the completions permanently, they can be moved to `/etc/bash_completion.d` or sourced from your `~/.bashrc` file. + +## Fish + +The fish shell is also supported: + +The simplest way to use the completions logic: + +```bash +> ipfs commands completion fish | source +``` + +To install the completions permanently, they can be moved to +`/etc/fish/completions` or `~/.config/fish/completions` or sourced from your `~/.config/fish/config.fish` file. diff --git a/test/sharness/t0012-completion-fish.sh b/test/sharness/t0012-completion-fish.sh new file mode 100755 index 000000000..9c794d993 --- /dev/null +++ b/test/sharness/t0012-completion-fish.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +test_description="Test generated fish completions" + +. lib/test-lib.sh + +test_expect_success "'ipfs commands completion fish' succeeds" ' + ipfs commands completion fish > completions.fish +' + +test_expect_success "generated completions completes 'ipfs version'" ' + fish -c "source completions.fish && complete -C \"ipfs ver\" | grep -q \"version.Show IPFS version information.\" " +' + +test_done +