spf13 / cobra

A Commander for modern Go CLI interactions
https://cobra.dev
Apache License 2.0
38.41k stars 2.86k forks source link

Detecting and handling commands with uses / names that conflict with existing commands #2185

Open andyfeller opened 2 months ago

andyfeller commented 2 months ago

Long time supporter, first time caller! 👋 Wanted to solicit maintainers' insights into how users avoid conflicting commands. I appreciate any guidance offered! ❤

Problem

As of v1.8.1, Command.AddCommand(cmds ...*Command) does not handle situations where a command added might conflict with the Use / Name of a previously added command, creating situations where commands can be overridden.

https://github.com/spf13/cobra/blob/e94f6d0dd9a5e5738dca6bce03c4b1207ffbc0ec/command.go#L1305-L1332

At first glance, it seems Command.Traverse(args []string) could be used to detect whether a command by name was previously added, however this searches too deeply.

https://github.com/spf13/cobra/blob/e94f6d0dd9a5e5738dca6bce03c4b1207ffbc0ec/command.go#L790-L831

This issue is aimed to understand how spf13/cobra expects multiple commands with overlap to behave, seeing if there are additional capabilities worth considering.

Background

The GitHub CLI has a static set of core commands we support while facilitating users being able to install and use extensions built by the community. These extensions are not specially namespaced, so it is possible for someone to create an extension as the same name of the core commandset to override them during execution.

https://github.com/cli/cli/blob/192f57ef429b4e89937c87dcbd330520730d4a3a/pkg/cmd/root/root.go#L172-L177

In this situation, a workaround would be iterating over the top-level commands looking for conflicting names:

    // Extensions
    em := f.ExtensionManager
    for _, e := range em.List() {
        var childCmd *cobra.Command
        for _, cc := range cmd.Commands() {
            if cc.Name() == e.Name() {
                childCmd = cc
                break
            }
        }
        if childCmd != nil {
            return nil, fmt.Errorf("cannot add extension %s as command by that name exists", e.Name())
        }
        extensionCmd := NewCmdExtension(io, em, e)
        cmd.AddCommand(extensionCmd)
    }
marckhouzam commented 2 months ago

Hi @andyfeller !

We can compare with kubectl’s approach. kubectl also allows users to define plugins and be named whatever they want. When starting, kubectl will generate it’s native commands first then it will attempt to create a cobra command for each plugin. However it first make sure that the name of the plug-in does not already exist as a native command when looking over each plugin it found.

If there’s a conflict, you can print an error message. kubectl also has a native plugin command, which will report a list of all conflicts and of valid plugins.

Helm on the other hand has a richer plug-in support which can block plug-ins from being installed if the name conflicts.

andyfeller commented 2 months ago

Thank you for taking the time and offering guidance, @marckhouzam! ❤ Let's see what kubectl and helm are doing to see how they might relate; feel free to correct any misinterpretations in my assessments below. 🙇

Digging into kubernetes/kubectl

In kubectl case, are core commands protected from plugins because they are in their own group? 🤔

Digging into helm/helm

In helm case, I think they have a similar issue as gh. 🤔