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: spf13#2017
Signed-off-by: Nir Soffer <nsoffer@redhat.com>
  • Loading branch information
nirs committed Sep 8, 2023
1 parent 0c72800 commit c35d881
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 2 deletions.
10 changes: 8 additions & 2 deletions command.go
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 @@ -1402,6 +1405,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
30 changes: 30 additions & 0 deletions command_test.go
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 c35d881

Please sign in to comment.