diff --git a/nushell_completions.go b/nushell_completions.go new file mode 100644 index 0000000..6b944bd --- /dev/null +++ b/nushell_completions.go @@ -0,0 +1,124 @@ +// 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" + "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 + buf := new(bytes.Buffer) + GenNushellComp(c, buf, &nameBuilder, true, includeDesc) + + _, 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) +} diff --git a/nushell_completions.md b/nushell_completions.md new file mode 100644 index 0000000..e0e94eb --- /dev/null +++ b/nushell_completions.md @@ -0,0 +1,4 @@ +## Generating Nushell Completions For Your cobra.Command + +Please refer to [Shell Completions](shell_completions.md) for details. + diff --git a/nushell_completions_test.go b/nushell_completions_test.go new file mode 100644 index 0000000..77bb00d --- /dev/null +++ b/nushell_completions_test.go @@ -0,0 +1,141 @@ +// 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" + "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() + + // 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") +} + +func TestGenNushellCompletionFile(t *testing.T) { + err := os.Mkdir("./tmp", 0755) + 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", false)) +} + +func TestFailGenNushellCompletionFile(t *testing.T) { + err := os.Mkdir("./tmp", 0755) + if err != nil { + log.Fatal(err.Error()) + } + + defer os.RemoveAll("./tmp") + + f, _ := os.OpenFile("./tmp/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.GenNushellCompletionFile("./tmp/test", false) + 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") + } + } +} diff --git a/site/content/completions/_index.md b/site/content/completions/_index.md index 02257ad..9cbde8e 100644 --- a/site/content/completions/_index.md +++ b/site/content/completions/_index.md @@ -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. @@ -68,9 +69,18 @@ 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 generate completions (replace YOUR_COMPLETION_DIR with actual path to save) + > %[1]s completion nushell | save /YOUR_COMPLETION_DIR/%[1]s-completions.nu + + # 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 + `,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 +92,8 @@ PowerShell: cmd.Root().GenFishCompletion(os.Stdout, true) case "powershell": cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + case "nushell": + cmd.Root().GenNushellCompletion(os.Stdout, true) } }, }