Skip to content
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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -469,9 +469,10 @@ func (c *Command) HelpFunc() func(*Command, []string) {
}
}

// Help puts out the help for the command.
// Used when a user calls help [command].
// Can be defined by user by overriding HelpFunc.
// Help invokes the HelpFunc without arguments.
// Kept for backwards compatibility.
marckhouzam marked this conversation as resolved.
Show resolved Hide resolved
// Can be a simple way to trigger the help
// if arguments are not needed.
func (c *Command) Help() error {
c.HelpFunc()(c, []string{})
return nil
Expand Down Expand Up @@ -1119,7 +1120,16 @@ 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)
Copy link
Collaborator Author

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.

// The call to execute() above has parsed the flags.
// We therefore only pass the remaining arguments to the help function.
remainingArgs := cmd.Flags().Args()
if cmd.DisableFlagParsing {
// For commands that have DisableFlagParsing == true, the flag parsing
// was not done and cmd.Flags().Args() is not filled. We therefore
// use the full set of arguments, which include flags.
remainingArgs = flags
}
marckhouzam marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

@nirs nirs Jun 11, 2024

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()

}

cmd.HelpFunc()(cmd, remainingArgs)
return cmd, nil
}

Expand Down Expand Up @@ -1260,14 +1270,14 @@ Simply type ` + c.displayName() + ` help [path to command] for full details.`,
return completions, ShellCompDirectiveNoFileComp
},
Run: func(c *Command, args []string) {
cmd, _, e := c.Root().Find(args)
cmd, remainingArgs, e := c.Root().Find(args)
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)
Copy link
Collaborator Author

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.

Copy link
Contributor

@nirs nirs Jun 11, 2024

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
}

}
},
GroupID: c.helpCommandGroupID,
Expand Down
191 changes: 191 additions & 0 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,197 @@ func TestHelpExecutedOnNonRunnableChild(t *testing.T) {
checkStringContains(t, output, childCmd.Long)
}

type overridingHelp struct {
expectedCmdName string
expectedArgs []string

helpCalled bool
err error
}

func (o *overridingHelp) helpFunc() func(c *Command, args []string) {
return func(c *Command, args []string) {
Copy link
Contributor

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)

o.helpCalled = true
if c.Name() != o.expectedCmdName {
o.err = fmt.Errorf("Expected command name: %q, got %q", o.expectedCmdName, c.Name())
return
}

if len(args) != len(o.expectedArgs) {
o.err = fmt.Errorf("Expected args %v, got %v", o.expectedArgs, args)
return
}

for i, arg := range o.expectedArgs {
if args[i] != arg {
o.err = fmt.Errorf("Expected args %v, got %v", o.expectedArgs, args)
return
}
}
Copy link
Contributor

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) checkError() error {
if o.err != nil {
return o.err
}
if !o.helpCalled {
return fmt.Errorf("Overridden help function not called")
}
return nil
}

func TestHelpOverrideOnRoot(t *testing.T) {
rootCmd := &Command{Use: "root"}

override := overridingHelp{
expectedCmdName: rootCmd.Name(),
expectedArgs: []string{"arg1", "arg2"},
}
rootCmd.SetHelpFunc(override.helpFunc())

_, err := executeCommand(rootCmd, "arg1", "arg2", "--help")
if err != nil {
t.Errorf("Unexpected error executing command: %v", err)
}

if err = override.checkError(); err != nil {
t.Errorf("Unexpected error from help function: %v", err)
}
}

func TestHelpOverrideOnChild(t *testing.T) {
rootCmd := &Command{Use: "root"}
subCmd := &Command{Use: "child"}
rootCmd.AddCommand(subCmd)

override := overridingHelp{
expectedCmdName: subCmd.Name(),
expectedArgs: []string{"arg1", "arg2"},
}
subCmd.SetHelpFunc(override.helpFunc())

_, err := executeCommand(rootCmd, "child", "arg1", "arg2", "--help")
if err != nil {
t.Errorf("Unexpected error executing command: %v", err)
}

if err = override.checkError(); err != nil {
t.Errorf("Unexpected error from help function: %v", err)
}
}

func TestHelpOverrideOnRootWithChild(t *testing.T) {
rootCmd := &Command{Use: "root"}
subCmd := &Command{Use: "child"}
rootCmd.AddCommand(subCmd)

override := overridingHelp{
expectedCmdName: subCmd.Name(),
expectedArgs: []string{"arg1", "arg2"},
}
rootCmd.SetHelpFunc(override.helpFunc())

_, err := executeCommand(rootCmd, "child", "arg1", "arg2", "--help")
if err != nil {
t.Errorf("Unexpected error executing command: %v", err)
}

if err = override.checkError(); err != nil {
t.Errorf("Unexpected error from help function: %v", err)
}
}

func TestHelpOverrideOnRootWithChildAndFlags(t *testing.T) {
rootCmd := &Command{Use: "root"}
subCmd := &Command{Use: "child"}
rootCmd.AddCommand(subCmd)

var myFlag bool
subCmd.Flags().BoolVar(&myFlag, "myflag", false, "")

override := overridingHelp{
expectedCmdName: subCmd.Name(),
expectedArgs: []string{"arg1", "arg2"},
}
rootCmd.SetHelpFunc(override.helpFunc())

_, err := executeCommand(rootCmd, "child", "arg1", "--myflag", "arg2", "--help")
Copy link
Contributor

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 != nil {
t.Errorf("Unexpected error executing command: %v", err)
}

if err = override.checkError(); err != nil {
t.Errorf("Unexpected error from help function: %v", err)
}
}

func TestHelpOverrideOnRootWithChildAndFlagsButParsingDisabled(t *testing.T) {
rootCmd := &Command{Use: "root"}
subCmd := &Command{Use: "child", DisableFlagParsing: true}
rootCmd.AddCommand(subCmd)

var myFlag bool
subCmd.Flags().BoolVar(&myFlag, "myflag", false, "")

override := overridingHelp{
expectedCmdName: subCmd.Name(),
expectedArgs: []string{"arg1", "--myflag", "arg2", "--help"},
}
rootCmd.SetHelpFunc(override.helpFunc())

_, err := executeCommand(rootCmd, "child", "arg1", "--myflag", "arg2", "--help")
if err != nil {
t.Errorf("Unexpected error executing command: %v", err)
}

if err = override.checkError(); err != nil {
t.Errorf("Unexpected error from help function: %v", err)
}
}

func TestHelpCommandOverrideOnChild(t *testing.T) {
rootCmd := &Command{Use: "root"}
subCmd := &Command{Use: "child"}
rootCmd.AddCommand(subCmd)

override := overridingHelp{
expectedCmdName: subCmd.Name(),
expectedArgs: []string{"arg1", "arg2"},
}
subCmd.SetHelpFunc(override.helpFunc())

_, err := executeCommand(rootCmd, "help", "child", "arg1", "arg2")
if err != nil {
t.Errorf("Unexpected error executing command: %v", err)
}

if err = override.checkError(); err != nil {
t.Errorf("Unexpected error from help function: %v", err)
}
}

func TestHelpCommandOverrideOnRootWithChild(t *testing.T) {
rootCmd := &Command{Use: "root"}
subCmd := &Command{Use: "child"}
rootCmd.AddCommand(subCmd)

override := overridingHelp{
expectedCmdName: subCmd.Name(),
expectedArgs: []string{"arg1", "arg2"},
}
rootCmd.SetHelpFunc(override.helpFunc())

_, err := executeCommand(rootCmd, "help", "child", "arg1", "arg2")
if err != nil {
t.Errorf("Unexpected error executing command: %v", err)
}

if err = override.checkError(); err != nil {
t.Errorf("Unexpected error from help function: %v", err)
}
}
Copy link
Contributor

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?


func TestVersionFlagExecuted(t *testing.T) {
rootCmd := &Command{Use: "root", Version: "1.0.0", Run: emptyRun}

Expand Down
Loading