diff --git a/autocomplete/bash_autocomplete b/autocomplete/bash_autocomplete index 7120a0d222..d63937d971 100755 --- a/autocomplete/bash_autocomplete +++ b/autocomplete/bash_autocomplete @@ -1,15 +1,15 @@ -#! /bin/bash +#!/bin/bash -: ${PROG:=$(basename ${BASH_SOURCE})} +# This is a shell completion script auto-generated by https://github.com/urfave/cli for bash. # Macs have bash3 for which the bash-completion package doesn't include # _init_completion. This is a minimal version of that function. -_cli_init_completion() { +__%[1]s_init_completion() { COMPREPLY=() _get_comp_words_by_ref "$@" cur prev words cword } -_cli_bash_autocomplete() { +__%[1]s_bash_autocomplete() { if [[ "${COMP_WORDS[0]}" != "source" ]]; then local cur opts base words COMPREPLY=() @@ -17,7 +17,7 @@ _cli_bash_autocomplete() { if declare -F _init_completion >/dev/null 2>&1; then _init_completion -n "=:" || return else - _cli_init_completion -n "=:" || return + __%[1]s_init_completion -n "=:" || return fi words=("${words[@]:0:$cword}") if [[ "$cur" == "-"* ]]; then @@ -31,5 +31,4 @@ _cli_bash_autocomplete() { fi } -complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG -unset PROG +complete -o bashdefault -o default -o nospace -F __%[1]s_bash_autocomplete %[1]s diff --git a/autocomplete/powershell_autocomplete.ps1 b/autocomplete/powershell_autocomplete.ps1 index cbf76942dd..6e0c422e25 100644 --- a/autocomplete/powershell_autocomplete.ps1 +++ b/autocomplete/powershell_autocomplete.ps1 @@ -6,4 +6,4 @@ Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock { Invoke-Expression $other | ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) } - } \ No newline at end of file + } diff --git a/autocomplete/zsh_autocomplete b/autocomplete/zsh_autocomplete index 943cf60b95..d24049a72f 100644 --- a/autocomplete/zsh_autocomplete +++ b/autocomplete/zsh_autocomplete @@ -1,19 +1,17 @@ -#compdef program -compdef _program program +#compdef %[1]s +compdef _%[1]s %[1]s -# Replace all occurrences of "program" in this file with the actual name of your -# CLI program. We recommend using Find+Replace feature of your editor. Let's say -# your CLI program is called "acme", then replace like so: -# * program => acme -# * _program => _acme +# This is a shell completion script auto-generated by https://github.com/urfave/cli for zsh. -_program() { - local -a opts - local cur - cur=${words[-1]} - if [[ "$cur" == "-"* ]]; then - opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-shell-completion)}") +_%[1]s() { + local -a opts # Declare a local array + local current + current=${words[-1]} # -1 means "the last element" + if [[ "$current" == "-"* ]]; then + # Current word starts with a hyphen, so complete flags/options + opts=("${(@f)$(${words[@]:0:#words[@]-1} ${current} --generate-shell-completion)}") else + # Current word does not start with a hyphen, so complete subcommands opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-shell-completion)}") fi @@ -24,7 +22,8 @@ _program() { fi } -# don't run the completion function when being source-ed or eval-ed -if [ "$funcstack[1]" = "_program" ]; then - _program +# Don't run the completion function when being source-ed or eval-ed. +# See https://github.com/urfave/cli/issues/1874 for discussion. +if [ "$funcstack[1]" = "_%[1]s" ]; then + _%[1]s fi diff --git a/command.go b/command.go index 3970f1c355..3b7c97ca97 100644 --- a/command.go +++ b/command.go @@ -257,7 +257,7 @@ func (cmd *Command) setupDefaults(osArgs []string) { } if cmd.EnableShellCompletion || cmd.Root().shellCompletion { - completionCommand := buildCompletionCommand() + completionCommand := buildCompletionCommand(osArgs[0]) if cmd.ShellCompletionCommandName != "" { tracef( diff --git a/completion.go b/completion.go index d344fc4d5c..d778380474 100644 --- a/completion.go +++ b/completion.go @@ -8,43 +8,48 @@ import ( ) const ( - completionCommandName = "generate-completion" - completionFlagName = "generate-shell-completion" - completionFlag = "--" + completionFlagName + completionCommandName = "completion" + + // This flag is supposed to only be used by the completion script itself to generate completions on the fly. + completionFlag = "--generate-shell-completion" ) +type renderCompletion func(cmd *Command, appName string) (string, error) + var ( //go:embed autocomplete autoCompleteFS embed.FS shellCompletions = map[string]renderCompletion{ - "bash": getCompletion("autocomplete/bash_autocomplete"), - "ps": getCompletion("autocomplete/powershell_autocomplete.ps1"), - "zsh": getCompletion("autocomplete/zsh_autocomplete"), - "fish": func(c *Command) (string, error) { + "bash": func(c *Command, appName string) (string, error) { + b, err := autoCompleteFS.ReadFile("autocomplete/bash_autocomplete") + return fmt.Sprintf(string(b), appName), err + }, + "zsh": func(c *Command, appName string) (string, error) { + b, err := autoCompleteFS.ReadFile("autocomplete/zsh_autocomplete") + return fmt.Sprintf(string(b), appName), err + }, + "fish": func(c *Command, appName string) (string, error) { return c.ToFishCompletion() }, + "pwsh": func(c *Command, appName string) (string, error) { + b, err := autoCompleteFS.ReadFile("autocomplete/powershell_autocomplete.ps1") + return string(b), err + }, } ) -type renderCompletion func(*Command) (string, error) - -func getCompletion(s string) renderCompletion { - return func(c *Command) (string, error) { - b, err := autoCompleteFS.ReadFile(s) - return string(b), err - } -} - -func buildCompletionCommand() *Command { +func buildCompletionCommand(appName string) *Command { return &Command{ Name: completionCommandName, Hidden: true, - Action: completionCommandAction, + Action: func(ctx context.Context, cmd *Command) error { + return printShellCompletion(ctx, cmd, appName) + }, } } -func completionCommandAction(ctx context.Context, cmd *Command) error { +func printShellCompletion(_ context.Context, cmd *Command, appName string) error { var shells []string for k := range shellCompletions { shells = append(shells, k) @@ -57,14 +62,20 @@ func completionCommandAction(ctx context.Context, cmd *Command) error { } s := cmd.Args().First() - if rc, ok := shellCompletions[s]; !ok { + renderCompletion, ok := shellCompletions[s] + if !ok { return Exit(fmt.Sprintf("unknown shell %s, available shells are %+v", s, shells), 1) - } else if c, err := rc(cmd); err != nil { + } + + completionScript, err := renderCompletion(cmd, appName) + if err != nil { return Exit(err, 1) - } else { - if _, err = cmd.Writer.Write([]byte(c)); err != nil { - return Exit(err, 1) - } } + + _, err = cmd.Writer.Write([]byte(completionScript)) + if err != nil { + return Exit(err, 1) + } + return nil } diff --git a/completion_test.go b/completion_test.go index e3c951b1b7..3fd1dfce38 100644 --- a/completion_test.go +++ b/completion_test.go @@ -200,7 +200,7 @@ func TestCompletionInvalidShell(t *testing.T) { assert.ErrorContains(t, err, "unknown shell junky-sheell") enableError := true - shellCompletions[unknownShellName] = func(c *Command) (string, error) { + shellCompletions[unknownShellName] = func(c *Command, appName string) (string, error) { if enableError { return "", fmt.Errorf("cant do completion") } diff --git a/help.go b/help.go index 10c2f6e8f0..eb07455c94 100644 --- a/help.go +++ b/help.go @@ -190,8 +190,8 @@ func cliArgContains(flagName string, args []string) bool { } func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) { - cur := strings.TrimPrefix(lastArg, "-") - cur = strings.TrimPrefix(cur, "-") + // Trim to handle both "-short" and "--long" flags. + cur := strings.TrimLeft(lastArg, "-") for _, flag := range flags { if bflag, ok := flag.(*BoolFlag); ok && bflag.Hidden { continue