diff --git a/README.md b/README.md index deecd43e7..e178b4566 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/core/commands/commands.go b/core/commands/commands.go index b45ac54c6..27be029c0 100644 --- a/core/commands/commands.go +++ b/core/commands/commands.go @@ -6,6 +6,7 @@ package commands import ( + "bytes" "fmt" "io" "os" @@ -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"), }, @@ -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 diff --git a/core/commands/commands_test.go b/core/commands/commands_test.go index 81f07c01b..288f8a1ad 100644 --- a/core/commands/commands_test.go +++ b/core/commands/commands_test.go @@ -22,6 +22,8 @@ func TestROCommands(t *testing.T) { "/block/stat", "/cat", "/commands", + "/commands/completion", + "/commands/completion/bash", "/dag", "/dag/get", "/dag/resolve", @@ -89,6 +91,8 @@ func TestCommands(t *testing.T) { "/bootstrap/rm/all", "/cat", "/commands", + "/commands/completion", + "/commands/completion/bash", "/config", "/config/edit", "/config/replace", diff --git a/core/commands/completion.go b/core/commands/completion.go new file mode 100644 index 000000000..ed1865d26 --- /dev/null +++ b/core/commands/completion.go @@ -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) +} diff --git a/docs/command-completion.md b/docs/command-completion.md index 73394d41a..5ca1edd16 100644 --- a/docs/command-completion.md +++ b/docs/command-completion.md @@ -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. diff --git a/misc/completion/ipfs-completion.bash b/misc/completion/ipfs-completion.bash deleted file mode 100644 index f0f400364..000000000 --- a/misc/completion/ipfs-completion.bash +++ /dev/null @@ -1,958 +0,0 @@ -_do_comp() -{ - if [[ $(type compopt) == *"builtin" ]]; then - compopt $@ - else - complete $@ - fi -} - -_ipfs_comp() -{ - COMPREPLY=( $(compgen -W "$1" -- ${word}) ) - if [[ ${#COMPREPLY[@]} == 1 && ${COMPREPLY[0]} == "--"*"=" ]] ; then - # If there's only one option, with =, then discard space - _do_comp -o nospace - fi -} - -_ipfs_help_only() -{ - _ipfs_comp "--help" -} - -_ipfs_add() -{ - if [[ "${prev}" == "--chunker" ]] ; then - _ipfs_comp "placeholder1 placeholder2 placeholder3" # TODO: a) Give real options, b) Solve autocomplete bug for "=" - elif [ "${prev}" == "--pin" ] ; then - _ipfs_comp "true false" - elif [[ ${word} == -* ]] ; then - _ipfs_comp "--recursive --dereference-args --stdin-name= --hidden --ignore= --ignore-rules-path= --quiet --quieter --silent --progress --trickle --only-hash --wrap-with-directory --chunker= --pin= --raw-leaves --nocopy --fscache --cid-version= --hash= --inline --inline-limit= --help " - else - _ipfs_filesystem_complete - fi -} - -_ipfs_bitswap() -{ - ipfs_comp "ledger stat wantlist --help" -} - -_ipfs_bitswap_ledger() -{ - _ipfs_help_only -} - -_ipfs_bitswap_stat() -{ - _ipfs_help_only -} - -_ipfs_bitswap_wantlist() -{ - ipfs_comp "--peer= --help" -} - -_ipfs_block() -{ - _ipfs_comp "get put rm stat --help" -} - -_ipfs_block_get() -{ - _ipfs_hash_complete -} - -_ipfs_block_put() -{ - if [ "${prev}" == "--format" ] ; then - _ipfs_comp "v0 placeholder2 placeholder3" # TODO: a) Give real options, b) Solve autocomplete bug for "=" - elif [[ ${word} == -* ]] ; then - _ipfs_comp "--format= --help" - else - _ipfs_filesystem_complete - fi -} - -_ipfs_block_rm() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--force --quiet --help" - else - _ipfs_hash_complete - fi -} - -_ipfs_block_stat() -{ - _ipfs_hash_complete -} - -_ipfs_bootstrap() -{ - _ipfs_comp "add list rm --help" -} - -_ipfs_bootstrap_add() -{ - _ipfs_comp "default --help" -} - -_ipfs_bootstrap_list() -{ - _ipfs_help_only -} - -_ipfs_bootstrap_rm() -{ - _ipfs_comp "all --help" -} - -_ipfs_cat() -{ - if [[ ${prev} == */* ]] ; then - COMPREPLY=() # Only one argument allowed - elif [[ ${word} == */* ]] ; then - _ipfs_hash_complete - else - _ipfs_pinned_complete - fi -} - -_ipfs_commands() -{ - _ipfs_comp "--flags --help" -} - -_ipfs_config() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--bool --json" - elif [[ ${prev} == *.* ]] ; then - COMPREPLY=() # Only one subheader of the config can be shown or edited. - else - _ipfs_comp "show edit replace" - fi -} - -_ipfs_config_edit() -{ - _ipfs_help_only -} - -_ipfs_config_replace() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--help" - else - _ipfs_filesystem_complete - fi -} - -_ipfs_config_show() -{ - _ipfs_help_only -} - -_ipfs_daemon() -{ - if [[ ${prev} == "--routing" ]] ; then - _ipfs_comp "dht dhtclient none" # TODO: Solve autocomplete bug for "=" - elif [[ ${prev} == "--mount-ipfs" ]] || [[ ${prev} == "--mount-ipns" ]] || [[ ${prev} == "=" ]]; then - _ipfs_filesystem_complete - elif [[ ${word} == -* ]] ; then - _ipfs_comp "--init --routing= --mount --writable --mount-ipfs= \ - --mount-ipns= --unrestricted-api --disable-transport-encryption \ - -- enable-gc --manage-fdlimit --offline --migrate --help" - fi -} - -_ipfs_dag() -{ - _ipfs_comp "get put --help" -} - -_ipfs_dag_get() -{ - _ipfs_help_only -} - -_ipfs_dag_put() -{ - if [[ ${prev} == "--format" ]] ; then - _ipfs_comp "cbor placeholder1" # TODO: a) Which format more than cbor is valid? b) Solve autocomplete bug for "=" - elif [[ ${prev} == "--input-enc" ]] ; then - _ipfs_comp "json placeholder1" # TODO: a) Which format more than json is valid? b) Solve autocomplete bug for "=" - elif [[ ${word} == -* ]] ; then - _ipfs_comp "--format= --input-enc= --help" - else - _ipfs_filesystem_complete - fi -} - -_ipfs_dht() -{ - _ipfs_comp "findpeer findprovs get provide put query --help" -} - -_ipfs_dht_findpeer() -{ - _ipfs_comp "--verbose --help" -} - -_ipfs_dht_findprovs() -{ - _ipfs_comp "--verbose --help" -} - -_ipfs_dht_get() -{ - _ipfs_comp "--verbose --help" -} - -_ipfs_dht_provide() -{ - _ipfs_comp "--recursive --verbose --help" -} - -_ipfs_dht_put() -{ - _ipfs_comp "--verbose --help" -} - -_ipfs_dht_query() -{ - _ipfs_comp "--verbose --help" -} - -_ipfs_diag() -{ - _ipfs_comp "sys cmds net --help" -} - -_ipfs_diag_cmds() -{ - if [[ ${prev} == "clear" ]] ; then - return 0 - elif [[ ${prev} =~ ^-?[0-9]+$ ]] ; then - _ipfs_comp "ns us µs ms s m h" # TODO: Trigger without space, eg. "ipfs diag set-time 10ns" not "... set-time 10 ns" - elif [[ ${prev} == "set-time" ]] ; then - _ipfs_help_only - elif [[ ${word} == -* ]] ; then - _ipfs_comp "--verbose --help" - else - _ipfs_comp "clear set-time" - fi -} - -_ipfs_diag_sys() -{ - _ipfs_help_only -} - -_ipfs_diag_net() -{ - if [[ ${prev} == "--vis" ]] ; then - _ipfs_comp "d3 dot text" # TODO: Solve autocomplete bug for "=" - elif [[ ${word} == -* ]] ; then - _ipfs_comp "--timeout= --vis= --help" - fi -} - -_ipfs_dns() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--recursive --help" - fi -} - -_ipfs_files() -{ - _ipfs_comp "mv rm flush read write cp ls mkdir stat" -} - -_ipfs_files_mv() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--recursive --flush" - elif [[ ${word} == /* ]] ; then - _ipfs_files_complete - else - COMPREPLY=( / ) - [[ $COMPREPLY = */ ]] && _do_comp -o nospace - fi -} - -_ipfs_files_rm() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--recursive --flush" - elif [[ ${word} == /* ]] ; then - _ipfs_files_complete - else - COMPREPLY=( / ) - [[ $COMPREPLY = */ ]] && _do_comp -o nospace - fi -} -_ipfs_files_flush() -{ - if [[ ${word} == /* ]] ; then - _ipfs_files_complete - else - COMPREPLY=( / ) - [[ $COMPREPLY = */ ]] && _do_comp -o nospace - fi -} - -_ipfs_files_read() -{ - if [[ ${prev} == "--count" ]] || [[ ${prev} == "--offset" ]] ; then - COMPREPLY=() # Numbers, just keep it empty - elif [[ ${word} == -* ]] ; then - _ipfs_comp "--offset --count --help" - elif [[ ${word} == /* ]] ; then - _ipfs_files_complete - else - COMPREPLY=( / ) - [[ $COMPREPLY = */ ]] && _do_comp -o nospace - fi -} - -_ipfs_files_write() -{ - if [[ ${prev} == "--count" ]] || [[ ${prev} == "--offset" ]] ; then # Dirty check - COMPREPLY=() # Numbers, just keep it empty - elif [[ ${word} == -* ]] ; then - _ipfs_comp "--offset --count --create --truncate --help" - elif [[ ${prev} == /* ]] ; then - _ipfs_filesystem_complete - elif [[ ${word} == /* ]] ; then - _ipfs_files_complete - else - COMPREPLY=( / ) - [[ $COMPREPLY = */ ]] && _do_comp -o nospace - fi -} - -_ipfs_files_cp() -{ - if [[ ${word} == /* ]] ; then - _ipfs_files_complete - else - COMPREPLY=( / ) - [[ $COMPREPLY = */ ]] && _do_comp -o nospace - fi -} - -_ipfs_files_ls() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "-l --help" - elif [[ ${prev} == /* ]] ; then - COMPREPLY=() # Path exist - elif [[ ${word} == /* ]] ; then - _ipfs_files_complete - else - COMPREPLY=( / ) - [[ $COMPREPLY = */ ]] && _do_comp -o nospace - fi -} - -_ipfs_files_mkdir() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--parents --help" - - elif [[ ${prev} == /* ]] ; then - COMPREPLY=() # Path exist - elif [[ ${word} == /* ]] ; then - _ipfs_files_complete - else - COMPREPLY=( / ) - [[ $COMPREPLY = */ ]] && _do_comp -o nospace - fi -} - -_ipfs_files_stat() -{ - if [[ ${prev} == /* ]] ; then - COMPREPLY=() # Path exist - elif [[ ${word} == /* ]] ; then - _ipfs_files_complete - else - COMPREPLY=( / ) - [[ $COMPREPLY = */ ]] && _do_comp -o nospace - fi -} - -_ipfs_file() -{ - if [[ ${prev} == "ls" ]] ; then - _ipfs_hash_complete - else - _ipfs_comp "ls --help" - fi -} - -_ipfs_file_ls() -{ - _ipfs_help_only -} - -_ipfs_get() -{ - if [ "${prev}" == "--output" ] ; then - _do_comp -o default # Re-enable default file read - COMPREPLY=() - elif [ "${prev}" == "--compression-level" ] ; then - _ipfs_comp "-1 1 2 3 4 5 6 7 8 9" # TODO: Solve autocomplete bug for "=" - elif [[ ${word} == -* ]] ; then - _ipfs_comp "--output= --archive --compress --compression-level= --help" - else - _ipfs_hash_complete - fi -} - -_ipfs_id() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--format= --help" - fi -} - -_ipfs_init() -{ - _ipfs_comp "--bits --force --empty-repo --help" -} - -_ipfs_log() -{ - _ipfs_comp "level ls tail --help" -} - -_ipfs_log_level() -{ - # TODO: auto-complete subsystem and level - _ipfs_help_only -} - -_ipfs_log_ls() -{ - _ipfs_help_only -} - -_ipfs_log_tail() -{ - _ipfs_help_only -} - -_ipfs_ls() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--headers --resolve-type=false --help" - else - _ipfs_hash_complete - fi -} - -_ipfs_mount() -{ - if [[ ${prev} == "--ipfs-path" ]] || [[ ${prev} == "--ipns-path" ]] || [[ ${prev} == "=" ]] ; then - _ipfs_filesystem_complete - elif [[ ${word} == -* ]] ; then - _ipfs_comp "--ipfs-path= --ipns-path= --help" - fi -} - -_ipfs_name() -{ - _ipfs_comp "publish resolve --help" -} - -_ipfs_name_publish() -{ - if [[ ${prev} == "--lifetime" ]] || [[ ${prev} == "--ttl" ]] ; then - COMPREPLY=() # Accept only numbers - elif [[ ${prev} =~ ^-?[0-9]+$ ]] ; then - _ipfs_comp "ns us µs ms s m h" # TODO: Trigger without space, eg. "ipfs diag set-time 10ns" not "... set-time 10 ns" - elif [[ ${word} == -* ]] ; then - _ipfs_comp "--resolve --lifetime --ttl --help" - elif [[ ${word} == */ ]]; then - _ipfs_hash_complete - else - _ipfs_pinned_complete - fi -} - -_ipfs_name_resolve() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--recursive --nocache --help" - fi -} - -_ipfs_object() -{ - _ipfs_comp "data diff get links new patch put stat --help" -} - -_ipfs_object_data() -{ - _ipfs_hash_complete -} - -_ipfs_object_diff() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--verbose --help" - else - _ipfs_hash_complete - fi -} - - -_ipfs_object_get() -{ - if [ "${prev}" == "--encoding" ] ; then - _ipfs_comp "protobuf json xml" - elif [[ ${word} == -* ]] ; then - _ipfs_comp "--encoding --help" - else - _ipfs_hash_complete - fi -} - -_ipfs_object_links() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--headers --help" - else - _ipfs_hash_complete - fi -} - -_ipfs_object_new() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--help" - else - _ipfs_comp "unixfs-dir" - fi -} - -_ipfs_object_patch() -{ - if [[ -n "${COMP_WORDS[3]}" ]] ; then # Root merkledag object exist - case "${COMP_WORDS[4]}" in - append-data) - _ipfs_help_only - ;; - add-link) - if [[ ${word} == -* ]] && [[ ${prev} == "add-link" ]] ; then # Dirty check - _ipfs_comp "--create" - #else - # TODO: Hash path autocomplete. This is tricky, can be hash or a name. - fi - ;; - rm-link) - _ipfs_hash_complete - ;; - set-data) - _ipfs_filesystem_complete - ;; - *) - _ipfs_comp "append-data add-link rm-link set-data" - ;; - esac - else - _ipfs_hash_complete - fi -} - -_ipfs_object_put() -{ - if [ "${prev}" == "--inputenc" ] ; then - _ipfs_comp "protobuf json" - elif [ "${prev}" == "--datafieldenc" ] ; then - _ipfs_comp "text base64" - elif [[ ${word} == -* ]] ; then - _ipfs_comp "--inputenc --datafieldenc --help" - else - _ipfs_hash_complete - fi -} - -_ipfs_object_stat() -{ - _ipfs_hash_complete -} - -_ipfs_pin() -{ - _ipfs_comp "rm ls add --help" -} - -_ipfs_pin_add() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--recursive= --help" - elif [[ ${word} == */ ]] && [[ ${word} != "/ipfs/" ]] ; then - _ipfs_hash_complete - fi -} - -_ipfs_pin_ls() -{ - if [[ ${prev} == "--type" ]] || [[ ${prev} == "-t" ]] ; then - _ipfs_comp "direct indirect recursive all" # TODO: Solve autocomplete bug for - elif [[ ${word} == -* ]] ; then - _ipfs_comp "--count --quiet --type= --help" - elif [[ ${word} == */ ]] && [[ ${word} != "/ipfs/" ]] ; then - _ipfs_hash_complete - fi -} - -_ipfs_pin_rm() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--recursive --help" - elif [[ ${word} == */ ]] && [[ ${word} != "/ipfs/" ]] ; then - COMPREPLY=() # TODO: _ipfs_hash_complete() + List local pinned hashes as default? - fi -} - -_ipfs_ping() -{ - _ipfs_comp "--count= --help" -} - -_ipfs_pubsub() -{ - _ipfs_comp "ls peers pub sub --help" -} - -_ipfs_pubsub_ls() -{ - _ipfs_help_only -} - -_ipfs_pubsub_peers() -{ - _ipfs_help_only -} - -_ipfs_pubsub_pub() -{ - _ipfs_help_only -} - -_ipfs_pubsub_sub() -{ - _ipfs_comp "--discover --help" -} - -_ipfs_refs() -{ - if [ "${prev}" == "--format" ] ; then - _ipfs_comp "src dst linkname" - elif [[ ${word} == -* ]] ; then - _ipfs_comp "local --format= --edges --unique --recursive --help" - #else - # TODO: Use "ipfs ref" and combine it with autocomplete, see _ipfs_hash_complete - fi -} - -_ipfs_refs_local() -{ - _ipfs_help_only -} - -_ipfs_repo() -{ - _ipfs_comp "fsck gc stat verify version --help" -} - -_ipfs_repo_version() -{ - _ipfs_comp "--quiet --help" -} - -_ipfs_repo_verify() -{ - _ipfs_help_only -} - -_ipfs_repo_gc() -{ - _ipfs_comp "--quiet --help" -} - -_ipfs_repo_stat() -{ - _ipfs_comp "--human --help" -} - -_ipfs_repo_fsck() -{ - _ipfs_help_only -} - -_ipfs_resolve() -{ - if [[ ${word} == /ipfs/* ]] ; then - _ipfs_hash_complete - elif [[ ${word} == /ipns/* ]] ; then - COMPREPLY=() # Can't autocomplete ipns - elif [[ ${word} == -* ]] ; then - _ipfs_comp "--recursive --help" - else - opts="/ipns/ /ipfs/" - COMPREPLY=( $(compgen -W "${opts}" -- ${word}) ) - [[ $COMPREPLY = */ ]] && _do_comp -o nospace - fi -} - -_ipfs_stats() -{ - _ipfs_comp "bitswap bw repo --help" -} - -_ipfs_stats_bitswap() -{ - _ipfs_help_only -} - -_ipfs_stats_bw() -{ - # TODO: Which protocol is valid? - _ipfs_comp "--peer= --proto= --poll --interval= --help" -} - -_ipfs_stats_repo() -{ - _ipfs_comp "--human= --help" -} - -_ipfs_swarm() -{ - _ipfs_comp "addrs connect disconnect filters peers --help" -} - -_ipfs_swarm_addrs() -{ - _ipfs_comp "local --help" -} - -_ipfs_swarm_addrs_local() -{ - _ipfs_comp "--id --help" -} - -_ipfs_swarm_connect() -{ - _ipfs_multiaddr_complete -} - -_ipfs_swarm_disconnect() -{ - local OLDIFS="$IFS" ; local IFS=$'\n' # Change divider for iterator one line below - opts=$(for x in `ipfs swarm peers`; do echo ${x} ; done) - IFS="$OLDIFS" # Reset divider to space, ' ' - COMPREPLY=( $(compgen -W "${opts}" -- ${word}) ) - [[ $COMPREPLY = */ ]] && _do_comp -o nospace -o filenames -} - -_ipfs_swarm_filters() -{ - if [[ ${prev} == "add" ]] || [[ ${prev} == "rm" ]]; then - _ipfs_multiaddr_complete - else - _ipfs_comp "add rm --help" - fi -} - -_ipfs_swarm_filters_add() -{ - _ipfs_help_only -} - -_ipfs_swarm_filters_rm() -{ - _ipfs_help_only -} - -_ipfs_swarm_peers() -{ - _ipfs_help_only -} - -_ipfs_tar() -{ - _ipfs_comp "add cat --help" -} - -_ipfs_tar_add() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--help" - else - _ipfs_filesystem_complete - fi -} - -_ipfs_tar_cat() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--help" - else - _ipfs_filesystem_complete - fi -} - -_ipfs_update() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--version" # TODO: How does "--verbose" option work? - else - _ipfs_comp "versions version install stash revert fetch" - fi -} - -_ipfs_update_install() -{ - if [[ ${prev} == v*.*.* ]] ; then - COMPREPLY=() - elif [[ ${word} == -* ]] ; then - _ipfs_comp "--version" - else - local OLDIFS="$IFS" ; local IFS=$'\n' # Change divider for iterator one line below - opts=$(for x in `ipfs update versions`; do echo ${x} ; done) - IFS="$OLDIFS" # Reset divider to space, ' ' - COMPREPLY=( $(compgen -W "${opts}" -- ${word}) ) - fi -} - -_ipfs_update_stash() -{ - if [[ ${word} == -* ]] ; then - _ipfs_comp "--tag --help" - fi -} -_ipfs_update_fetch() -{ - if [[ ${prev} == "--output" ]] ; then - _ipfs_filesystem_complete - elif [[ ${word} == -* ]] ; then - _ipfs_comp "--output --help" - fi -} - -_ipfs_version() -{ - _ipfs_comp "--number --commit --repo" -} - -_ipfs_hash_complete() -{ - local lastDir=${word%/*}/ - echo "LastDir: ${lastDir}" >> ~/Downloads/debug-ipfs.txt - local OLDIFS="$IFS" ; local IFS=$'\n' # Change divider for iterator one line below - opts=$(for x in `ipfs file ls ${lastDir}`; do echo ${lastDir}${x}/ ; done) # TODO: Implement "ipfs file ls -F" to get rid of frontslash after files. This take long time to run first time on a new shell. - echo "Options: ${opts}" >> ~/Downloads/debug-ipfs.txt - IFS="$OLDIFS" # Reset divider to space, ' ' - echo "Current: ${word}" >> ~/Downloads/debug-ipfs.txt - COMPREPLY=( $(compgen -W "${opts}" -- ${word}) ) - echo "Suggestion: ${COMPREPLY}" >> ~/Downloads/debug-ipfs.txt - [[ $COMPREPLY = */ ]] && _do_comp -o nospace -o filenames # Removing whitespace after output & handle output as filenames. (Only printing the latest folder of files.) - return 0 -} - -_ipfs_files_complete() -{ - local lastDir=${word%/*}/ - local OLDIFS="$IFS" ; local IFS=$'\n' # Change divider for iterator one line below - opts=$(for x in `ipfs files ls ${lastDir}`; do echo ${lastDir}${x}/ ; done) # TODO: Implement "ipfs files ls -F" to get rid of frontslash after files. This does currently throw "Error: /cats/foo/ is not a directory" - IFS="$OLDIFS" # Reset divider to space, ' ' - COMPREPLY=( $(compgen -W "${opts}" -- ${word}) ) - [[ $COMPREPLY = */ ]] && _do_comp -o nospace -o filenames - return 0 -} - -_ipfs_multiaddr_complete() -{ - local lastDir=${word%/*}/ - # Special case - if [[ ${word} == */"ipcidr"* ]] ; then # TODO: Broken, fix it. - opts="1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32" # TODO: IPv6? - COMPREPLY=( $(compgen -W "${opts}" -- ${word}) ) - # "Loop" - elif [[ ${word} == /*/ ]] || [[ ${word} == /*/* ]] ; then - if [[ ${word} == /*/*/*/*/*/ ]] ; then - COMPREPLY=() - elif [[ ${word} == /*/*/*/*/ ]] ; then - word=${word##*/} - opts="ipfs/ " - COMPREPLY=( $(compgen -W "${opts}" -- ${word}) ) - elif [[ ${word} == /*/*/*/ ]] ; then - word=${word##*/} - opts="4001/ " - COMPREPLY=( $(compgen -W "${opts}" -- ${word}) ) - elif [[ ${word} == /*/*/ ]] ; then - word=${word##*/} - opts="udp/ tcp/ ipcidr/" - COMPREPLY=( $(compgen -W "${opts}" -- ${word}) ) - elif [[ ${word} == /*/ ]] ; then - COMPREPLY=() # TODO: This need to return something to NOT break the function. Maybe a "/" in the end as well due to -o filename option. - fi - COMPREPLY=${lastDir}${COMPREPLY} - else # start case - opts="/ip4/ /ip6/" - COMPREPLY=( $(compgen -W "${opts}" -- ${word}) ) - fi - [[ $COMPREPLY = */ ]] && _do_comp -o nospace -o filenames - return 0 -} - -_ipfs_pinned_complete() -{ - local OLDIFS="$IFS" ; local IFS=$'\n' - local pinned=$(ipfs pin ls) - COMPREPLY=( $(compgen -W "${pinned}" -- ${word}) ) - IFS="$OLDIFS" - if [[ ${#COMPREPLY[*]} -eq 1 ]]; then # Only one completion, remove pretty output - COMPREPLY=( ${COMPREPLY[0]/ *//} ) #Remove ' ' and everything after - [[ $COMPREPLY = */ ]] && _do_comp -o nospace # Removing whitespace after output - fi -} -_ipfs_filesystem_complete() -{ - _do_comp -o default # Re-enable default file read - COMPREPLY=() -} - -_ipfs() -{ - COMPREPLY=() - _do_comp +o default # Disable default to not deny completion, see: http://stackoverflow.com/a/19062943/1216348 - - local word="${COMP_WORDS[COMP_CWORD]}" - local prev="${COMP_WORDS[COMP_CWORD-1]}" - - case "${COMP_CWORD}" in - 1) - local opts="add bitswap block bootstrap cat commands config daemon dag dht \ - diag dns file files get id init log ls mount name object pin ping pubsub \ - refs repo resolve stats swarm tar update version" - COMPREPLY=( $(compgen -W "${opts}" -- ${word}) );; - 2) - local command="${COMP_WORDS[1]}" - eval "_ipfs_$command" 2> /dev/null ;; - *) - local command="${COMP_WORDS[1]}" - local subcommand="${COMP_WORDS[2]}" - eval "_ipfs_${command}_${subcommand}" 2> /dev/null && return - eval "_ipfs_$command" 2> /dev/null ;; - esac -} -complete -F _ipfs ipfs diff --git a/test/sharness/t0011-completion.sh b/test/sharness/t0011-completion.sh new file mode 100755 index 000000000..cc1c3f0dc --- /dev/null +++ b/test/sharness/t0011-completion.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +test_description="Test generated bash completions" + +. lib/test-lib.sh + +test_expect_success "'ipfs commands completion bash' succeeds" ' + ipfs commands completion bash > completions.bash +' + +test_expect_success "generated completions defines '_ipfs'" ' + bash -c "source completions.bash && type -t _ipfs" +' + +test_done