diff --git a/README.md b/README.md index 0fb0373cb..6ffa0cfba 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Cobra provides: * Automatic help generation for commands and flags * Grouping help for subcommands * Automatic help flag recognition of `-h`, `--help`, etc. -* Automatically generated shell autocomplete for your application (bash, zsh, fish, powershell) +* Automatically generated shell autocomplete for your application (bash, zsh, fish, powershell, nushell) * Automatically generated man pages for your application * Command aliases so you can change things without breaking them * The flexibility to define your own help, usage, etc. diff --git a/completions.go b/completions.go index 8fccdaf2c..9bee0a6cd 100644 --- a/completions.go +++ b/completions.go @@ -836,14 +836,46 @@ to your powershell profile. return cmd.Root().GenPowerShellCompletion(out) } return cmd.Root().GenPowerShellCompletionWithDesc(out) - }, } if haveNoDescFlag { powershell.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc) } - completionCmd.AddCommand(bash, zsh, fish, powershell) + nushell := &Command{ + Use: "nushell", + Short: fmt.Sprintf(shortDesc, "nushell"), + Long: fmt.Sprintf(`Generate the autocompletion script for nushell. + +To configure the Nushell cobra external completer for the first time: + # 1. Edit the nushell config file: + > config nu + # 2. Copy the output of %[1]s completion nushell to the end of the file. + # 3. Add a section like the following below at the end of the file: + $env.config.completions.external = { + enable: true + max_results: 100 + completer: $cobra_completer + } + +This completer will work for all cobra based commands. +More information can be found in the External Completions section of the Nushell book: +https://www.nushell.sh/book/custom_completions.html#external-completions + +Information on setting up more than one external completer can be found in the Multiple completer section of the Nushell cookbook: +https://www.nushell.sh/cookbook/external_completers.html#multiple-completer +`, c.Root().Name()), + Args: NoArgs, + ValidArgsFunction: NoFileCompletions, + RunE: func(cmd *Command, args []string) error { + return cmd.Root().GenNushellCompletion(out) + }, + } + if haveNoDescFlag { + nushell.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc) + } + + completionCmd.AddCommand(bash, zsh, fish, powershell, nushell) } func findFlag(cmd *Command, name string) *pflag.Flag { @@ -876,7 +908,7 @@ func CompDebug(msg string, printToStdErr bool) { // variable BASH_COMP_DEBUG_FILE to the path of some file to be used. if path := os.Getenv("BASH_COMP_DEBUG_FILE"); path != "" { f, err := os.OpenFile(path, - os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err == nil { defer f.Close() WriteStringAndCheck(f, msg) diff --git a/completions_test.go b/completions_test.go index df153fcf2..718f55d83 100644 --- a/completions_test.go +++ b/completions_test.go @@ -2577,6 +2577,7 @@ func TestCompleteCompletion(t *testing.T) { expected := strings.Join([]string{ "bash", "fish", + "nushell", "powershell", "zsh", ":4", diff --git a/nushell_completions.go b/nushell_completions.go new file mode 100644 index 000000000..830ffe4d9 --- /dev/null +++ b/nushell_completions.go @@ -0,0 +1,131 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cobra + +import ( + "bytes" + "fmt" + "io" + "os" +) + +func (c *Command) GenNushellCompletion(w io.Writer) error { + buf := new(bytes.Buffer) + WriteStringAndCheck(buf, "# nushell completion -*- shell-script -*- \n") + WriteStringAndCheck(buf, fmt.Sprintf(` +let cobra_completer = {|spans| + let ShellCompDirectiveError = %[1]d + let ShellCompDirectiveNoSpace = %[2]d + let ShellCompDirectiveNoFileComp = %[3]d + let ShellCompDirectiveFilterFileExt = %[4]d + let ShellCompDirectiveFilterDirs = %[5]d + let ShellCompDirectiveKeepOrder = %[6]d + + let cmd = $spans | first + let rest = $spans | skip + + def cobra_log [message] { + let file = do -i {$env | get NUSHELL_COMP_DEBUG_FILE} + if $file != null { + echo $"($message)\n" | save $file --append + } + } + + cobra_log $"External Completer called for cmd ($cmd)" + + def exec_complete [ + spans: list + ] { + # This will catch the stderr message related to the directive and any other errors, + # such as the command not being a cobra based command + let result = do --ignore-errors { COBRA_ACTIVE_HELP=0 run-external $cmd "__complete" ...$spans | complete } + + if $result != null and $result.exit_code == 0 { + let completions = $result.stdout | lines + + # the directive is the last line + let directive = do -i { $completions | last | str replace ':' '' | into int } + + let completions = $completions | drop | each { |it| + # the first word is the command, the rest is the description + let words = $it | split row -r '\s{1}' + + # If the last span contains a hypen and equals, attach it to the name + let last_span = $spans | last + let words = if ($last_span =~ '^-') and ($last_span =~ '=$') { + $words | each {|it| $"($last_span)($it)" } + } else { + $words + } + + {value: ($words | first | str trim), description: ($words | skip | str join ' ')} + } + + {completions: $completions, directive: $directive} + } else { + {completions: [], directive: -1} + } + } + + if (not ($rest | is-empty)) { + let result = exec_complete $rest + let completions = $result.completions + let directive = $result.directive + + # Add space at the end of each completion + let completions = if $directive != $ShellCompDirectiveNoSpace { + $completions | each {|it| {value: $"($it.value) ", description: $it.description}} + } else { + $completions + } + + # Cobra returns a list of completions that are supported with this directive + # There is no way to currently support this in a nushell external completer + let completions = if $directive == $ShellCompDirectiveFilterFileExt { + [] + } else { + $completions + } + + if $directive == $ShellCompDirectiveNoFileComp { + # Allow empty results as this will stop file completion + $completions + } else if ($completions | is-empty) or $directive == $ShellCompDirectiveError { + # Not returning null causes file completions to break + # Return null if there are no completions or ShellCompDirectiveError + null + } else { + $completions + } + } else { + null + } +} +`, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, + ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder)) + + _, err := buf.WriteTo(w) + return err +} + +func (c *Command) GenNushellCompletionFile(filename string) error { + outFile, err := os.Create(filename) + if err != nil { + return err + } + defer outFile.Close() + + return c.GenNushellCompletion(outFile) +} diff --git a/nushell_completions_test.go b/nushell_completions_test.go new file mode 100644 index 000000000..41a98f46d --- /dev/null +++ b/nushell_completions_test.go @@ -0,0 +1,98 @@ +// Copyright 2013-2022 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cobra + +import ( + "bytes" + "errors" + "fmt" + "log" + "os" + "path/filepath" + "testing" +) + +func TestGenNushellCompletion(t *testing.T) { + rootCmd := &Command{Use: "kubectl", Run: emptyRun} + rootCmd.PersistentFlags().String("server", "s", "The address and port of the Kubernetes API server") + rootCmd.PersistentFlags().BoolP("skip-headers", "", false, "The address and port of the Kubernetes API serverIf true, avoid header prefixes in the log messages") + getCmd := &Command{ + Use: "get", + Short: "Display one or many resources", + ArgAliases: []string{"pods", "nodes", "services", "replicationcontrollers", "po", "no", "svc", "rc"}, + ValidArgs: []string{"pod", "node", "service", "replicationcontroller"}, + Run: emptyRun, + } + rootCmd.AddCommand(getCmd) + + buf := new(bytes.Buffer) + assertNoErr(t, rootCmd.GenNushellCompletion(buf)) +} + +func TestGenNushellCompletionFile(t *testing.T) { + tmpFile, err := os.CreateTemp("", "cobra-test") + if err != nil { + log.Fatal(err.Error()) + } + + defer os.RemoveAll(tmpFile.Name()) + + rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun} + child := &Command{ + Use: "child", + ValidArgsFunction: validArgsFunc, + Run: emptyRun, + } + rootCmd.AddCommand(child) + + assertNoErr(t, rootCmd.GenNushellCompletionFile(tmpFile.Name())) +} + +func TestFailGenNushellCompletionFile(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "cobra-test") + if err != nil { + t.Fatal(err.Error()) + } + + defer os.RemoveAll(tmpDir) + + f, _ := os.OpenFile(filepath.Join(tmpDir, "test"), os.O_CREATE, 0400) + defer f.Close() + + rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun} + child := &Command{ + Use: "child", + ValidArgsFunction: validArgsFunc, + Run: emptyRun, + } + rootCmd.AddCommand(child) + + got := rootCmd.GenNushellCompletionFile(f.Name()) + if !errors.Is(got, os.ErrPermission) { + t.Errorf("got: %s, want: %s", got.Error(), os.ErrPermission.Error()) + } +} + +func TestNushellCompletionNoActiveHelp(t *testing.T) { + c := &Command{Use: "c", Run: emptyRun} + + buf := new(bytes.Buffer) + assertNoErr(t, c.GenNushellCompletion(buf)) + output := buf.String() + + // check that active help is being disabled + activeHelpVar := activeHelpGlobalEnvVar + check(t, output, fmt.Sprintf("%s=0", activeHelpVar)) +} diff --git a/site/content/completions/_index.md b/site/content/completions/_index.md index 02257ade2..3548478f0 100644 --- a/site/content/completions/_index.md +++ b/site/content/completions/_index.md @@ -6,6 +6,7 @@ The currently supported shells are: - Zsh - fish - PowerShell +- Nushell Cobra will automatically provide your program with a fully functional `completion` command, similarly to how it provides the `help` command. @@ -28,7 +29,7 @@ and then modifying the generated `cmd/completion.go` file to look something like ```go var completionCmd = &cobra.Command{ - Use: "completion [bash|zsh|fish|powershell]", + Use: "completion [bash|zsh|fish|powershell|nushell]", Short: "Generate completion script", Long: fmt.Sprintf(`To load completions: @@ -68,9 +69,30 @@ PowerShell: # To load completions for every new session, run: PS> %[1]s completion powershell > %[1]s.ps1 # and source this file from your PowerShell profile. + +Nushell: + + # To configure the Nushell cobra external completer for the first time: + # 1. Edit the nushell config file: + > config nu + # 2. Copy the output of %[1]s completion nushell to the end of the file. + # 3. Add a section like the following below at the end of the file: + $env.config.completions.external = { + enable: true + max_results: 100 + completer: $cobra_completer + } + +This completer will work for all cobra based commands. +More information can be found in the External Completions section of the Nushell book: +https://www.nushell.sh/book/custom_completions.html#external-completions + +Information on setting up more than one external completer can be found in the Multiple completer section of the Nushell cookbook: +https://www.nushell.sh/cookbook/external_completers.html#multiple-completer + `,cmd.Root().Name()), DisableFlagsInUseLine: true, - ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + ValidArgs: []string{"bash", "zsh", "fish", "powershell", "nushell"}, Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), Run: func(cmd *cobra.Command, args []string) { switch args[0] { @@ -82,6 +104,8 @@ PowerShell: cmd.Root().GenFishCompletion(os.Stdout, true) case "powershell": cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + case "nushell": + cmd.Root().GenNushellCompletion(os.Stdout, true) } }, } diff --git a/site/content/completions/nushell_completions.md b/site/content/completions/nushell_completions.md new file mode 100644 index 000000000..3bd8065d8 --- /dev/null +++ b/site/content/completions/nushell_completions.md @@ -0,0 +1,3 @@ +## Generating Nushell Completions For Your cobra.Command + +Please refer to [Shell Completions](_index.md#nushell-completions) for details.