Skip to content

Commit

Permalink
Merge pull request #8043 from ipfs/feat/completions
Browse files Browse the repository at this point in the history
programmatic shell completions
  • Loading branch information
Stebalien committed Jul 2, 2021
2 parents fcfe793 + 7940371 commit ae30699
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 982 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,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)
- Read [docs/fuse.md](docs/fuse.md) for more details on setting up FUSE (so that you can mount the filesystem).
- 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
42 changes: 42 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,44 @@ 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
if err := writeBashCompletions(root, &buf); err != nil {
return err
}
res.SetLength(uint64(buf.Len()))
return res.Emit(&buf)
},
},
},
}
}

type nonFatalError string

// streamResult is a helper function to stream results that possibly
Expand Down
4 changes: 4 additions & 0 deletions core/commands/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ func TestROCommands(t *testing.T) {
"/block/stat",
"/cat",
"/commands",
"/commands/completion",
"/commands/completion/bash",
"/dag",
"/dag/get",
"/dag/resolve",
Expand Down Expand Up @@ -89,6 +91,8 @@ func TestCommands(t *testing.T) {
"/bootstrap/rm/all",
"/cat",
"/commands",
"/commands/completion",
"/commands/completion/bash",
"/config",
"/config/edit",
"/config/replace",
Expand Down
142 changes: 142 additions & 0 deletions core/commands/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
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.Template

func init() {
commandTemplate := template.Must(template.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 -}}
`))

bashCompletionTemplate = template.Must(commandTemplate.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
`))
}

// 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.Execute(out, cmds)
}
30 changes: 7 additions & 23 deletions docs/command-completion.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,13 @@
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 "eval" the completions logic:

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
> eval "$(ipfs commands 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 ae30699

Please sign in to comment.