From a65a44c79cabf2ce493fadea5a79fd460ca81db0 Mon Sep 17 00:00:00 2001 From: Nir Soffer Date: Wed, 16 Aug 2023 19:21:17 +0300 Subject: [PATCH] Support usage as plugin for tools like kubectl In this case the executable is `kubectl-plugin`, but we run it as: kubectl plugin And the help text should reflect the actual usage of the command. To create a plugin, add the cobra.CommandDisplayNameAnnotation: rootCmd := &cobra.Command{ Use: "plugin", Annotations: map[string]string{ cobra.CommandDisplayNameAnnotation: "kubectl plugin", } } Internally this change modifies CommandPath() for the root command to return the command display name instead of the command name. This is used for error messages, help text generation, and completions. CommandPath() is expected to have spaces and code using it already handle spaces (e.g replacing with _), so hopefully this does not break anything. Fixes: #2017 Signed-off-by: Nir Soffer --- command.go | 12 +++++++++--- command_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/command.go b/command.go index 01f7c6f1c5..d607293c8a 100644 --- a/command.go +++ b/command.go @@ -30,7 +30,10 @@ import ( flag "github.com/spf13/pflag" ) -const FlagSetByCobraAnnotation = "cobra_annotation_flag_set_by_cobra" +const ( + FlagSetByCobraAnnotation = "cobra_annotation_flag_set_by_cobra" + CommandDisplayNameAnnotation = "cobra_annotation_command_display_name" +) // FParseErrWhitelist configures Flag parse errors to be ignored type FParseErrWhitelist flag.ParseErrorsWhitelist @@ -99,7 +102,7 @@ type Command struct { Deprecated string // Annotations are key/value pairs that can be used by applications to identify or - // group commands. + // group commands or set special options. Annotations map[string]string // Version defines the version for this command. If this value is non-empty and the command does not @@ -1380,6 +1383,9 @@ func (c *Command) CommandPath() string { if c.HasParent() { return c.Parent().CommandPath() + " " + c.Name() } + if displayName, ok := c.Annotations[CommandDisplayNameAnnotation]; ok { + return displayName + } return c.Name() } @@ -1441,7 +1447,7 @@ func (c *Command) DebugFlags() { debugflags(c) } -// Name returns the command's name: the first word in the use line. +// Name returns CommandName or the first word in the use line. func (c *Command) Name() string { name := c.Use i := strings.Index(name, " ") diff --git a/command_test.go b/command_test.go index b0f5e860ee..b044e7c482 100644 --- a/command_test.go +++ b/command_test.go @@ -366,6 +366,36 @@ func TestAliasPrefixMatching(t *testing.T) { EnablePrefixMatching = defaultPrefixMatching } +// TestPlugin checks usage as plugin for another command such as kubectl. The +// executable is `kubectl-plugin`, but we run it as `kubectl plugin`. The help +// text should reflect the way we run the command. +func TestPlugin(t *testing.T) { + rootCmd := &Command{ + Use: "plugin", + Args: NoArgs, + Annotations: map[string]string{ + CommandDisplayNameAnnotation: "kubectl plugin", + }, + } + + subCmd := &Command{Use: "sub [flags]", Args: NoArgs, Run: emptyRun} + rootCmd.AddCommand(subCmd) + + rootHelp, err := executeCommand(rootCmd, "-h") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + checkStringContains(t, rootHelp, "kubectl plugin [command]") + + childHelp, err := executeCommand(rootCmd, "sub", "-h") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + checkStringContains(t, childHelp, "kubectl plugin sub [flags]") +} + // TestChildSameName checks the correct behaviour of cobra in cases, // when an application with name "foo" and with subcommand "foo" // is executed with args "foo foo".