From fc30c1eba15c55f12129447ce77c1821f7d05531 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Sun, 27 Oct 2024 16:48:05 +0100 Subject: [PATCH 01/21] fix linter warning --- completion.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/completion.go b/completion.go index d344fc4d5c..39397f274c 100644 --- a/completion.go +++ b/completion.go @@ -44,7 +44,7 @@ func buildCompletionCommand() *Command { } } -func completionCommandAction(ctx context.Context, cmd *Command) error { +func completionCommandAction(_ context.Context, cmd *Command) error { var shells []string for k := range shellCompletions { shells = append(shells, k) From ed434ac7b503fdcaf4f7b15f2d70cd611956fa79 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Sun, 27 Oct 2024 17:02:01 +0100 Subject: [PATCH 02/21] update autocomplete script with useful link --- autocomplete/zsh_autocomplete | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/autocomplete/zsh_autocomplete b/autocomplete/zsh_autocomplete index 943cf60b95..110254cb9a 100644 --- a/autocomplete/zsh_autocomplete +++ b/autocomplete/zsh_autocomplete @@ -24,7 +24,8 @@ _program() { fi } -# don't run the completion function when being source-ed or eval-ed +# 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]" = "_program" ]; then _program fi From bf5d7924be765dfed44d8162d17147b908a23048 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Mon, 28 Oct 2024 01:10:40 +0100 Subject: [PATCH 03/21] start impl --- command.go | 2 +- completion.go | 79 +++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/command.go b/command.go index 3970f1c355..e80432ade2 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(cmd.Name) if cmd.ShellCompletionCommandName != "" { tracef( diff --git a/completion.go b/completion.go index 39397f274c..b7c772b039 100644 --- a/completion.go +++ b/completion.go @@ -8,43 +8,80 @@ 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. + completionFlagName = "generate-shell-completion" + completionFlag = "--" + completionFlagName ) var ( //go:embed autocomplete autoCompleteFS embed.FS - shellCompletions = map[string]renderCompletion{ + shellCompletions = map[string]renderCompletionFunc{ "bash": getCompletion("autocomplete/bash_autocomplete"), "ps": getCompletion("autocomplete/powershell_autocomplete.ps1"), - "zsh": getCompletion("autocomplete/zsh_autocomplete"), - "fish": func(c *Command) (string, error) { + "zsh": func(cmd *Command, appName string) (string, error) { + return genZshCompletion(appName), nil + }, + "fish": func(c *Command, appName string) (string, error) { return c.ToFishCompletion() }, } ) -type renderCompletion func(*Command) (string, error) +type renderCompletionFunc func(cmd *Command, appName string) (string, error) -func getCompletion(s string) renderCompletion { - return func(c *Command) (string, error) { +func getCompletion(s string) renderCompletionFunc { + return func(cmd *Command, appName string) (string, error) { b, err := autoCompleteFS.ReadFile(s) return string(b), err } } -func buildCompletionCommand() *Command { +func genZshCompletion(appName string) string { + return fmt.Sprintf(`#compdef %[1]s +compdef _%[1]s %[1]s + +# This is a shell completion script auto-generated by https://github.com/urfave/cli. + +_%[1]s() { + local -a opts + local cur + cur=${words[-1]} + if [[ "$cur" == "-"* ]]; then + opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-shell-completion)}") + else + opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-shell-completion)}") + fi + + if [[ "${opts[1]}" != "" ]]; then + _describe 'values' opts + else + _files + fi +} + +# 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 +`, appName) +} + +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(_ 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 +94,20 @@ func completionCommandAction(_ 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 } From 717b07ce8077f3fe09c7eae5d228f31059667e6a Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Mon, 28 Oct 2024 01:46:51 +0100 Subject: [PATCH 04/21] delete zsh_autocomplete file --- autocomplete/zsh_autocomplete | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 autocomplete/zsh_autocomplete diff --git a/autocomplete/zsh_autocomplete b/autocomplete/zsh_autocomplete deleted file mode 100644 index 110254cb9a..0000000000 --- a/autocomplete/zsh_autocomplete +++ /dev/null @@ -1,31 +0,0 @@ -#compdef program -compdef _program program - -# 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 - -_program() { - local -a opts - local cur - cur=${words[-1]} - if [[ "$cur" == "-"* ]]; then - opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-shell-completion)}") - else - opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-shell-completion)}") - fi - - if [[ "${opts[1]}" != "" ]]; then - _describe 'values' opts - else - _files - fi -} - -# 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]" = "_program" ]; then - _program -fi From f554abb00b96928097e05093b4aeb0511f56f665 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Mon, 28 Oct 2024 01:48:31 +0100 Subject: [PATCH 05/21] create a simple app to test out completion --- examples/simpletask/main.go | 70 +++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 examples/simpletask/main.go diff --git a/examples/simpletask/main.go b/examples/simpletask/main.go new file mode 100644 index 0000000000..a797fa86a3 --- /dev/null +++ b/examples/simpletask/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/urfave/cli/v3" +) + +func main() { + app := &cli.Command{ + Name: "simpletask", + Usage: "a dead simple task manager", + EnableShellCompletion: true, + Action: func(ctx context.Context, command *cli.Command) error { + fmt.Println("decide what to do!") + return nil + }, + Commands: []*cli.Command{ + { + Name: "add", + Aliases: []string{"a"}, + Usage: "add a task to the list", + Action: func(ctx context.Context, cmd *cli.Command) error { + fmt.Println("added task: ", cmd.Args().First()) + return nil + }, + }, + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(ctx context.Context, cmd *cli.Command) error { + fmt.Println("completed task: ", cmd.Args().First()) + return nil + }, + }, + { + Name: "template", + Aliases: []string{"t"}, + Usage: "options for task templates", + Commands: []*cli.Command{ + { + Name: "add", + Usage: "add a new template", + Action: func(ctx context.Context, cmd *cli.Command) error { + fmt.Println("new task template: ", cmd.Args().First()) + return nil + }, + }, + { + Name: "remove", + Usage: "remove an existing template", + Action: func(ctx context.Context, cmd *cli.Command) error { + fmt.Println("removed task template: ", cmd.Args().First()) + return nil + }, + }, + }, + }, + }, + } + + err := app.Run(context.Background(), os.Args) + if err != nil { + log.Fatal(err) + } +} From f81b2cba609744e05b62bdaae3d466b53429f3be Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Mon, 28 Oct 2024 02:06:28 +0100 Subject: [PATCH 06/21] help.go printFlagSuggestions(): add small comment --- help.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/help.go b/help.go index 10c2f6e8f0..527f6d7b91 100644 --- a/help.go +++ b/help.go @@ -190,8 +190,9 @@ 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 the prefix twice to handle both "-short" and "--long" flags. + currentArg := strings.TrimPrefix(lastArg, "-") + currentArg = strings.TrimPrefix(currentArg, "-") for _, flag := range flags { if bflag, ok := flag.(*BoolFlag); ok && bflag.Hidden { continue @@ -214,7 +215,7 @@ func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) { continue } // match if last argument matches this flag and it is not repeated - if strings.HasPrefix(name, cur) && cur != name && !cliArgContains(name, os.Args) { + if strings.HasPrefix(name, currentArg) && currentArg != name && !cliArgContains(name, os.Args) { flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) if usage != "" && strings.HasSuffix(os.Getenv("SHELL"), "zsh") { flagCompletion = fmt.Sprintf("%s:%s", flagCompletion, usage) From 4dcd89b523e7718c82f32d12f7a9f1b873b2d738 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Mon, 28 Oct 2024 02:24:19 +0100 Subject: [PATCH 07/21] fix tests --- completion_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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") } From c84b5cae695e3c93a1b35a73f5b15c819ecbf535 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Wed, 30 Oct 2024 23:14:04 +0100 Subject: [PATCH 08/21] zsh completion: add comments to help understand code --- completion.go | 64 ++++++++++++++++++++++++++------------------------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/completion.go b/completion.go index b7c772b039..4379f7557e 100644 --- a/completion.go +++ b/completion.go @@ -40,37 +40,6 @@ func getCompletion(s string) renderCompletionFunc { } } -func genZshCompletion(appName string) string { - return fmt.Sprintf(`#compdef %[1]s -compdef _%[1]s %[1]s - -# This is a shell completion script auto-generated by https://github.com/urfave/cli. - -_%[1]s() { - local -a opts - local cur - cur=${words[-1]} - if [[ "$cur" == "-"* ]]; then - opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-shell-completion)}") - else - opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-shell-completion)}") - fi - - if [[ "${opts[1]}" != "" ]]; then - _describe 'values' opts - else - _files - fi -} - -# 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 -`, appName) -} - func buildCompletionCommand(appName string) *Command { return &Command{ Name: completionCommandName, @@ -111,3 +80,36 @@ func printShellCompletion(_ context.Context, cmd *Command, appName string) error return nil } + +func genZshCompletion(appName string) string { + return fmt.Sprintf(`#compdef %[1]s +compdef _%[1]s %[1]s + +# This is a shell completion script auto-generated by https://github.com/urfave/cli. + +_%[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 + + if [[ "${opts[1]}" != "" ]]; then + _describe 'values' opts + else + _files + fi +} + +# 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 +`, appName) +} From cffef653bb47bb616f0aa543d3c4e637771f20af Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Wed, 30 Oct 2024 23:49:21 +0100 Subject: [PATCH 09/21] move all autocomplete shell files into Go code strings --- autocomplete/bash_autocomplete | 35 ------------------------ autocomplete/powershell_autocomplete.ps1 | 9 ------ 2 files changed, 44 deletions(-) delete mode 100755 autocomplete/bash_autocomplete delete mode 100644 autocomplete/powershell_autocomplete.ps1 diff --git a/autocomplete/bash_autocomplete b/autocomplete/bash_autocomplete deleted file mode 100755 index 7120a0d222..0000000000 --- a/autocomplete/bash_autocomplete +++ /dev/null @@ -1,35 +0,0 @@ -#! /bin/bash - -: ${PROG:=$(basename ${BASH_SOURCE})} - -# 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() { - COMPREPLY=() - _get_comp_words_by_ref "$@" cur prev words cword -} - -_cli_bash_autocomplete() { - if [[ "${COMP_WORDS[0]}" != "source" ]]; then - local cur opts base words - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - if declare -F _init_completion >/dev/null 2>&1; then - _init_completion -n "=:" || return - else - _cli_init_completion -n "=:" || return - fi - words=("${words[@]:0:$cword}") - if [[ "$cur" == "-"* ]]; then - requestComp="${words[*]} ${cur} --generate-shell-completion" - else - requestComp="${words[*]} --generate-shell-completion" - fi - opts=$(eval "${requestComp}" 2>/dev/null) - COMPREPLY=($(compgen -W "${opts}" -- ${cur})) - return 0 - fi -} - -complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG -unset PROG diff --git a/autocomplete/powershell_autocomplete.ps1 b/autocomplete/powershell_autocomplete.ps1 deleted file mode 100644 index cbf76942dd..0000000000 --- a/autocomplete/powershell_autocomplete.ps1 +++ /dev/null @@ -1,9 +0,0 @@ -$fn = $($MyInvocation.MyCommand.Name) -$name = $fn -replace "(.*)\.ps1$", '$1' -Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock { - param($commandName, $wordToComplete, $cursorPosition) - $other = "$wordToComplete --generate-shell-completion" - Invoke-Expression $other | ForEach-Object { - [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) - } - } \ No newline at end of file From 24524dad7cd38fce505955566a65e1bc3df613bc Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Wed, 30 Oct 2024 23:49:25 +0100 Subject: [PATCH 10/21] impl --- completion.go | 91 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 25 deletions(-) diff --git a/completion.go b/completion.go index 4379f7557e..cecb2de8cd 100644 --- a/completion.go +++ b/completion.go @@ -2,7 +2,6 @@ package cli import ( "context" - "embed" "fmt" "sort" ) @@ -10,36 +9,28 @@ import ( const ( completionCommandName = "completion" - // This flag is supposed to only be used by the completion script itself, to generate completions on the fly. + // This flag is supposed to only be used by the completion script itself to generate completions on the fly. completionFlagName = "generate-shell-completion" completionFlag = "--" + completionFlagName ) -var ( - //go:embed autocomplete - autoCompleteFS embed.FS - - shellCompletions = map[string]renderCompletionFunc{ - "bash": getCompletion("autocomplete/bash_autocomplete"), - "ps": getCompletion("autocomplete/powershell_autocomplete.ps1"), - "zsh": func(cmd *Command, appName string) (string, error) { - return genZshCompletion(appName), nil - }, - "fish": func(c *Command, appName string) (string, error) { - return c.ToFishCompletion() - }, - } -) +var shellCompletions = map[string]renderCompletionFunc{ + "bash": func(cmd *Command, appName string) (string, error) { + return genBashCompletion(appName), nil + }, + "zsh": func(cmd *Command, appName string) (string, error) { + return genZshCompletion(appName), nil + }, + "fish": func(c *Command, appName string) (string, error) { + return c.ToFishCompletion() + }, + "pwsh": func(cmd *Command, appName string) (string, error) { + return genPwshCompletion(), nil + }, +} type renderCompletionFunc func(cmd *Command, appName string) (string, error) -func getCompletion(s string) renderCompletionFunc { - return func(cmd *Command, appName string) (string, error) { - b, err := autoCompleteFS.ReadFile(s) - return string(b), err - } -} - func buildCompletionCommand(appName string) *Command { return &Command{ Name: completionCommandName, @@ -81,11 +72,49 @@ func printShellCompletion(_ context.Context, cmd *Command, appName string) error return nil } +func genBashCompletion(appName string) string { + return fmt.Sprintf(`#!/usr/env/bin bash + +# 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() { + COMPREPLY=() + _get_comp_words_by_ref "$@" cur prev words cword +} + +_cli_bash_autocomplete() { + if [[ "${COMP_WORDS[0]}" != "source" ]]; then + local cur opts base words + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + if declare -F _init_completion >/dev/null 2>&1; then + _init_completion -n "=:" || return + else + _cli_init_completion -n "=:" || return + fi + words=("${words[@]:0:$cword}") + if [[ "$cur" == "-"* ]]; then + requestComp="${words[*]} ${cur} --generate-shell-completion" + else + requestComp="${words[*]} --generate-shell-completion" + fi + opts=$(eval "${requestComp}" 2>/dev/null) + COMPREPLY=($(compgen -W "${opts}" -- ${cur})) + return 0 + fi +} + +complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete %[1]s +`, appName) +} + func genZshCompletion(appName string) string { return fmt.Sprintf(`#compdef %[1]s compdef _%[1]s %[1]s -# This is a shell completion script auto-generated by https://github.com/urfave/cli. +# This is a shell completion script auto-generated by https://github.com/urfave/cli for zsh. _%[1]s() { local -a opts # Declare a local array @@ -113,3 +142,15 @@ if [ "$funcstack[1]" = "_%[1]s" ]; then fi `, appName) } + +func genPwshCompletion() string { + return `$fn = $($MyInvocation.MyCommand.Name) +$name = $fn -replace "(.*)\.ps1$", '$1' +Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock { + param($commandName, $wordToComplete, $cursorPosition) + $other = "$wordToComplete --generate-shell-completion" + Invoke-Expression $other | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } + }` +} From 319a0612dbd98550405935fd25dcc645ff82412f Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Tue, 5 Nov 2024 21:05:03 +0100 Subject: [PATCH 11/21] use osArgs instead of cmd.Name Co-authored-by: dearchap --- command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command.go b/command.go index e80432ade2..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(cmd.Name) + completionCommand := buildCompletionCommand(osArgs[0]) if cmd.ShellCompletionCommandName != "" { tracef( From cc5c66ab07fc5c38d23f05c3dea36a83087edaea Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Tue, 5 Nov 2024 21:19:03 +0100 Subject: [PATCH 12/21] bash completion: fix function names --- completion.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/completion.go b/completion.go index cecb2de8cd..0c1729d466 100644 --- a/completion.go +++ b/completion.go @@ -79,12 +79,12 @@ func genBashCompletion(appName string) string { # 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=() @@ -92,7 +92,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 @@ -106,7 +106,7 @@ _cli_bash_autocomplete() { fi } -complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete %[1]s +complete -o bashdefault -o default -o nospace -F __%[1]s_bash_autocomplete %[1]s `, appName) } From d211484f9f170becfd99c012e9fd77cf6000155e Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Wed, 6 Nov 2024 01:52:13 +0100 Subject: [PATCH 13/21] remove redundant variable completionFlagName --- completion.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/completion.go b/completion.go index 0c1729d466..b8d9dd3618 100644 --- a/completion.go +++ b/completion.go @@ -10,8 +10,7 @@ const ( completionCommandName = "completion" // This flag is supposed to only be used by the completion script itself to generate completions on the fly. - completionFlagName = "generate-shell-completion" - completionFlag = "--" + completionFlagName + completionFlag = "--generate-shell-completion" ) var shellCompletions = map[string]renderCompletionFunc{ From e27feb4f7daaec964c9d0776ca2bc517336ff8cb Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Sat, 23 Nov 2024 22:14:31 +0100 Subject: [PATCH 14/21] Revert "move all autocomplete shell files into Go code strings" This reverts commit cffef653bb47bb616f0aa543d3c4e637771f20af. --- autocomplete/bash_autocomplete | 35 ++++++++++++++++++++++++ autocomplete/powershell_autocomplete.ps1 | 9 ++++++ 2 files changed, 44 insertions(+) create mode 100755 autocomplete/bash_autocomplete create mode 100644 autocomplete/powershell_autocomplete.ps1 diff --git a/autocomplete/bash_autocomplete b/autocomplete/bash_autocomplete new file mode 100755 index 0000000000..7120a0d222 --- /dev/null +++ b/autocomplete/bash_autocomplete @@ -0,0 +1,35 @@ +#! /bin/bash + +: ${PROG:=$(basename ${BASH_SOURCE})} + +# 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() { + COMPREPLY=() + _get_comp_words_by_ref "$@" cur prev words cword +} + +_cli_bash_autocomplete() { + if [[ "${COMP_WORDS[0]}" != "source" ]]; then + local cur opts base words + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + if declare -F _init_completion >/dev/null 2>&1; then + _init_completion -n "=:" || return + else + _cli_init_completion -n "=:" || return + fi + words=("${words[@]:0:$cword}") + if [[ "$cur" == "-"* ]]; then + requestComp="${words[*]} ${cur} --generate-shell-completion" + else + requestComp="${words[*]} --generate-shell-completion" + fi + opts=$(eval "${requestComp}" 2>/dev/null) + COMPREPLY=($(compgen -W "${opts}" -- ${cur})) + return 0 + fi +} + +complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG +unset PROG diff --git a/autocomplete/powershell_autocomplete.ps1 b/autocomplete/powershell_autocomplete.ps1 new file mode 100644 index 0000000000..cbf76942dd --- /dev/null +++ b/autocomplete/powershell_autocomplete.ps1 @@ -0,0 +1,9 @@ +$fn = $($MyInvocation.MyCommand.Name) +$name = $fn -replace "(.*)\.ps1$", '$1' +Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock { + param($commandName, $wordToComplete, $cursorPosition) + $other = "$wordToComplete --generate-shell-completion" + Invoke-Expression $other | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } + } \ No newline at end of file From 9b41c34f665d6e743ea395c0020fd1388a379191 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Sat, 23 Nov 2024 22:27:01 +0100 Subject: [PATCH 15/21] move scripts back to files --- autocomplete/bash_autocomplete | 13 ++- autocomplete/powershell_autocomplete.ps1 | 2 +- autocomplete/zsh_autocomplete | 29 +++++ completion.go | 131 ++++++----------------- 4 files changed, 69 insertions(+), 106 deletions(-) create mode 100644 autocomplete/zsh_autocomplete diff --git a/autocomplete/bash_autocomplete b/autocomplete/bash_autocomplete index 7120a0d222..116c699921 100755 --- a/autocomplete/bash_autocomplete +++ b/autocomplete/bash_autocomplete @@ -1,15 +1,15 @@ -#! /bin/bash +#!/usr/env/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 new file mode 100644 index 0000000000..d24049a72f --- /dev/null +++ b/autocomplete/zsh_autocomplete @@ -0,0 +1,29 @@ +#compdef %[1]s +compdef _%[1]s %[1]s + +# This is a shell completion script auto-generated by https://github.com/urfave/cli for zsh. + +_%[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 + + if [[ "${opts[1]}" != "" ]]; then + _describe 'values' opts + else + _files + fi +} + +# 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/completion.go b/completion.go index b8d9dd3618..2886ab45f0 100644 --- a/completion.go +++ b/completion.go @@ -2,6 +2,7 @@ package cli import ( "context" + "embed" "fmt" "sort" ) @@ -13,22 +14,39 @@ const ( completionFlag = "--generate-shell-completion" ) -var shellCompletions = map[string]renderCompletionFunc{ - "bash": func(cmd *Command, appName string) (string, error) { - return genBashCompletion(appName), nil - }, - "zsh": func(cmd *Command, appName string) (string, error) { - return genZshCompletion(appName), nil - }, - "fish": func(c *Command, appName string) (string, error) { - return c.ToFishCompletion() - }, - "pwsh": func(cmd *Command, appName string) (string, error) { - return genPwshCompletion(), nil - }, -} +type renderCompletion func(cmd *Command, appName string) (string, error) -type renderCompletionFunc func(cmd *Command, appName string) (string, error) +var ( + //go:embed autocomplete + autoCompleteFS embed.FS + + shellCompletions = map[string]renderCompletion{ + "bash": func(c *Command, appName string) (string, error) { + b, err := autoCompleteFS.ReadFile("autocomplete/bash_autocomplete") + if err != nil { + return "", fmt.Errorf("read file: %w", err) + } + return fmt.Sprintf(string(b), appName), nil + }, + "zsh": func(c *Command, appName string) (string, error) { + b, err := autoCompleteFS.ReadFile("autocomplete/zsh_autocomplete") + if err != nil { + return "", fmt.Errorf("read file: %w", err) + } + return fmt.Sprintf(string(b), appName), nil + }, + "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") + if err != nil { + return "", fmt.Errorf("read file: %w", err) + } + return string(b), nil + }, + } +) func buildCompletionCommand(appName string) *Command { return &Command{ @@ -70,86 +88,3 @@ func printShellCompletion(_ context.Context, cmd *Command, appName string) error return nil } - -func genBashCompletion(appName string) string { - return fmt.Sprintf(`#!/usr/env/bin bash - -# 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. -__%[1]s_init_completion() { - COMPREPLY=() - _get_comp_words_by_ref "$@" cur prev words cword -} - -__%[1]s_bash_autocomplete() { - if [[ "${COMP_WORDS[0]}" != "source" ]]; then - local cur opts base words - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - if declare -F _init_completion >/dev/null 2>&1; then - _init_completion -n "=:" || return - else - __%[1]s_init_completion -n "=:" || return - fi - words=("${words[@]:0:$cword}") - if [[ "$cur" == "-"* ]]; then - requestComp="${words[*]} ${cur} --generate-shell-completion" - else - requestComp="${words[*]} --generate-shell-completion" - fi - opts=$(eval "${requestComp}" 2>/dev/null) - COMPREPLY=($(compgen -W "${opts}" -- ${cur})) - return 0 - fi -} - -complete -o bashdefault -o default -o nospace -F __%[1]s_bash_autocomplete %[1]s -`, appName) -} - -func genZshCompletion(appName string) string { - return fmt.Sprintf(`#compdef %[1]s -compdef _%[1]s %[1]s - -# This is a shell completion script auto-generated by https://github.com/urfave/cli for zsh. - -_%[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 - - if [[ "${opts[1]}" != "" ]]; then - _describe 'values' opts - else - _files - fi -} - -# 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 -`, appName) -} - -func genPwshCompletion() string { - return `$fn = $($MyInvocation.MyCommand.Name) -$name = $fn -replace "(.*)\.ps1$", '$1' -Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock { - param($commandName, $wordToComplete, $cursorPosition) - $other = "$wordToComplete --generate-shell-completion" - Invoke-Expression $other | ForEach-Object { - [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) - } - }` -} From de30973e16f3853ccd839de470cb502df28239bc Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Sun, 24 Nov 2024 19:03:59 +0100 Subject: [PATCH 16/21] revert var name change --- help.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/help.go b/help.go index 527f6d7b91..241c2744a4 100644 --- a/help.go +++ b/help.go @@ -191,8 +191,8 @@ func cliArgContains(flagName string, args []string) bool { func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) { // Trim the prefix twice to handle both "-short" and "--long" flags. - currentArg := strings.TrimPrefix(lastArg, "-") - currentArg = strings.TrimPrefix(currentArg, "-") + cur := strings.TrimPrefix(lastArg, "-") + cur = strings.TrimPrefix(cur, "-") for _, flag := range flags { if bflag, ok := flag.(*BoolFlag); ok && bflag.Hidden { continue @@ -215,7 +215,7 @@ func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) { continue } // match if last argument matches this flag and it is not repeated - if strings.HasPrefix(name, currentArg) && currentArg != name && !cliArgContains(name, os.Args) { + if strings.HasPrefix(name, cur) && cur != name && !cliArgContains(name, os.Args) { flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) if usage != "" && strings.HasSuffix(os.Getenv("SHELL"), "zsh") { flagCompletion = fmt.Sprintf("%s:%s", flagCompletion, usage) From afd23dfb3e0a8fdb1d065d5d567866c47d8edfca Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Sun, 24 Nov 2024 19:04:51 +0100 Subject: [PATCH 17/21] delete `simpletask` example program --- examples/simpletask/main.go | 70 ------------------------------------- 1 file changed, 70 deletions(-) delete mode 100644 examples/simpletask/main.go diff --git a/examples/simpletask/main.go b/examples/simpletask/main.go deleted file mode 100644 index a797fa86a3..0000000000 --- a/examples/simpletask/main.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "os" - - "github.com/urfave/cli/v3" -) - -func main() { - app := &cli.Command{ - Name: "simpletask", - Usage: "a dead simple task manager", - EnableShellCompletion: true, - Action: func(ctx context.Context, command *cli.Command) error { - fmt.Println("decide what to do!") - return nil - }, - Commands: []*cli.Command{ - { - Name: "add", - Aliases: []string{"a"}, - Usage: "add a task to the list", - Action: func(ctx context.Context, cmd *cli.Command) error { - fmt.Println("added task: ", cmd.Args().First()) - return nil - }, - }, - { - Name: "complete", - Aliases: []string{"c"}, - Usage: "complete a task on the list", - Action: func(ctx context.Context, cmd *cli.Command) error { - fmt.Println("completed task: ", cmd.Args().First()) - return nil - }, - }, - { - Name: "template", - Aliases: []string{"t"}, - Usage: "options for task templates", - Commands: []*cli.Command{ - { - Name: "add", - Usage: "add a new template", - Action: func(ctx context.Context, cmd *cli.Command) error { - fmt.Println("new task template: ", cmd.Args().First()) - return nil - }, - }, - { - Name: "remove", - Usage: "remove an existing template", - Action: func(ctx context.Context, cmd *cli.Command) error { - fmt.Println("removed task template: ", cmd.Args().First()) - return nil - }, - }, - }, - }, - }, - } - - err := app.Run(context.Background(), os.Args) - if err != nil { - log.Fatal(err) - } -} From 4f29af7a62c092ec8601aeb012c80e93c82aacdb Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Sun, 24 Nov 2024 19:05:59 +0100 Subject: [PATCH 18/21] use TrimLeft instead of TrimPrefix --- help.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/help.go b/help.go index 241c2744a4..eb07455c94 100644 --- a/help.go +++ b/help.go @@ -190,9 +190,8 @@ func cliArgContains(flagName string, args []string) bool { } func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) { - // Trim the prefix twice to handle both "-short" and "--long" flags. - 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 From 19c9c1e34e9f442b0dab59fddc49fdc09ff6aef6 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Sun, 24 Nov 2024 20:05:51 +0100 Subject: [PATCH 19/21] fix mistake in shebang --- autocomplete/bash_autocomplete | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autocomplete/bash_autocomplete b/autocomplete/bash_autocomplete index 116c699921..bde285c5e5 100755 --- a/autocomplete/bash_autocomplete +++ b/autocomplete/bash_autocomplete @@ -1,4 +1,4 @@ -#!/usr/env/bin bash +#!/usr/bin/env bash # This is a shell completion script auto-generated by https://github.com/urfave/cli for bash. From 4f2cec40cd5a15f36a375e3c330090007ac7d0f1 Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Sun, 24 Nov 2024 21:20:24 +0100 Subject: [PATCH 20/21] dont wrap error --- completion.go | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/completion.go b/completion.go index 2886ab45f0..d778380474 100644 --- a/completion.go +++ b/completion.go @@ -23,27 +23,18 @@ var ( shellCompletions = map[string]renderCompletion{ "bash": func(c *Command, appName string) (string, error) { b, err := autoCompleteFS.ReadFile("autocomplete/bash_autocomplete") - if err != nil { - return "", fmt.Errorf("read file: %w", err) - } - return fmt.Sprintf(string(b), appName), nil + return fmt.Sprintf(string(b), appName), err }, "zsh": func(c *Command, appName string) (string, error) { b, err := autoCompleteFS.ReadFile("autocomplete/zsh_autocomplete") - if err != nil { - return "", fmt.Errorf("read file: %w", err) - } - return fmt.Sprintf(string(b), appName), nil + 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") - if err != nil { - return "", fmt.Errorf("read file: %w", err) - } - return string(b), nil + return string(b), err }, } ) From c5ff6533883cc210b36d626544ac6a114ae8caca Mon Sep 17 00:00:00 2001 From: Bartek Pacia Date: Sun, 24 Nov 2024 21:23:39 +0100 Subject: [PATCH 21/21] bring back /bin/bash shebang --- autocomplete/bash_autocomplete | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autocomplete/bash_autocomplete b/autocomplete/bash_autocomplete index bde285c5e5..d63937d971 100755 --- a/autocomplete/bash_autocomplete +++ b/autocomplete/bash_autocomplete @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash # This is a shell completion script auto-generated by https://github.com/urfave/cli for bash.