-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
subcommand handling is inconsistent #981
Comments
In addition to the MP fixnig some of the subcommands, I think we can get a new help fixing most of the issues above (suggesting and requiring subcommands): // subcommandsRequiredWithSuggestions will ensure we have a subcommand provided by the user and augments it with
// suggestion for commands, alias and help on root command.
func subcommandsRequiredWithSuggestions(cmd *cobra.Command, args []string) error {
requireMsg := "%s requires a valid subcommand"
// This will be triggered if cobra didn't find any subcommands.
// Find some suggestions.
var suggestions []string
if len(args) != 0 && !cmd.DisableSuggestions {
typedName := args[0]
if cmd.SuggestionsMinimumDistance <= 0 {
cmd.SuggestionsMinimumDistance = 2
}
// subcommand suggestions
suggestions = append(cmd.SuggestionsFor(args[0]))
// subcommand alias suggestions (with distance, not exact)
for _, c := range cmd.Commands() {
if c.IsAvailableCommand() {
for _, alias := range c.Aliases {
levenshteinDistance := ld(typedName, alias, true)
suggestByLevenshtein := levenshteinDistance <= cmd.SuggestionsMinimumDistance
suggestByPrefix := strings.HasPrefix(strings.ToLower(alias), strings.ToLower(typedName))
if suggestByLevenshtein || suggestByPrefix {
suggestions = append(suggestions, alias)
}
}
}
}
// help for root command
if !cmd.HasParent() {
help := "help"
levenshteinDistance := ld(typedName, help, true)
suggestByLevenshtein := levenshteinDistance <= cmd.SuggestionsMinimumDistance
suggestByPrefix := strings.HasPrefix(strings.ToLower(help), strings.ToLower(typedName))
if suggestByLevenshtein || suggestByPrefix {
suggestions = append(suggestions, help)
}
}
}
var suggestionsMsg string
if len(suggestions) > 0 {
suggestionsMsg += "Did you mean this?\n"
for _, s := range suggestions {
suggestionsMsg += fmt.Sprintf("\t%v\n", s)
}
}
if suggestionsMsg != "" {
requireMsg = fmt.Sprintf("%s. %s", requireMsg, suggestionsMsg)
}
return fmt.Errorf(requireMsg, cmd.Name())
} I think some of the code could be extracted/factorized directly in cobra's SuggestFor(), and give an helper for require/not requiring subcommands. The only parts not fixed are "Changing usage" and "No suggestion for flags" by the code above. Integrating something around this in cobra directly will enable to prevent code and string duplication and not having to copy the "ld()" function directly. I'm happy to work on another MP for this is that makes sense. Here is the complete example: package main
import (
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
)
var (
rootCmd = &cobra.Command{
Use: "foo COMMAND",
SilenceErrors: true,
Args: subcommandsRequiredWithSuggestions,
Run: func(cmd *cobra.Command, args []string) {},
}
subCmd1 = &cobra.Command{
Use: "sub1 COMMAND",
Aliases: []string{"alias1"},
Args: subcommandsRequiredWithSuggestions,
Run: func(cmd *cobra.Command, args []string) {},
}
subCmdA = &cobra.Command{
Use: "subsubA",
Run: func(cmd *cobra.Command, args []string) {},
}
subCmdB = &cobra.Command{
Use: "subsubB",
Run: func(cmd *cobra.Command, args []string) {},
}
subCmd2 = &cobra.Command{
Use: "sub2",
Run: func(cmd *cobra.Command, args []string) {},
}
subCmd3 = &cobra.Command{
Use: "sub3",
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {},
Args: subcommandsRequiredWithSuggestions,
}
subCmdC = &cobra.Command{
Use: "subsubC",
Run: func(cmd *cobra.Command, args []string) {},
}
)
func init() {
rootCmd.AddCommand(subCmd1, subCmd2)
rootCmd.AddCommand(subCmd3)
rootCmd.PersistentFlags().Bool("verbose", false, "")
rootCmd.Flags().Bool("baz", false, "")
subCmd1.AddCommand(subCmdA, subCmdB)
subCmd3.AddCommand(subCmdC)
subCmd3.Flags().Bool("bar", false, "")
subCmdC.Flags().Bool("other", false, "")
}
// subcommandsRequiredWithSuggestions will ensure we have a subcommand provided by the user and augments it with
// suggestion for commands, alias and help on root command.
func subcommandsRequiredWithSuggestions(cmd *cobra.Command, args []string) error {
requireMsg := "%s requires a valid subcommand"
// This will be triggered if cobra didn't find any subcommands.
// Find some suggestions.
var suggestions []string
if len(args) != 0 && !cmd.DisableSuggestions {
typedName := args[0]
if cmd.SuggestionsMinimumDistance <= 0 {
cmd.SuggestionsMinimumDistance = 2
}
// subcommand suggestions
suggestions = append(cmd.SuggestionsFor(args[0]))
// subcommand alias suggestions (with distance, not exact)
for _, c := range cmd.Commands() {
if c.IsAvailableCommand() {
for _, alias := range c.Aliases {
levenshteinDistance := ld(typedName, alias, true)
suggestByLevenshtein := levenshteinDistance <= cmd.SuggestionsMinimumDistance
suggestByPrefix := strings.HasPrefix(strings.ToLower(alias), strings.ToLower(typedName))
if suggestByLevenshtein || suggestByPrefix {
suggestions = append(suggestions, alias)
}
}
}
}
// help for root command
if !cmd.HasParent() {
help := "help"
levenshteinDistance := ld(typedName, help, true)
suggestByLevenshtein := levenshteinDistance <= cmd.SuggestionsMinimumDistance
suggestByPrefix := strings.HasPrefix(strings.ToLower(help), strings.ToLower(typedName))
if suggestByLevenshtein || suggestByPrefix {
suggestions = append(suggestions, help)
}
}
}
var suggestionsMsg string
if len(suggestions) > 0 {
suggestionsMsg += "Did you mean this?\n"
for _, s := range suggestions {
suggestionsMsg += fmt.Sprintf("\t%v\n", s)
}
}
if suggestionsMsg != "" {
requireMsg = fmt.Sprintf("%s. %s", requireMsg, suggestionsMsg)
}
return fmt.Errorf(requireMsg, cmd.Name())
}
func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
// ld compares two strings and returns the levenshtein distance between them.
func ld(s, t string, ignoreCase bool) int {
if ignoreCase {
s = strings.ToLower(s)
t = strings.ToLower(t)
}
d := make([][]int, len(s)+1)
for i := range d {
d[i] = make([]int, len(t)+1)
}
for i := range d {
d[i][0] = i
}
for j := range d[0] {
d[0][j] = j
}
for j := 1; j <= len(t); j++ {
for i := 1; i <= len(s); i++ {
if s[i-1] == t[j-1] {
d[i][j] = d[i-1][j-1]
} else {
min := d[i-1][j]
if d[i][j-1] < min {
min = d[i][j-1]
}
if d[i-1][j-1] < min {
min = d[i-1][j-1]
}
d[i][j] = min + 1
}
}
}
return d[len(s)][len(t)]
} |
We can now suggests "help", aliases and subcommands when the user type a command that doesn't match exactly any command. You can find more details reported upstream on why we needed to reimplement part of cobra's suggestion algorithm: spf13/cobra#981
We generate those using cobra handlers. Note that: * we needed to fork the bash completion mechanism to include our fixes for hidden commands and file completion to match github.com/didrocks/cobra branch-for-completion branch. The goal is to fix the completion issues addressed in spf13/cobra#983 and file completion if no match was found. A detailed upstream bug is available at spf13/cobra#981. * we post-process the markdown generation to skip SEE Also and other irrelevant sections and add subindentation.
Hi @didrocks and thanks for this very detailed issue! It seems that there's content for a bunch of PRs here... let's go step by step. I think that the base code you provided should be used for a set of tests. Then, each of the sections/issues that you commented would fit as a specific test. For now, we can only validate supported features, and fail speculatively (i.e. assuming that additional content with a specific format will be added). I'm the author of #842, which is based on #841. I think that #841 is ready to be merged, and it provides the foundation to implement proper detection of commands with/without flags/args/subcommands. Hence, I'd be glad if we could create the set of tests on top of #841. On the one hand, it would probably fix some of the issues related to From there on, we might see how much of |
This issue is being marked as stale due to a long period of inactivity |
Hi @umarcor , just wanted to know what's the status of this issue and when can we expect in cobra ? |
For completion, there was progress.
|
Hi @marckhouzam , thank you for the information. But I wanted to know the status of subcommands suggestions as in the current cobra version we don't get the subcommands suggestions. example:- Current situation :-
Expected:-
|
#841 is ready for merging since March 2019. Two users did already review it, and a maintainer was requested to do so in June 2019: #841 (comment). If/when #841 is merged (some day), the work in #842 may continue. |
@umarcor Thanks for the update 🙂 |
FWIW, it seems that setting |
Any updates on this? Found the following workaround: // https://github.com/containerd/nerdctl/blob/242e6fc6e861b61b878bd7df8bf25e95674c036d/cmd/nerdctl/main.go#L401-L418
func unknownSubcommandAction(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return cmd.Help()
}
err := fmt.Sprintf("unknown subcommand %q for %q", args[0], cmd.Name())
if suggestions := cmd.SuggestionsFor(args[0]); len(suggestions) > 0 {
err += "\n\nDid you mean this?\n"
for _, s := range suggestions {
err += fmt.Sprintf("\t%v\n", s)
}
}
return fmt.Errorf(err)
} And use: cmd := &cobra.Command{
Use: "test",
RunE: unknownSubcommandAction,
SilenceUsage: true,
SilenceErrors: true,
} |
This change enable to have a correct completion on hidden commands flags and its subcommands if present, when using bash. If an exact hidden command is typed on the command line and then the user request completion, instead of printing other available subcommand at the same level (which is thus incorrect), it will print any of its subcommand if available. Flags completion request (starting with -) will also work as expected, as well as flag completion request on any subcommand (persistent flags or local). Merge spf13/cobra#983 Fixes spf13/cobra#981
Related: #706. |
This is with cobra v0.0.5.
This is some kind of meta-bug (we can separate this if needed, but I think a general view can help getting a global schema) to gather some of where subcommand support in cobra is falling short. It has a little overlap with bug #870, pr #842, and as I saw the issue discussion that once 1.0 is out, there is some willingness to revisit the maintainance governance, I think this is the right moment to discuss this.
Here is a small example:
foo has 3 subcommands, one having sub subcommand and another one being hidden:
foo:
Flag --bar
The base code is:
Requiring subcommands
We don't want to be able to run
foo
orfoo sub1
without any arguments, but force providing a subcommand. We could specifyValidArgs
at this stage, but the error message is then confusing and doesn't mention subcommands. This is easily doable by a dedicated runE:With this:
Only sub1 and sub2 are shown, as expected, in addition to help.
Changing usage
Note though that the usage isn't correct:
So, we change this to:
Now, the usage is a little bit better, but still not accurate:
I couldn't find in cobra's code any exported function which enables removing the invalid "foo sub1 [command]" line.
I think in general a revisited way to tell "this command requires subcommands only" would help fixing the additional code and other issues.
Suggestions for command
Suggestions for subcommands works, just after the root command:
cobra only shows non hidden matching commands and not the alias, great.
Help (which is a valid subcommand of the rootCmd isn't suggested)
Alias are not suggested either
Similar, aliases are not suggested even if it should match:
Sub-subcommand's suggestion doesn't work
Subcommands of sub1 doesn't show up any suggestions:
We can see that:
legacyArgs()`` not being called as
commandFound.Args != nilfor subcommand (in
Find()). This is the only place where the internal
cmd.findSuggestions()is called (with valid args checking, more on that later). Even if
legacyArgs()would have been called, the check
!cmd.HasParent()` would prevent it to give any suggestions.I would prefer to not have to duplicate the strings and checks for the internal
findSuggestions()
(see the comment on the first bug report as well, where we have duplicated string).I found a way to mitigate this and the previous issues is to useArgs: cobra.OnlyValidArgs,
and expandValidArgs
dynamically.We now have subcommands suggestions:
However, the experience vastly different from the one printing on the root cmd:
-> this is way shorter (no usage printed), and we have "unkown command…" instead of "invalid argument".
If we add similarly the same kind of argument check on the root command, of course, the inconsistency is "fixed", but this is by using the less accurate version, the desired strings being in
legacyArgs()
.Note that this doesn't work though for aliases or "help" command, even if they are added and attached to the root command:
This returns:
And aliases don't work either:
This is due to
SuggestionsFor()
only checking subcommands (so discaring "help") and their names (discaring thusAliases
).Note that we could potentially override it from the program and attach a new function to each command, as it's exported, it will need to replace c.commands by c.Commands().
No suggestion for flags:
There is no suggestion for flags:
"--bar" could be suggested. This is what the initial bug reported I linked at was about.
completions once entered in a "hidden" command
Completions for subcommands are also generally puzzled by this.
Note that to not puzzle the result with hidden commands, I needed to remove
As other "help", "aliases" and hidden commands would show up (which is one of the limitation of the previous approach).
Via a call to
rootCmd.GenBashCompletionFile
and sourcing the resulting file, here is what we get:-> sub3 is hidden, as expected.
Aliases and help
However, Aliases, nor Help aren't completed
$ foo [tab]
completes to "foo sub".Hidden command completion
Worse, hidden commands, once typed, doesn't complete and provide invalid completions:
From a user perspective, I would expect not being completed by other commands, but by available flags on sub3 (as I typed the "hidden command" already).
Sorry for all this to be a little bit long to digest, but I wanted to put everything where I think cobra is falling short in term of subcommand handling vs suggestions and completions. I dont' like to open meta-bug, but I guess putting all those issues in perspective in a single place help if there is a desire to rework that part. Am I wrong on my approach and things can be mitigate in a more consistant way?
For reference, complete code
This is the one with some workarounds as previously stated (but doesn't fix all the issues as mentionned above)
In summary, the remaining issues are:
The text was updated successfully, but these errors were encountered: