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

Completion support for Nushell #1857

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a1431b2
'Completions for nushell'
ayax79 Nov 12, 2022
594faef
Changed the nushell completion implementation to be a nushell externa…
ayax79 Nov 26, 2022
2f276e3
Incorporating pull request feedback.
ayax79 Nov 27, 2022
1324e0c
Added nushell to the Use: line
ayax79 Nov 27, 2022
37ac745
Added a whitelist to add cobra apps to to prevent non-cobra apps from
ayax79 Nov 27, 2022
fa746d4
Incorporating pull request feedback
ayax79 Nov 28, 2022
d86bac4
"fixing oddities with fuzzy searching and bug with active help"
ayax79 Dec 15, 2022
247e8e6
"ignoring return value when directive is ShellComp#directiveFilterFil…
ayax79 Dec 16, 2022
3b01684
"fixing completions that contain a /"
ayax79 Dec 16, 2022
4c4bde6
"removing extra lines/whitespace"
ayax79 Dec 24, 2022
3bfdd64
Update completions.go
ayax79 Dec 24, 2022
ead0ff3
Update completions.go
ayax79 Dec 24, 2022
8cb9728
Update completions.go
ayax79 Dec 24, 2022
1a69a83
"Added nushell to list of shell autompletes"
ayax79 Dec 24, 2022
f963733
"Reverted to old version without formatting changes and readded nushell"
ayax79 Dec 24, 2022
29af015
"fixed whitespace"
ayax79 Dec 27, 2022
e7abbf3
rewriting nushell implementation
ayax79 Nov 28, 2024
7eac0e1
Format with spaces instead of tabs
ayax79 Nov 30, 2024
8b4aa59
Added new directive to the list of directives for posterity.
ayax79 Nov 30, 2024
7006719
fixing formatting
ayax79 Nov 30, 2024
982ba40
fix tests
ayax79 Nov 30, 2024
59726e2
fixed a typo
ayax79 Nov 30, 2024
18d7a29
more documentation tweaks
ayax79 Nov 30, 2024
2f80e08
changed from being markdown, as I don't think the docs here are markdown
ayax79 Nov 30, 2024
1886f6b
minor formatting tweak
ayax79 Dec 2, 2024
ddb3992
More documentation work for nushell
ayax79 Dec 2, 2024
470ca0a
Added a comment at the beginning to remain consistent with other shells.
ayax79 Dec 2, 2024
93a4133
tmpfile handling changes
ayax79 Dec 2, 2024
e0ac28f
fix fish reference
ayax79 Dec 9, 2024
14e642e
fixed bad grammar mistake
ayax79 Dec 9, 2024
d728bbb
Nushell instructions tweak
ayax79 Dec 9, 2024
52185be
tweaking extra completion instructions
ayax79 Dec 9, 2024
1df1dbd
cobra logging command
ayax79 Dec 9, 2024
da1bab9
Fixed the ShellCompDirectiveNoFileComp case
ayax79 Dec 12, 2024
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
35 changes: 32 additions & 3 deletions completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -836,14 +836,43 @@ 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 completer to at the end of the file.
ayax79 marked this conversation as resolved.
Show resolved Hide resolved
ayax79 marked this conversation as resolved.
Show resolved Hide resolved
# 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
}

NOTE: This completer will work for all cobra based commands.
More information can be found in the External Completions (https://www.nushell.sh/book/custom_completions.html#custom-descriptions) section of the Nushell book.
ayax79 marked this conversation as resolved.
Show resolved Hide resolved
ayax79 marked this conversation as resolved.
Show resolved Hide resolved
Information on setting up more than one external completer can be found in the Multiple completer (https://www.nushell.sh/cookbook/external_completers.html#multiple-completer) section of the Nushell cookbook.
`),
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 {
Expand Down Expand Up @@ -876,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)
ayax79 marked this conversation as resolved.
Show resolved Hide resolved
if err == nil {
defer f.Close()
WriteStringAndCheck(f, msg)
Expand Down
1 change: 1 addition & 0 deletions completions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2577,6 +2577,7 @@ func TestCompleteCompletion(t *testing.T) {
expected := strings.Join([]string{
"bash",
"fish",
"nushell",
ayax79 marked this conversation as resolved.
Show resolved Hide resolved
"powershell",
"zsh",
":4",
Expand Down
128 changes: 128 additions & 0 deletions nushell_completions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The includeDesc variable is not used.

What you need to do is simple, if includeDesc == false you want to use __completeNoDesc in the script, instead of __complete.
If we have a single cobra_completer this won't really work as it will affect every program.
Let's see where we end up with having different completers and revisit at that time.

Copy link
Author

Choose a reason for hiding this comment

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

We could allow this to be set via an environment variable? Maybe something like this:

$env.COBRA_NO_DESC_COMMANDS = [kubectl minikube]

buf := new(bytes.Buffer)
WriteStringAndCheck(buf, "# nushell completion -*- shell-script -*- \n")
WriteStringAndCheck(buf, fmt.Sprintf(`
ayax79 marked this conversation as resolved.
Show resolved Hide resolved
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 exec_complete [
spans: list<string>
] {
# 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
ayax79 marked this conversation as resolved.
Show resolved Hide resolved
$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
}

if ($completions | is-empty) {
null
} else {
$completions
}
} else {
null
}
}
`, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp,
ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, ShellCompDirectiveKeepOrder))

_, 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)
}
98 changes: 98 additions & 0 deletions nushell_completions_test.go
Original file line number Diff line number Diff line change
@@ -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, true))
}

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(), true))
}

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.GenFishCompletionFile(f.Name(), false)
ayax79 marked this conversation as resolved.
Show resolved Hide resolved
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, true))
output := buf.String()

// check that active help is being disabled
activeHelpVar := activeHelpGlobalEnvVar
check(t, output, fmt.Sprintf("%s=0", activeHelpVar))
}
ayax79 marked this conversation as resolved.
Show resolved Hide resolved
25 changes: 23 additions & 2 deletions site/content/completions/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The currently supported shells are:
- Zsh
- fish
- PowerShell
- Nushell
ayax79 marked this conversation as resolved.
Show resolved Hide resolved

Cobra will automatically provide your program with a fully functional `completion` command,
similarly to how it provides the `help` command.
Expand All @@ -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:

Expand Down Expand Up @@ -68,9 +69,27 @@ 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 completer to at the end of the file.
ayax79 marked this conversation as resolved.
Show resolved Hide resolved
# 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
}

NOTE: This completer will work for all cobra based commands.
More information can be found in the External Completions (https://www.nushell.sh/book/custom_completions.html#custom-descriptions) section of the Nushell book.
Information on setting up more than one external completer can be found in the Multiple completer (https://www.nushell.sh/cookbook/external_completers.html#multiple-completer) section of the Nushell cookbook.

`,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] {
Expand All @@ -82,6 +101,8 @@ PowerShell:
cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
case "nushell":
cmd.Root().GenNushellCompletion(os.Stdout, true)
}
},
}
Expand Down
3 changes: 3 additions & 0 deletions site/content/completions/nushell_completions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Generating Nushell Completions For Your cobra.Command

Please refer to [Shell Completions](_index.md#nushell-completions) for details.