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 a1431b2c57
commit 594faef23f
3 changed files with 58 additions and 146 deletions

View file

@ -16,109 +16,59 @@ package cobra
import ( import (
"bytes" "bytes"
"fmt"
"io" "io"
"os" "os"
"regexp"
"strings"
"github.com/spf13/pflag"
) )
var carrageReturnRE = regexp.MustCompile(`\r?\n`) func (c *Command) GenNushellCompletion(w io.Writer) error {
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
buf := new(bytes.Buffer) 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) _, err := buf.WriteTo(w)
return err return err
} }
func (c *Command) GenNushellCompletionFile(filename string, includeDesc bool) error { func (c *Command) GenNushellCompletionFile(filename string) error {
outFile, err := os.Create(filename) outFile, err := os.Create(filename)
if err != nil { if err != nil {
return err return err
} }
defer outFile.Close() defer outFile.Close()
return c.GenNushellCompletion(outFile, includeDesc) return c.GenNushellCompletion(outFile)
} }

View file

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

View file

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