Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the command for printing completion scripts #1998

Merged
merged 21 commits into from
Nov 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions autocomplete/bash_autocomplete
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
#! /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=()
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
__%[1]s_init_completion -n "=:" || return
fi
words=("${words[@]:0:$cword}")
if [[ "$cur" == "-"* ]]; then
Expand All @@ -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
2 changes: 1 addition & 1 deletion autocomplete/powershell_autocomplete.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock {
Invoke-Expression $other | ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}
}
}
31 changes: 15 additions & 16 deletions autocomplete/zsh_autocomplete
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
2 changes: 1 addition & 1 deletion command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
61 changes: 36 additions & 25 deletions completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wonder if we can ignore the error since we are loading from embedFS. Saves us from writing unit tests as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch! I think it's not worth worrying about the error here

Copy link
Member Author

@bartekpacia bartekpacia Nov 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just decided to not additionaly wrap the errors to minimize amount of changes.

I think it's fine now.

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)
Expand All @@ -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
}
2 changes: 1 addition & 1 deletion completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
4 changes: 2 additions & 2 deletions help.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading