Skip to content

Commit

Permalink
Support usage as plugin for tools like kubectl
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
nirs committed Sep 8, 2023
1 parent fd865a4 commit a65a44c
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 3 deletions.
12 changes: 9 additions & 3 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -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, " ")
Expand Down
30 changes: 30 additions & 0 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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".
Expand Down

0 comments on commit a65a44c

Please sign in to comment.