Skip to content

Commit

Permalink
feat: programmatic shell completions
Browse files Browse the repository at this point in the history
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 #4551
fixes #8033
  • Loading branch information
Stebalien committed Apr 1, 2021
1 parent 4cdb67f commit 18e82d6
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 982 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ dependencies as well.
We strongly recommend you use the [latest version of OSX FUSE](http://osxfuse.github.io/).
(See https://github.com/ipfs/go-ipfs/issues/177)
- For more details on setting up FUSE (so that you can mount the filesystem), see the docs folder.
- 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.
- 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.
- 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.

### Updating go-ipfs
Expand Down
40 changes: 40 additions & 0 deletions core/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package commands

import (
"bytes"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -63,6 +64,9 @@ func CommandsCmd(root *cmds.Command) *cmds.Command {
Tagline: "List all available commands.",
ShortDescription: `Lists all available commands (and subcommands) and exits.`,
},
Subcommands: map[string]*cmds.Command{
"completion": CompletionCmd(root),
},
Options: []cmds.Option{
cmds.BoolOption(flagsOptionName, "f", "Show command flags"),
},
Expand Down Expand Up @@ -131,6 +135,42 @@ func cmdPathStrings(cmd *Command, showOptions bool) []string {
return cmds
}

func CompletionCmd(root *cmds.Command) *cmds.Command {
return &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Generate shell completions.",
},
NoRemote: true,
Subcommands: map[string]*cmds.Command{
"bash": {
Helptext: cmds.HelpText{
Tagline: "Generate bash shell completions.",
ShortDescription: "Generates command completions for the bash shell.",
LongDescription: `
Generates command completions for the bash shell.
The simplest way to see it working is write the completions
to a file and then source it:
> ipfs commands completion bash > ipfs-completion.bash
> source ./ipfs-completion.bash
To install the completions permanently, they can be moved to
/etc/bash_completion.d or sourced from your ~/.bashrc file.
`,
},
NoRemote: true,
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
var buf bytes.Buffer
writeBashCompletions(root, &buf)
res.SetLength(uint64(buf.Len()))
return res.Emit(&buf)
},
},
},
}
}

type nonFatalError string

// streamResult is a helper function to stream results that possibly
Expand Down
138 changes: 138 additions & 0 deletions core/commands/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package commands

import (
"io"
"sort"
"text/template"

cmds "github.com/ipfs/go-ipfs-cmds"
)

type completionCommand struct {
Name string
Subcommands []*completionCommand
ShortFlags []string
ShortOptions []string
LongFlags []string
LongOptions []string
}

func commandToCompletions(name string, cmd *cmds.Command) *completionCommand {
parsed := &completionCommand{
Name: name,
}
for name, subCmd := range cmd.Subcommands {
parsed.Subcommands = append(parsed.Subcommands, commandToCompletions(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 {
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)
break
}
}
} else {
parsed.LongOptions = append(parsed.LongOptions, opt.Name())
for _, name := range opt.Names() {
if len(name) == 1 {
parsed.ShortOptions = append(parsed.ShortOptions, name)
break
}
}
}
}
sort.Slice(parsed.LongFlags, func(i, j int) bool {
return parsed.LongFlags[i] < parsed.LongFlags[j]
})
sort.Slice(parsed.ShortFlags, func(i, j int) bool {
return parsed.ShortFlags[i] < parsed.ShortFlags[j]
})
sort.Slice(parsed.LongOptions, func(i, j int) bool {
return parsed.LongOptions[i] < parsed.LongOptions[j]
})
sort.Slice(parsed.ShortOptions, func(i, j int) bool {
return parsed.ShortOptions[i] < parsed.ShortOptions[j]
})
return parsed
}

var bashCompletionTemplate = template.Must(template.New("root").Parse(`#!/bin/bash
_ipfs_compgen() {
local oldifs="$IFS"
IFS=$'\n'
while read -r line; do
COMPREPLY+=("$line")
done < <(compgen "$@")
IFS="$oldifs"
}
_ipfs() {
COMPREPLY=()
local index=1
local argidx=0
local word="${COMP_WORDS[COMP_CWORD]}"
{{ template "command" . }}
}
complete -o nosort -o nospace -o default -F _ipfs ipfs
`))

var bashCompletionCommandTemplate = template.Must(bashCompletionTemplate.New("command").Parse(`
while [[ ${index} -lt ${COMP_CWORD} ]]; do
case "${COMP_WORDS[index]}" in
-*)
let index++
continue
;;
{{ range .Subcommands }}
"{{ .Name }}")
let index++
{{ template "command" . }}
return 0
;;
{{ end }}
esac
break
done
if [[ "${word}" == -* ]]; then
{{ if .ShortFlags -}}
_ipfs_compgen -W $'{{ range .ShortFlags }}-{{.}} \n{{end}}' -- "${word}"
{{ end -}}
{{- if .ShortOptions -}}
_ipfs_compgen -S = -W $'{{ range .ShortOptions }}-{{.}}\n{{end}}' -- "${word}"
{{ end -}}
{{- if .LongFlags -}}
_ipfs_compgen -W $'{{ range .LongFlags }}--{{.}} \n{{end}}' -- "${word}"
{{ end -}}
{{- if .LongOptions -}}
_ipfs_compgen -S = -W $'{{ range .LongOptions }}--{{.}}\n{{end}}' -- "${word}"
{{ end -}}
return 0
fi
while [[ ${index} -lt ${COMP_CWORD} ]]; do
if [[ "${COMP_WORDS[index]}" != -* ]]; then
let argidx++
fi
let index++
done
{{- if .Subcommands }}
if [[ "${argidx}" -eq 0 ]]; then
_ipfs_compgen -W $'{{ range .Subcommands }}{{.Name}} \n{{end}}' -- "${word}"
fi
{{ end -}}
`))

// WritebashCompletions generates a bash completion script for the given command tree.
func writeBashCompletions(cmd *cmds.Command, out io.Writer) error {
cmds := commandToCompletions("ipfs", cmd)
return bashCompletionTemplate.ExecuteTemplate(out, "root", cmds)
}
32 changes: 9 additions & 23 deletions docs/command-completion.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,15 @@
Command Completion
==================
# Command Completion

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

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

Installation
------------
The simplest way to see it working is to run
`source misc/completion/ipfs-completion.bash` straight from your shell. This
is only temporary and to fully enable it, you'll have to follow one of the steps
below.

### Bash on Linux
For bash, completion can be enabled in a couple of ways. One is to copy the
completion script to the directory `~/.ipfs/` and then in the file
`~/.bash_completion` add
```bash
source ~/.ipfs/ipfs-completion.bash
> ipfs commands completion bash > ipfs-completion.bash
> source ./ipfs-completion.bash
```
It will automatically be loaded the next time bash is loaded.
To enable ipfs command completion globally on your system you may also
copy the completion script to `/etc/bash_completion.d/`.


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

0 comments on commit 18e82d6

Please sign in to comment.