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

Feature:(issue_351) Add support for generation of shell completion sc… #1606

Merged
merged 6 commits into from
Dec 11, 2022
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
37 changes: 23 additions & 14 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ type App struct {
Commands []*Command
// List of flags to parse
Flags []Flag
// Boolean to enable bash completion commands
EnableBashCompletion bool
// Boolean to enable shell completion commands
EnableShellCompletion bool
// Shell Completion generation command name
ShellCompletionCommandName string
// Boolean to hide built-in help command and help flag
HideHelp bool
// Boolean to hide built-in help command but keep help flag.
Expand All @@ -65,7 +67,7 @@ type App struct {
// flagCategories contains the categorized flags and is populated on app startup
flagCategories FlagCategories
// An action to execute when the shell completion flag is set
BashComplete BashCompleteFunc
ShellComplete ShellCompleteFunc
// An action to execute before any subcommands are run, but after the context is ready
// If a non-nil error is returned, no subcommands are run
Before BeforeFunc
Expand Down Expand Up @@ -131,14 +133,14 @@ type SuggestCommandFunc func(commands []*Command, provided string) string
// Usage, Version and Action.
func NewApp() *App {
return &App{
Name: filepath.Base(os.Args[0]),
Usage: "A new cli application",
UsageText: "",
BashComplete: DefaultAppComplete,
Action: helpCommand.Action,
Reader: os.Stdin,
Writer: os.Stdout,
ErrWriter: os.Stderr,
Name: filepath.Base(os.Args[0]),
Usage: "A new cli application",
UsageText: "",
ShellComplete: DefaultAppComplete,
Action: helpCommand.Action,
Reader: os.Stdin,
Writer: os.Stdout,
ErrWriter: os.Stderr,
}
}

Expand Down Expand Up @@ -168,8 +170,8 @@ func (a *App) Setup() {
a.HideVersion = true
}

if a.BashComplete == nil {
a.BashComplete = DefaultAppComplete
if a.ShellComplete == nil {
a.ShellComplete = DefaultAppComplete
}

if a.Action == nil {
Expand Down Expand Up @@ -227,6 +229,13 @@ func (a *App) Setup() {
a.appendFlag(VersionFlag)
}

if a.EnableShellCompletion {
if a.ShellCompletionCommandName != "" {
completionCommand.Name = a.ShellCompletionCommandName
}
a.appendCommand(completionCommand)
}

a.categories = newCommandCategories()
for _, command := range a.Commands {
a.categories.AddCommand(command.Category, command)
Expand Down Expand Up @@ -260,7 +269,7 @@ func (a *App) newRootCommand() *Command {
UsageText: a.UsageText,
Description: a.Description,
ArgsUsage: a.ArgsUsage,
BashComplete: a.BashComplete,
ShellComplete: a.ShellComplete,
Before: a.Before,
After: a.After,
Action: a.Action,
Expand Down
38 changes: 19 additions & 19 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,11 @@ func ExampleApp_Run_subcommandNoAction() {

func ExampleApp_Run_bashComplete_withShortFlag() {
os.Setenv("SHELL", "bash")
os.Args = []string{"greet", "-", "--generate-bash-completion"}
os.Args = []string{"greet", "-", "--generate-shell-completion"}

app := NewApp()
app.Name = "greet"
app.EnableBashCompletion = true
app.EnableShellCompletion = true
app.Flags = []Flag{
&IntFlag{
Name: "other",
Expand All @@ -260,11 +260,11 @@ func ExampleApp_Run_bashComplete_withShortFlag() {

func ExampleApp_Run_bashComplete_withLongFlag() {
os.Setenv("SHELL", "bash")
os.Args = []string{"greet", "--s", "--generate-bash-completion"}
os.Args = []string{"greet", "--s", "--generate-shell-completion"}

app := NewApp()
app.Name = "greet"
app.EnableBashCompletion = true
app.EnableShellCompletion = true
app.Flags = []Flag{
&IntFlag{
Name: "other",
Expand All @@ -290,11 +290,11 @@ func ExampleApp_Run_bashComplete_withLongFlag() {

func ExampleApp_Run_bashComplete_withMultipleLongFlag() {
os.Setenv("SHELL", "bash")
os.Args = []string{"greet", "--st", "--generate-bash-completion"}
os.Args = []string{"greet", "--st", "--generate-shell-completion"}

app := NewApp()
app.Name = "greet"
app.EnableBashCompletion = true
app.EnableShellCompletion = true
app.Flags = []Flag{
&IntFlag{
Name: "int-flag",
Expand Down Expand Up @@ -323,11 +323,11 @@ func ExampleApp_Run_bashComplete_withMultipleLongFlag() {

func ExampleApp_Run_bashComplete() {
os.Setenv("SHELL", "bash")
os.Args = []string{"greet", "--generate-bash-completion"}
os.Args = []string{"greet", "--generate-shell-completion"}

app := &App{
Name: "greet",
EnableBashCompletion: true,
Name: "greet",
EnableShellCompletion: true,
Commands: []*Command{
{
Name: "describeit",
Expand Down Expand Up @@ -361,12 +361,12 @@ func ExampleApp_Run_bashComplete() {

func ExampleApp_Run_zshComplete() {
// set args for examples sake
os.Args = []string{"greet", "--generate-bash-completion"}
os.Args = []string{"greet", "--generate-shell-completion"}
_ = os.Setenv("SHELL", "/usr/bin/zsh")

app := NewApp()
app.Name = "greet"
app.EnableBashCompletion = true
app.EnableShellCompletion = true
app.Commands = []*Command{
{
Name: "describeit",
Expand Down Expand Up @@ -1369,7 +1369,7 @@ func TestApp_BeforeAfterFuncShellCompletion(t *testing.T) {
var err error

app := &App{
EnableBashCompletion: true,
EnableShellCompletion: true,
Before: func(*Context) error {
counts.Total++
counts.Before = counts.Total
Expand Down Expand Up @@ -1397,7 +1397,7 @@ func TestApp_BeforeAfterFuncShellCompletion(t *testing.T) {
}

// run with the Before() func succeeding
err = app.Run([]string{"command", "--opt", "succeed", "sub", "--generate-bash-completion"})
err = app.Run([]string{"command", "--opt", "succeed", "sub", "--generate-shell-completion"})

if err != nil {
t.Fatalf("Run error: %s", err)
Expand Down Expand Up @@ -1736,8 +1736,8 @@ func TestApp_OrderOfOperations(t *testing.T) {
resetCounts := func() { counts = &opCounts{} }

app := &App{
EnableBashCompletion: true,
BashComplete: func(*Context) {
EnableShellCompletion: true,
ShellComplete: func(*Context) {
counts.Total++
counts.ShellComplete = counts.Total
},
Expand Down Expand Up @@ -1803,7 +1803,7 @@ func TestApp_OrderOfOperations(t *testing.T) {

resetCounts()

_ = app.Run([]string{"command", fmt.Sprintf("--%s", "generate-bash-completion")})
_ = app.Run([]string{"command", fmt.Sprintf("--%s", "generate-shell-completion")})
expect(t, counts.ShellComplete, 1)
expect(t, counts.Total, 1)

Expand Down Expand Up @@ -2618,8 +2618,8 @@ func TestShellCompletionForIncompleteFlags(t *testing.T) {
Name: "test-completion",
},
},
EnableBashCompletion: true,
BashComplete: func(ctx *Context) {
EnableShellCompletion: true,
ShellComplete: func(ctx *Context) {
for _, command := range ctx.App.Commands {
if command.Hidden {
continue
Expand Down Expand Up @@ -2651,7 +2651,7 @@ func TestShellCompletionForIncompleteFlags(t *testing.T) {
},
Writer: io.Discard,
}
err := app.Run([]string{"", "--test-completion", "--" + "generate-bash-completion"})
err := app.Run([]string{"", "--test-completion", "--" + "generate-shell-completion"})
if err != nil {
t.Errorf("app should not return an error: %s", err)
}
Expand Down
4 changes: 2 additions & 2 deletions autocomplete/bash_autocomplete
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ _cli_bash_autocomplete() {
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
if [[ "$cur" == "-"* ]]; then
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-shell-completion )
else
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-shell-completion )
fi
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
Expand Down
2 changes: 1 addition & 1 deletion autocomplete/powershell_autocomplete.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ $fn = $($MyInvocation.MyCommand.Name)
$name = $fn -replace "(.*)\.ps1$", '$1'
Register-ArgumentCompleter -Native -CommandName $name -ScriptBlock {
param($commandName, $wordToComplete, $cursorPosition)
$other = "$wordToComplete --generate-bash-completion"
$other = "$wordToComplete --generate-shell-completion"
Invoke-Expression $other | ForEach-Object {
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
}
Expand Down
4 changes: 2 additions & 2 deletions autocomplete/zsh_autocomplete
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ _cli_zsh_autocomplete() {
local cur
cur=${words[-1]}
if [[ "$cur" == "-"* ]]; then
opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}")
opts=("${(@f)$(${words[@]:0:#words[@]-1} ${cur} --generate-shell-completion)}")
else
opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-bash-completion)}")
opts=("${(@f)$(${words[@]:0:#words[@]-1} --generate-shell-completion)}")
fi

if [[ "${opts[1]}" != "" ]]; then
Expand Down
13 changes: 6 additions & 7 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ type Command struct {
ArgsUsage string
// The category the command is part of
Category string
// The function to call when checking for bash command completions
BashComplete BashCompleteFunc
// The function to call when checking for shell command completions
ShellComplete ShellCompleteFunc
// An action to execute before any sub-subcommands are run, but after the context is ready
// If a non-nil error is returned, no sub-subcommands are run
Before BeforeFunc
Expand Down Expand Up @@ -107,6 +107,9 @@ func (cmd *Command) Command(name string) *Command {
}

func (c *Command) setup(ctx *Context) {
if c.ShellComplete == nil {
c.ShellComplete = DefaultCompleteWithFlags(c)
}
if c.Command(helpCommand.Name) == nil && !c.HideHelp {
if !c.HideHelpCommand {
helpCommand.HelpName = fmt.Sprintf("%s %s", c.HelpName, helpName)
Expand Down Expand Up @@ -149,11 +152,7 @@ func (c *Command) Run(cCtx *Context, arguments ...string) (err error) {
set, err := c.parseFlags(&a, cCtx)
cCtx.flagSet = set

if c.isRoot {
if checkCompletions(cCtx) {
return nil
}
} else if checkCommandCompletions(cCtx, c.Name) {
if checkCompletions(cCtx) {
return nil
}

Expand Down
8 changes: 4 additions & 4 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,8 @@ func TestCommand_Run_CustomShellCompleteAcceptsMalformedFlags(t *testing.T) {
for _, c := range cases {
var outputBuffer bytes.Buffer
app := &App{
Writer: &outputBuffer,
EnableBashCompletion: true,
Writer: &outputBuffer,
EnableShellCompletion: true,
Commands: []*Command{
{
Name: "bar",
Expand All @@ -361,7 +361,7 @@ func TestCommand_Run_CustomShellCompleteAcceptsMalformedFlags(t *testing.T) {
Usage: "A number to parse",
},
},
BashComplete: func(c *Context) {
ShellComplete: func(c *Context) {
fmt.Fprintf(c.App.Writer, "found %d args", c.NArg())
},
},
Expand All @@ -370,7 +370,7 @@ func TestCommand_Run_CustomShellCompleteAcceptsMalformedFlags(t *testing.T) {

osArgs := args{"foo", "bar"}
osArgs = append(osArgs, c.testArgs...)
osArgs = append(osArgs, "--generate-bash-completion")
osArgs = append(osArgs, "--generate-shell-completion")

err := app.Run(osArgs)
stdout := outputBuffer.String()
Expand Down
61 changes: 61 additions & 0 deletions completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package cli

import (
"embed"
"fmt"
"sort"
)

const (
completionCommandName = "generate-completion"
)

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(a *App) (string, error) {
return a.ToFishCompletion()
},
}
)

type renderCompletion func(a *App) (string, error)

func getCompletion(s string) renderCompletion {
return func(a *App) (string, error) {
b, err := autoCompleteFS.ReadFile(s)
return string(b), err
}
}

var completionCommand = &Command{
Name: completionCommandName,
Hidden: true,
Action: func(ctx *Context) error {
var shells []string
for k := range shellCompletions {
shells = append(shells, k)
}
dearchap marked this conversation as resolved.
Show resolved Hide resolved

sort.Strings(shells)

if ctx.Args().Len() == 0 {
return Exit(fmt.Sprintf("no shell provided for completion command. available shells are %+v", shells), 1)
}
s := ctx.Args().First()

if rc, ok := shellCompletions[s]; !ok {
return Exit(fmt.Sprintf("unknown shell %s, available shells are %+v", s, shells), 1)
} else if c, err := rc(ctx.App); err != nil {
return Exit(err, 1)
} else {
ctx.App.Writer.Write([]byte(c))
}
return nil
},
}
Loading