From 594faef23fa9b46685a50570bfd15c2175d440f1 Mon Sep 17 00:00:00 2001 From: Jack Wright Date: Sat, 26 Nov 2022 14:39:29 -0800 Subject: [PATCH] Changed the nushell completion implementation to be a nushell external completer --- nushell_completions.go | 128 +++++++++-------------------- nushell_completions_test.go | 61 ++------------ site/content/completions/_index.md | 15 +++- 3 files changed, 58 insertions(+), 146 deletions(-) diff --git a/nushell_completions.go b/nushell_completions.go index 6b944bd..4363946 100644 --- a/nushell_completions.go +++ b/nushell_completions.go @@ -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) } diff --git a/nushell_completions_test.go b/nushell_completions_test.go index 77bb00d..3e3c36d 100644 --- a/nushell_completions_test.go +++ b/nushell_completions_test.go @@ -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") } diff --git a/site/content/completions/_index.md b/site/content/completions/_index.md index 9cbde8e..b8a56d4 100644 --- a/site/content/completions/_index.md +++ b/site/content/completions/_index.md @@ -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,