-
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
Pass arguments to the help function #2158
base: main
Are you sure you want to change the base?
Conversation
Signed-off-by: Marc Khouzam <marc.khouzam@gmail.com>
This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size. |
@@ -1119,7 +1122,13 @@ func (c *Command) ExecuteC() (cmd *Command, err error) { | |||
// Always show help if requested, even if SilenceErrors is in | |||
// effect | |||
if errors.Is(err, flag.ErrHelp) { | |||
cmd.HelpFunc()(cmd, args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of args
here was added in this commit and it does seem that it was a mistake; I believe the flags
variable should have been used instead.
command.go
Outdated
if cmd.DisableFlagParsing { | ||
argWoFlags = flags | ||
} | ||
cmd.HelpFunc()(cmd, argWoFlags) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to discuss backwards-compatibility for this change.
Before this PR cobra would pass all the arguments on the command-line (which was incorrect).
With this PR cobra will pass only the arguments after removing the sub-command and flags, as it does when calling the *Run/RunE
functions.
It is possible that some programs worked around this bug and fixing it may affect them.
However, considering that before this PR the help function sometime received all the args (when using the --help
flag) but no args at all when called from the help
command, it makes it less likely that a workaround used the actual args
parameter.
So, I feel it is ok to make this change but if we do maybe we should include a note in the release notes.
@jpmcb how do you feel about this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sending only the remaining arguments is better when using -h. If we think this may break someone, we can do this only if a new flag is set like HelpGetRemainingArgs so users can opt-int to use the new behavior.
When using help command
I'm not sure arguments make sense. You use this to learn about a command before you use it. You use -h,--help to get help for a command while trying to use it. You expect that adding -h,--help anywhere in the current command will show the help and getting more specific help for the current argument is nice improvement. Personally I never use help command since -h is much easier to type.
-h, --help use case:
Trying to run a command:
$ foo bar --baz
error: ...
It did not work, add -h:
$ foo bar --baz -h
help on foo bar...
Use it correctly:
$ foo bar --baz missing-value
success
help use case
What is the foo bar command?
$ foo help bar
help on foo bar
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your use case description is correct. However, there is another (obscure) situation, which is actually the one I'm really trying to fix 😄.
The tanzu
CLI uses plugins to provide many of its sub-commands. So a user may do:
tanzu help cluster list
to get the help on the cluster list
command.
However, the tanzu
CLI only has cluster
as a sub-command and needs to call that "cluster" plugin to ask it for the help for the cluster list
command. Therefore, the arguments passed to the original tanzu help
command are required to know which sub-command of the plugin the user is asking about.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently, the tanzu
CLI code has to use os.Args
to get that info: https://github.com/vmware-tanzu/tanzu-cli/blob/6a11ce93cd4e811e213e8439e090e1d73a053fd3/pkg/cli/plugin_cmd.go#L192-L201
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for explaning this, so this is needed when a command loads its sub commands dynamically. The help commands may need to do the same dynamic loading to display the help.
if cmd == nil || e != nil { | ||
c.Printf("Unknown help topic %#q\n", args) | ||
CheckErr(c.Root().Usage()) | ||
} else { | ||
cmd.InitDefaultHelpFlag() // make possible 'help' flag to be shown | ||
cmd.InitDefaultVersionFlag() // make possible 'version' flag to be shown | ||
CheckErr(cmd.Help()) | ||
cmd.HelpFunc()(cmd, remainingArgs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't feel this has any backwards-compatibility implications since the args
value was always empty before this change for this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we are missing documentation explaning that Run, RunE, and HelpFunc accept the remaining arguments after parsing. Previously HelpFunc sometimes accepted no arguments and sometimes accepted the raw parsed arguments.
Can be here:
// SetHelpFunc sets help function. Can be defined by Application.
// The help function is called with the remaining arguments after parsing
// the flags. If flag parsing is disabled it is called with all arguments.
func (c *Command) SetHelpFunc(f func(*Command, []string)) {
c.helpFunc = f
}
command.go
Outdated
if cmd.DisableFlagParsing { | ||
argWoFlags = flags | ||
} | ||
cmd.HelpFunc()(cmd, argWoFlags) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sending only the remaining arguments is better when using -h. If we think this may break someone, we can do this only if a new flag is set like HelpGetRemainingArgs so users can opt-int to use the new behavior.
When using help command
I'm not sure arguments make sense. You use this to learn about a command before you use it. You use -h,--help to get help for a command while trying to use it. You expect that adding -h,--help anywhere in the current command will show the help and getting more specific help for the current argument is nice improvement. Personally I never use help command since -h is much easier to type.
-h, --help use case:
Trying to run a command:
$ foo bar --baz
error: ...
It did not work, add -h:
$ foo bar --baz -h
help on foo bar...
Use it correctly:
$ foo bar --baz missing-value
success
help use case
What is the foo bar command?
$ foo help bar
help on foo bar
command_test.go
Outdated
if len(args) != 2 || args[0] != "arg1" || args[1] != "arg2" { | ||
t.Errorf("Expected args [args1 arg2], got %v", args) | ||
} | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We repeat the code of the help function many times, maybe add a helper struct keeping the , so we can do:
h := helper{t: t, command: "child", args: []string{"args1", "arg2"}}
rootCmd.SetHelpFunc(h.helpFunc)
_, err := executeCommand(rootCmd, "help", "child", "arg1", "arg2")
if !h.helpCalled {
...
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like it!
I tried to do what you suggest.
I think this change is not needed and we can keep the current behavior, and fix the
Why would someone type:
and then type work hard to remove the help command to run the actual command, instead of adding -h to the actual command:
and finally remove the -h for running the actual command? I guess tanszu smart help works both for help and -h/--help, so if we fix -h,--help, we can remove the workarounds? It we spend time on the help command, we should make it easy to show different content for "help command". For example try |
This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size. |
Signed-off-by: Marc Khouzam <marc.khouzam@gmail.com>
bec76b4
to
9fbba57
Compare
This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size. |
Answered here: #2158 (comment)
I agree that the
The same override of
This sounds like a good idea. Programs using Cobra can do this already by overriding the |
o.err = fmt.Errorf("Expected args %v, got %v", o.expectedArgs, args) | ||
return | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can extract lines 1098 to 1108 to a argsEqual() helper that can be useful in other tests, and can be replaced with slices.Equal when we can required Go 1.18. Can also be a good place to document why we cannot use slices.Equal.
Would be little bit nicer since since we don't have duplicate the error here:
if !argsEqual(args, o,expectedArgs) {
o.err = fmt.Errorf("Expected args %v, got %v", o.expectedArgs, args)
}
} | ||
|
||
func (o *overridingHelp) helpFunc() func(c *Command, args []string) { | ||
return func(c *Command, args []string) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason for returning a help function instead of implementing one?
This could be:
func (o *overridingHelp) helpFunc(c *Command, args []string) {
...
}
And used later as:
rootCmd.SetHelpFunc(override.helpFunc)
} | ||
rootCmd.SetHelpFunc(override.helpFunc()) | ||
|
||
_, err := executeCommand(rootCmd, "child", "arg1", "--myflag", "arg2", "--help") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why test additional flags separately? this way we don't cover all cases.
if err = override.checkError(); err != nil { | ||
t.Errorf("Unexpected error from help function: %v", err) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In both help command you don't test additional flags - if you want to have the same behavior for help command, would it be good to ensure that parsed flags are not passed to the help function?
// was not done and cmd.Flags().Args() is not filled. We therefore | ||
// use the full set of arguments, which include flags. | ||
remainingArgs = flags | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for explaining and adding the comment.
I think this will communicates better the intent of the code:
// The call to execute() above has parsed the flags.
// We therefore only pass the remaining arguments to the help function.
var remainingArgs []string
if cmd.DisableFlagParsing {
// For commands that have DisableFlagParsing == true, the flag parsing was
// not done, therefore use the full set of arguments, which include flags.
remainingArgs = flags
} else {
remainingArgs = cmd.Flags().Args()
}
command.go
Outdated
if cmd.DisableFlagParsing { | ||
argWoFlags = flags | ||
} | ||
cmd.HelpFunc()(cmd, argWoFlags) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for explaning this, so this is needed when a command loads its sub commands dynamically. The help commands may need to do the same dynamic loading to display the help.
if cmd == nil || e != nil { | ||
c.Printf("Unknown help topic %#q\n", args) | ||
CheckErr(c.Root().Usage()) | ||
} else { | ||
cmd.InitDefaultHelpFlag() // make possible 'help' flag to be shown | ||
cmd.InitDefaultVersionFlag() // make possible 'version' flag to be shown | ||
CheckErr(cmd.Help()) | ||
cmd.HelpFunc()(cmd, remainingArgs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we are missing documentation explaning that Run, RunE, and HelpFunc accept the remaining arguments after parsing. Previously HelpFunc sometimes accepted no arguments and sometimes accepted the raw parsed arguments.
Can be here:
// SetHelpFunc sets help function. Can be defined by Application.
// The help function is called with the remaining arguments after parsing
// the flags. If flag parsing is disabled it is called with all arguments.
func (c *Command) SetHelpFunc(f func(*Command, []string)) {
c.helpFunc = f
}
Fixes #2154
This PR makes sure that when Cobra calls
HelpFunc()(cmd, args)
, the proper arguments are passed to the function.With this, when the help function is overridden, the new function can choose to print the help output based on what the args are.
For example, say I write my own
ls
program, I could override the help function usingSetHelpFunc()
so thatls -h
would printList the content of the current directory
ls mydir -h
would instead printList the content of the "mydir" directory
This PR fixed the two cases where the help function is called:
--help/-h
flag (e.g.,prog sub arg1 -h
)help
command (e.g.,prog help sub arg1
)Another value of this fix is for programs that use plugins and want to ask the plugin what the help output should be.
For instance the
tanzu
CLI does this, where if I typetanzu help cluster list
the help function will call thecluster
plugin but needs to tell it that it needs the help for thelist
command. Thislist
argument needs this PR to get passed to the help function. One can see how thetanzu
code had to useos.Args
to work around this bug.Unit tests have been added which also illustrate the different cases that that PR fixes.
A test program is provided in #2154 to show the problem before this PR.
Please refer to a couple of comments I will add in the PR review for a concern about backwards-compatibility.