This commit is contained in:
Jack Wright 2024-04-23 22:06:31 +00:00 committed by GitHub
commit daff378c57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 336 additions and 6 deletions

View file

@ -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.

View file

@ -835,14 +835,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 {
@ -875,7 +905,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)

View file

@ -2577,6 +2577,7 @@ func TestCompleteCompletion(t *testing.T) {
expected := strings.Join([]string{
"bash",
"fish",
"nushell",
"powershell",
"zsh",
":4",

175
nushell_completions.go Normal file
View file

@ -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)
}

4
nushell_completions.md Normal file
View file

@ -0,0 +1,4 @@
## Generating Nushell Completions For Your cobra.Command
Please refer to [Shell Completions](shell_completions.md) for details.

View file

@ -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")
}
}
}

View file

@ -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)
}
},
}