diff --git a/command.go b/command.go index b31e22c86..0419cb150 100644 --- a/command.go +++ b/command.go @@ -33,6 +33,7 @@ import ( const ( FlagSetByCobraAnnotation = "cobra_annotation_flag_set_by_cobra" CommandDisplayNameAnnotation = "cobra_annotation_command_display_name" + trueString = "true" ) // FParseErrWhitelist configures Flag parse errors to be ignored @@ -1158,7 +1159,7 @@ func (c *Command) ValidateRequiredFlags() error { if !found { return } - if (requiredAnnotation[0] == "true") && !pflag.Changed { + if (requiredAnnotation[0] == trueString) && !pflag.Changed { missingFlagNames = append(missingFlagNames, pflag.Name) } }) @@ -1196,7 +1197,7 @@ func (c *Command) InitDefaultHelpFlag() { usage += name } c.Flags().BoolP("help", "h", false, usage) - _ = c.Flags().SetAnnotation("help", FlagSetByCobraAnnotation, []string{"true"}) + _ = c.Flags().SetAnnotation("help", FlagSetByCobraAnnotation, []string{trueString}) } } @@ -1222,7 +1223,7 @@ func (c *Command) InitDefaultVersionFlag() { } else { c.Flags().Bool("version", false, usage) } - _ = c.Flags().SetAnnotation("version", FlagSetByCobraAnnotation, []string{"true"}) + _ = c.Flags().SetAnnotation("version", FlagSetByCobraAnnotation, []string{trueString}) } } @@ -1259,6 +1260,23 @@ Simply type ` + c.displayName() + ` help [path to command] for full details.`, } return completions, ShellCompDirectiveNoFileComp }, + PersistentPreRunE: func(cmd *Command, args []string) error { + cmd.Flags().VisitAll(func(pflag *flag.Flag) { + requiredAnnotation, found := pflag.Annotations[BashCompOneRequiredFlag] + if found && requiredAnnotation[0] == trueString { + // Disable any persistent required flags for the help command + pflag.Annotations[BashCompOneRequiredFlag] = []string{"false"} + } + }) + // Adding PersistentPreRun on sub-commands prevents root's PersistentPreRun from being called. + // So it is intentionally called here. + if cmd.Root().PersistentPreRunE != nil { + return cmd.Root().PersistentPreRunE(cmd, args) + } else if cmd.Root().PersistentPreRun != nil { + cmd.Root().PersistentPreRun(cmd, args) + } + return nil + }, Run: func(c *Command, args []string) { cmd, _, e := c.Root().Find(args) if cmd == nil || e != nil { diff --git a/command_test.go b/command_test.go index 9ce7a529b..40a95664d 100644 --- a/command_test.go +++ b/command_test.go @@ -946,6 +946,18 @@ func TestHelpCommandExecuted(t *testing.T) { checkStringContains(t, output, rootCmd.Long) } +func TestHelpCommandExecutedWithPersistentRequiredFlags(t *testing.T) { + rootCmd := &Command{Use: "root", Run: emptyRun} + rootCmd.PersistentFlags().Bool("foo", false, "") + childCmd := &Command{Use: "child", Run: emptyRun} + rootCmd.AddCommand(childCmd) + assertNoErr(t, rootCmd.MarkPersistentFlagRequired("foo")) + + if _, err := executeCommand(rootCmd, "help"); err != nil { + t.Errorf("unexpected error: %v", err) + } +} + func TestHelpCommandExecutedOnChild(t *testing.T) { rootCmd := &Command{Use: "root", Run: emptyRun} childCmd := &Command{Use: "child", Long: "Long description", Run: emptyRun} @@ -1681,6 +1693,25 @@ func testPersistentHooks(t *testing.T, expectedHookRunOrder []string) { } } +func TestPersistentPreRunHooksForHelpCommand(t *testing.T) { + executed := false + + rootCmd := &Command{ + Use: "root", + PersistentPreRun: func(*Command, []string) { executed = true }, + Run: emptyRun, + } + childCmd := &Command{Use: "child", Run: emptyRun} + rootCmd.AddCommand(childCmd) + + if _, err := executeCommand(rootCmd, "help"); err != nil { + t.Errorf("Unexpected error: %v", err) + } + if !executed { + t.Error("Root PersistentPreRun should have been executed") + } +} + // Related to https://github.com/spf13/cobra/issues/521. func TestGlobalNormFuncPropagation(t *testing.T) { normFunc := func(f *pflag.FlagSet, name string) pflag.NormalizedName { diff --git a/completions.go b/completions.go index c0c08b057..d60d8c2f3 100644 --- a/completions.go +++ b/completions.go @@ -711,6 +711,23 @@ See each sub-command's help for details on how to use the generated script. ValidArgsFunction: NoFileCompletions, Hidden: c.CompletionOptions.HiddenDefaultCmd, GroupID: c.completionCommandGroupID, + PersistentPreRunE: func(cmd *Command, args []string) error { + cmd.Flags().VisitAll(func(flag *pflag.Flag) { + requiredAnnotation, found := flag.Annotations[BashCompOneRequiredFlag] + if found && requiredAnnotation[0] == "true" { + // Disable any persistent required flags for the completion command + flag.Annotations[BashCompOneRequiredFlag] = []string{"false"} + } + }) + // Adding PersistentPreRun on sub-commands prevents root's PersistentPreRun from being called. + // So it is intentionally called here. + if cmd.Root().PersistentPreRunE != nil { + return cmd.Root().PersistentPreRunE(cmd, args) + } else if cmd.Root().PersistentPreRun != nil { + cmd.Root().PersistentPreRun(cmd, args) + } + return nil + }, } c.AddCommand(completionCmd) diff --git a/completions_test.go b/completions_test.go index df153fcf2..28acb236c 100644 --- a/completions_test.go +++ b/completions_test.go @@ -2558,6 +2558,42 @@ func TestDefaultCompletionCmd(t *testing.T) { rootCmd.CompletionOptions.HiddenDefaultCmd = false // Remove completion command for the next test removeCompCmd(rootCmd) + + // Test that required flag will be ignored + rootCmd.PersistentFlags().Bool("foo", false, "") + assertNoErr(t, rootCmd.MarkPersistentFlagRequired("foo")) + for _, shell := range []string{"bash", "fish", "powershell", "zsh"} { + if _, err = executeCommand(rootCmd, compCmdName, shell); err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + // Remove completion command for the next test + removeCompCmd(rootCmd) +} + +func TestPersistentPreRunHooksForCompletionCommand(t *testing.T) { + executed := false + + rootCmd := &Command{ + Use: "root", + PersistentPreRun: func(*Command, []string) { executed = true }, + Run: emptyRun, + } + subCmd := &Command{ + Use: "sub", + Run: emptyRun, + } + rootCmd.AddCommand(subCmd) + + for _, shell := range []string{"bash", "fish", "powershell", "zsh"} { + if _, err := executeCommand(rootCmd, compCmdName, shell); err != nil { + t.Errorf("Unexpected error: %v", err) + } + if !executed { + t.Error("Root PersistentPreRun should have been executed") + } + executed = false + } } func TestCompleteCompletion(t *testing.T) {