Changed the nushell completion implementation to be a nushell external completer

This commit is contained in:
Jack Wright 2022-11-26 14:39:29 -08:00
parent 9f4bdb8eb8
commit e687d2e9e1
3 changed files with 58 additions and 146 deletions

View file

@ -16,109 +16,59 @@ package cobra
import (
"bytes"
"fmt"
"io"
"os"
"regexp"
"strings"
"github.com/spf13/pflag"
)
var carrageReturnRE = regexp.MustCompile(`\r?\n`)
func descriptionString(desc string) string {
// Remove any carriage returns, this will break the extern
desc = carrageReturnRE.ReplaceAllString(desc, " ")
// Lets keep the descriptions short-ish
if len(desc) > 100 {
desc = desc[0:97] + "..."
}
return desc
}
func GenNushellComp(c *Command, buf io.StringWriter, nameBuilder *strings.Builder, isRoot bool, includeDesc bool) {
processFlags := func(flags *pflag.FlagSet) {
flags.VisitAll(func(f *pflag.Flag) {
WriteStringAndCheck(buf, fmt.Sprintf("\t--%[1]s", f.Name))
if f.Shorthand != "" {
WriteStringAndCheck(buf, fmt.Sprintf("(-%[1]s)", f.Shorthand))
}
if includeDesc && f.Usage != "" {
desc := descriptionString(f.Usage)
WriteStringAndCheck(buf, fmt.Sprintf("\t# %[1]s", desc))
}
WriteStringAndCheck(buf, "\n")
})
}
cmdName := c.Name()
// commands after root name will be like "git pull"
if !isRoot {
nameBuilder.WriteString(" ")
}
nameBuilder.WriteString(cmdName)
// only create an extern block if there is something to put in it
if len(c.ValidArgs) > 0 || c.HasAvailableFlags() {
builderString := nameBuilder.String()
// ensure there is a space before any previous content
// otherwise it will break descriptions
WriteStringAndCheck(buf, "\n")
funcName := builderString
if !isRoot {
funcName = fmt.Sprintf("\"%[1]s\"", builderString)
}
if includeDesc && c.Short != "" {
desc := descriptionString(c.Short)
WriteStringAndCheck(buf, fmt.Sprintf("# %[1]s\n", desc))
}
WriteStringAndCheck(buf, fmt.Sprintf("export extern %[1]s [\n", funcName))
// valid args
for _, arg := range c.ValidArgs {
WriteStringAndCheck(buf, fmt.Sprintf("\t%[1]s?\n", arg))
}
processFlags(c.InheritedFlags())
processFlags(c.LocalFlags())
// End extern statement
WriteStringAndCheck(buf, "]\n")
}
// process sub commands
for _, child := range c.Commands() {
childBuilder := strings.Builder{}
childBuilder.WriteString(nameBuilder.String())
GenNushellComp(child, buf, &childBuilder, false, includeDesc)
}
}
func (c *Command) GenNushellCompletion(w io.Writer, includeDesc bool) error {
var nameBuilder strings.Builder
func (c *Command) GenNushellCompletion(w io.Writer) error {
buf := new(bytes.Buffer)
GenNushellComp(c, buf, &nameBuilder, true, includeDesc)
WriteStringAndCheck(buf, `
# An external configurator that works with any cobra based
# command line application (e.g. kubectl, minikube)
let cobra_configurator = {|spans|
let cmd = $spans.0
# 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 ($spans | last | str trim | is-empty) {
$'($cmd_args) ""'
} else {
$cmd_args
}
# The full command to be executed
let full_cmd = $'($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 $completions = ($stdout_lines | drop | parse -r '([\w\-\.:\+]*)\t?(.*)' | rename value description)
let result = ({
completions: $completions
directive_str: ($result.stderr)
directive: ($stdout_lines | last)
})
$result.completions
}`)
_, err := buf.WriteTo(w)
return err
}
func (c *Command) GenNushellCompletionFile(filename string, includeDesc bool) error {
func (c *Command) GenNushellCompletionFile(filename string) error {
outFile, err := os.Create(filename)
if err != nil {
return err
}
defer outFile.Close()
return c.GenNushellCompletion(outFile, includeDesc)
return c.GenNushellCompletion(outFile)
}

View file

@ -22,13 +22,9 @@ import (
)
func TestGenNushellCompletion(t *testing.T) {
rootCmd := &Command{
Use: "kubectl",
Run: emptyRun,
}
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",
@ -36,58 +32,17 @@ func TestGenNushellCompletion(t *testing.T) {
ValidArgs: []string{"pod", "node", "service", "replicationcontroller"},
Run: emptyRun,
}
rootCmd.AddCommand(getCmd)
buf := new(bytes.Buffer)
assertNoErr(t, rootCmd.GenNushellCompletion(buf, true))
assertNoErr(t, rootCmd.GenNushellCompletion(buf))
output := buf.String()
// root command has no local options, it should not be displayed
checkOmit(t, output, "export extern kubectl")
check(t, output, "export extern \"kubectl get\"")
check(t, output, "--server")
check(t, output, "--skip-headers")
check(t, output, "pod?")
check(t, output, "node?")
check(t, output, "service?")
check(t, output, "replicationcontroller?")
check(t, output, "The address and port of the Kubernetes API serverIf true, avoid header prefixes in the log messages")
check(t, output, "The address and port of the Kubernetes API server")
check(t, output, "Display one or many resources")
}
func TestGenNushellCompletionWithoutDesc(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, false))
output := buf.String()
checkOmit(t, output, "The address and port of the Kubernetes API server")
checkOmit(t, output, "The address and port of the Kubernetes API serverIf true, avoid header prefixes in the log messages")
checkOmit(t, output, "Display one or many resources")
check(t, output, "let full_cmd = $'($cmd) __complete ($cmd_args)'")
}
func TestGenNushellCompletionFile(t *testing.T) {
err := os.Mkdir("./tmp", 0755)
err := os.Mkdir("./tmp", 0o755)
if err != nil {
log.Fatal(err.Error())
}
@ -102,18 +57,18 @@ func TestGenNushellCompletionFile(t *testing.T) {
}
rootCmd.AddCommand(child)
assertNoErr(t, rootCmd.GenNushellCompletionFile("./tmp/test", false))
assertNoErr(t, rootCmd.GenNushellCompletionFile("./tmp/test"))
}
func TestFailGenNushellCompletionFile(t *testing.T) {
err := os.Mkdir("./tmp", 0755)
err := os.Mkdir("./tmp", 0o755)
if err != nil {
log.Fatal(err.Error())
}
defer os.RemoveAll("./tmp")
f, _ := os.OpenFile("./tmp/test", os.O_CREATE, 0400)
f, _ := os.OpenFile("./tmp/test", os.O_CREATE, 0o400)
defer f.Close()
rootCmd := &Command{Use: "root", Args: NoArgs, Run: emptyRun}
@ -124,7 +79,7 @@ func TestFailGenNushellCompletionFile(t *testing.T) {
}
rootCmd.AddCommand(child)
got := rootCmd.GenNushellCompletionFile("./tmp/test", false)
got := rootCmd.GenNushellCompletionFile("./tmp/test")
if got == nil {
t.Error("should raise permission denied error")
}

View file

@ -72,11 +72,18 @@ PowerShell:
Nushell:
# To generate completions (replace YOUR_COMPLETION_DIR with actual path to save)
> %[1]s completion nushell | save /YOUR_COMPLETION_DIR/%[1]s-completions.nu
# 1. Copy the output of the command below:
> %[1]s completion nushell
# To load completions for each session, execute once (replace YOUR_COMPLETION_DIR with actual path):
> echo "use /YOUR_COMPLETION_DIR/%[1]s-completions.nu *" | save --append $nu.config-path
# 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 for this setup to take effect.
`,cmd.Root().Name()),
DisableFlagsInUseLine: true,