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..f6924ba72 100644 --- a/completions.go +++ b/completions.go @@ -836,14 +836,44 @@ 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. Copy the output of the command below: + > %[1]s completion nushell + # 2. Edit the nushell config file: + > config nu + # 3. Paste above the "let-env config" line. + # 4. Change the config block's external_completer line to be external_completer: $cobra_completer + # 5. You will need to start a new shel for this setup to take effect. + +If you have already setup the cobra external completer for other Cobra-based applications: + # 1. Edit the nushell config file: + > config nu + # 2. Modify the cobra_apps variable to contain this new application: + > let cobra_apps = [ "othercobraapp", "%[1]s" ] + # 3. You will need to start a new shell for this setup to take effect. +`, c.Root().Name()), + Args: NoArgs, + ValidArgsFunction: NoFileCompletions, + RunE: func(cmd *Command, args []string) error { + return cmd.Root().GenNushellCompletion(out, !noDesc) + }, + } + 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 +906,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..fb3c11563 --- /dev/null +++ b/nushell_completions.go @@ -0,0 +1,175 @@ +// 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, includeDesc bool) error { + buf := new(bytes.Buffer) + name := c.Name() + WriteStringAndCheck(buf, fmt.Sprintf(` +# A list of cobra apps that completion will be attempted for. +# Add new apps to this list to enable completion for them. +let cobra_apps = ["%[1]s"] + +# An external completer that works with any cobra based +# command line application (e.g. kubectl, minikube) +let cobra_completer = {|spans| + let cmd = $spans.0 + + if not ($cobra_apps | where $cmd =~ $it | is-empty) { + let ShellCompDirectiveError = %[2]d + let ShellCompDirectiveNoSpace = %[3]d + let ShellCompDirectiveNoFileComp = %[4]d + let ShellCompDirectiveFilterFileExt = %[5]d + let ShellCompDirectiveFilterDirs = %[6]d + let last_span = ($spans | last | str trim) + + def exec_complete [ + --fuzzy, + spans: list + ] { + let params = { + last_span: ($spans | last | str trim), + spans: $spans + } + # If there is an equals in the last span + # parse the span into two + let params = if $last_span =~ '=' { + let split = ($last_span | split row '=') + if ($split | length) > 1 { + { + last_span: ($split | last), + spans: ($spans | drop | append ($split | first) | append ($split | last)) + } + } else { + { + last_span: '', + spans: ($spans | drop | append ($split | first) | append '') + } + } + } else { + $params + } + + let last_span = $params.last_span + let spans = $params.spans + + # Drop the last param so we can fuzzy search on it + let spans = if $fuzzy { + $spans | drop + } else { + $spans + } + + # skip the first entry in the span (the command) and join the rest of the span to create __complete args + let cmd_args = ($spans | skip 1 | str join ' ') + + # If the last span entry was empty add "" to the end of the command args + let cmd_args = if ($last_span | is-empty) or $fuzzy { + $'($cmd_args) ""' + } else { + $cmd_args + } + + # The full command to be executed with active help disable (Nushell does not support active help) + let full_cmd = $'COBRA_ACTIVE_HELP=0 ($cmd) __complete ($cmd_args)' + + # Since nushell doesn't have anything like eval, execute in a subshell + let result = (do -i { nu -c $"'($full_cmd)'" } | complete) + + # Create a record with all completion related info. + # directive and directive_str are for posterity + let stdout_lines = ($result.stdout | lines) + let directive = ($stdout_lines | last | str trim | str replace ":" "" | into int) + let completions = ($stdout_lines | drop | parse -r '([\w\-\.:\+\=\/]*)\t?(.*)' | rename value description) + let completions = if $fuzzy { + $completions | where $it.value =~ $last_span + + } else { + ($completions | where {|it| $it.value | str starts-with $last_span }) + } + + { + directive: $directive, + completions: $completions + } + } + + let result = (exec_complete $spans) + let result = if (not ($last_span | is-empty)) and ($result.completions | is-empty) { + exec_complete --fuzzy $spans + } else { + $result + } + + let directive = $result.directive + let completions = $result.completions + + # 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 + } + + let return_val = if $last_span =~ '=' { + # if the completion is of the form -n= return flag as part of the completion so that it doesn't get replaced + $completions | each {|it| $"($last_span | split row '=' | first)=($it.value)" } + } else 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 + } + + $return_val + } else { + null + } +} +`, name, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, + ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) + + _, err := buf.WriteTo(w) + return err +} + +func (c *Command) GenNushellCompletionFile(filename string, includeDesc bool) error { + outFile, err := os.Create(filename) + if err != nil { + return err + } + defer outFile.Close() + + return c.GenNushellCompletion(outFile, includeDesc) +} diff --git a/nushell_completions.md b/nushell_completions.md new file mode 100644 index 000000000..e0e94eb14 --- /dev/null +++ b/nushell_completions.md @@ -0,0 +1,4 @@ +## Generating Nushell Completions For Your cobra.Command + +Please refer to [Shell Completions](shell_completions.md) for details. + diff --git a/nushell_completions_test.go b/nushell_completions_test.go new file mode 100644 index 000000000..73fedd073 --- /dev/null +++ b/nushell_completions_test.go @@ -0,0 +1,97 @@ +// 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" + "log" + "os" + "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, true)) + output := buf.String() + + check(t, output, fmt.Sprintf("let cobra_apps = [\"%[1]s\"]", rootCmd.Name())) +} + +func TestGenNushellCompletionFile(t *testing.T) { + err := os.Mkdir("./tmp", 0o755) + if err != nil { + log.Fatal(err.Error()) + } + + defer os.RemoveAll("./tmp") + + rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun} + child := &Command{ + Use: "child", + ValidArgsFunction: validArgsFunc, + Run: emptyRun, + } + rootCmd.AddCommand(child) + + assertNoErr(t, rootCmd.GenNushellCompletionFile("./tmp/test", true)) +} + +func TestFailGenNushellCompletionFile(t *testing.T) { + err := os.Mkdir("./tmp", 0o755) + if err != nil { + log.Fatal(err.Error()) + } + + defer os.RemoveAll("./tmp") + + f, _ := os.OpenFile("./tmp/test", os.O_CREATE, 0o400) + 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("./tmp/test", true) + if got == nil { + t.Error("should raise permission denied error") + } + + if os.Getenv("MSYSTEM") == "MINGW64" { + if got.Error() != "open ./tmp/test: Access is denied." { + t.Errorf("got: %s, want: %s", got.Error(), "open ./tmp/test: Access is denied.") + } + } else { + if got.Error() != "open ./tmp/test: permission denied" { + t.Errorf("got: %s, want: %s", got.Error(), "open ./tmp/test: permission denied") + } + } +} diff --git a/site/content/completions/_index.md b/site/content/completions/_index.md index 02257ade2..2f598c7c7 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,29 @@ 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. Copy the output of the command below: + > %[1]s completion nushell + # 2. Edit the nushell config file: + > config nu + # 3. Paste above the "let-env config" line. + # 4. Change the config block's external_completer line to be + external_completer: $cobra_completer + # 5. You will need to start a new shell or for this setup to take effect. + + # If you have already setup the cobra external completer: + # 1. Edit the nushell config file: + > config nu + # 2. Modify the cobra_apps varible to contain this application: + > let cobra_apps = [ "othercobraapp", "%[1]s" ] + # 3. You will need to start a new shell for this setup to take effect. + `,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 +103,8 @@ PowerShell: cmd.Root().GenFishCompletion(os.Stdout, true) case "powershell": cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + case "nushell": + cmd.Root().GenNushellCompletion(os.Stdout, true) } }, }