From c81c46a015b48d9f79c73cc6f528f90eaabb0c7e Mon Sep 17 00:00:00 2001 From: Martijn Evers <94963229+marevers@users.noreply.github.com> Date: Sun, 16 Jul 2023 18:38:22 +0200 Subject: [PATCH 01/45] Add 'one required flag' group (#1952) --- completions_test.go | 98 ++++++++++++++++++++++++++++++++++++++ flag_groups.go | 68 +++++++++++++++++++++++++- flag_groups_test.go | 63 ++++++++++++++++++++---- site/content/user_guide.md | 11 ++++- 4 files changed, 228 insertions(+), 12 deletions(-) diff --git a/completions_test.go b/completions_test.go index 0588da0..0172466 100644 --- a/completions_test.go +++ b/completions_test.go @@ -2830,6 +2830,104 @@ func TestCompletionForGroupedFlags(t *testing.T) { } } +func TestCompletionForOneRequiredGroupFlags(t *testing.T) { + getCmd := func() *Command { + rootCmd := &Command{ + Use: "root", + Run: emptyRun, + } + childCmd := &Command{ + Use: "child", + ValidArgsFunction: func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + return []string{"subArg"}, ShellCompDirectiveNoFileComp + }, + Run: emptyRun, + } + rootCmd.AddCommand(childCmd) + + rootCmd.PersistentFlags().Int("ingroup1", -1, "ingroup1") + rootCmd.PersistentFlags().String("ingroup2", "", "ingroup2") + + childCmd.Flags().Bool("ingroup3", false, "ingroup3") + childCmd.Flags().Bool("nogroup", false, "nogroup") + + // Add flags to a group + childCmd.MarkFlagsOneRequired("ingroup1", "ingroup2", "ingroup3") + + return rootCmd + } + + // Each test case uses a unique command from the function above. + testcases := []struct { + desc string + args []string + expectedOutput string + }{ + { + desc: "flags in group suggested without - prefix", + args: []string{"child", ""}, + expectedOutput: strings.Join([]string{ + "--ingroup1", + "--ingroup2", + "--ingroup3", + "subArg", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n"), + }, + { + desc: "flags in group suggested with - prefix", + args: []string{"child", "-"}, + expectedOutput: strings.Join([]string{ + "--ingroup1", + "--ingroup2", + "--ingroup3", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n"), + }, + { + desc: "when any flag in group present, other flags in group not suggested without - prefix", + args: []string{"child", "--ingroup2", "value", ""}, + expectedOutput: strings.Join([]string{ + "subArg", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n"), + }, + { + desc: "when all flags in group present, flags not suggested without - prefix", + args: []string{"child", "--ingroup1", "8", "--ingroup2", "value2", "--ingroup3", ""}, + expectedOutput: strings.Join([]string{ + "subArg", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n"), + }, + { + desc: "group ignored if some flags not applicable", + args: []string{"--ingroup2", "value", ""}, + expectedOutput: strings.Join([]string{ + "child", + "completion", + "help", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n"), + }, + } + + for _, tc := range testcases { + t.Run(tc.desc, func(t *testing.T) { + c := getCmd() + args := []string{ShellCompNoDescRequestCmd} + args = append(args, tc.args...) + output, err := executeCommand(c, args...) + switch { + case err == nil && output != tc.expectedOutput: + t.Errorf("expected: %q, got: %q", tc.expectedOutput, output) + case err != nil: + t.Errorf("Unexpected error %q", err) + } + }) + } +} + func TestCompletionForMutuallyExclusiveFlags(t *testing.T) { getCmd := func() *Command { rootCmd := &Command{ diff --git a/flag_groups.go b/flag_groups.go index b35fde1..0671ec5 100644 --- a/flag_groups.go +++ b/flag_groups.go @@ -24,6 +24,7 @@ import ( const ( requiredAsGroup = "cobra_annotation_required_if_others_set" + oneRequired = "cobra_annotation_one_required" mutuallyExclusive = "cobra_annotation_mutually_exclusive" ) @@ -43,6 +44,22 @@ func (c *Command) MarkFlagsRequiredTogether(flagNames ...string) { } } +// MarkFlagsOneRequired marks the given flags with annotations so that Cobra errors +// if the command is invoked without at least one flag from the given set of flags. +func (c *Command) MarkFlagsOneRequired(flagNames ...string) { + c.mergePersistentFlags() + for _, v := range flagNames { + f := c.Flags().Lookup(v) + if f == nil { + panic(fmt.Sprintf("Failed to find flag %q and mark it as being in a one-required flag group", v)) + } + if err := c.Flags().SetAnnotation(v, oneRequired, append(f.Annotations[oneRequired], strings.Join(flagNames, " "))); err != nil { + // Only errs if the flag isn't found. + panic(err) + } + } +} + // MarkFlagsMutuallyExclusive marks the given flags with annotations so that Cobra errors // if the command is invoked with more than one flag from the given set of flags. func (c *Command) MarkFlagsMutuallyExclusive(flagNames ...string) { @@ -59,7 +76,7 @@ func (c *Command) MarkFlagsMutuallyExclusive(flagNames ...string) { } } -// ValidateFlagGroups validates the mutuallyExclusive/requiredAsGroup logic and returns the +// ValidateFlagGroups validates the mutuallyExclusive/oneRequired/requiredAsGroup logic and returns the // first error encountered. func (c *Command) ValidateFlagGroups() error { if c.DisableFlagParsing { @@ -71,15 +88,20 @@ func (c *Command) ValidateFlagGroups() error { // groupStatus format is the list of flags as a unique ID, // then a map of each flag name and whether it is set or not. groupStatus := map[string]map[string]bool{} + oneRequiredGroupStatus := map[string]map[string]bool{} mutuallyExclusiveGroupStatus := map[string]map[string]bool{} flags.VisitAll(func(pflag *flag.Flag) { processFlagForGroupAnnotation(flags, pflag, requiredAsGroup, groupStatus) + processFlagForGroupAnnotation(flags, pflag, oneRequired, oneRequiredGroupStatus) processFlagForGroupAnnotation(flags, pflag, mutuallyExclusive, mutuallyExclusiveGroupStatus) }) if err := validateRequiredFlagGroups(groupStatus); err != nil { return err } + if err := validateOneRequiredFlagGroups(oneRequiredGroupStatus); err != nil { + return err + } if err := validateExclusiveFlagGroups(mutuallyExclusiveGroupStatus); err != nil { return err } @@ -142,6 +164,27 @@ func validateRequiredFlagGroups(data map[string]map[string]bool) error { return nil } +func validateOneRequiredFlagGroups(data map[string]map[string]bool) error { + keys := sortedKeys(data) + for _, flagList := range keys { + flagnameAndStatus := data[flagList] + var set []string + for flagname, isSet := range flagnameAndStatus { + if isSet { + set = append(set, flagname) + } + } + if len(set) >= 1 { + continue + } + + // Sort values, so they can be tested/scripted against consistently. + sort.Strings(set) + return fmt.Errorf("at least one of the flags in the group [%v] is required", flagList) + } + return nil +} + func validateExclusiveFlagGroups(data map[string]map[string]bool) error { keys := sortedKeys(data) for _, flagList := range keys { @@ -176,6 +219,7 @@ func sortedKeys(m map[string]map[string]bool) []string { // enforceFlagGroupsForCompletion will do the following: // - when a flag in a group is present, other flags in the group will be marked required +// - when none of the flags in a one-required group are present, all flags in the group will be marked required // - when a flag in a mutually exclusive group is present, other flags in the group will be marked as hidden // This allows the standard completion logic to behave appropriately for flag groups func (c *Command) enforceFlagGroupsForCompletion() { @@ -185,9 +229,11 @@ func (c *Command) enforceFlagGroupsForCompletion() { flags := c.Flags() groupStatus := map[string]map[string]bool{} + oneRequiredGroupStatus := map[string]map[string]bool{} mutuallyExclusiveGroupStatus := map[string]map[string]bool{} c.Flags().VisitAll(func(pflag *flag.Flag) { processFlagForGroupAnnotation(flags, pflag, requiredAsGroup, groupStatus) + processFlagForGroupAnnotation(flags, pflag, oneRequired, oneRequiredGroupStatus) processFlagForGroupAnnotation(flags, pflag, mutuallyExclusive, mutuallyExclusiveGroupStatus) }) @@ -204,6 +250,26 @@ func (c *Command) enforceFlagGroupsForCompletion() { } } + // If none of the flags of a one-required group are present, we make all the flags + // of that group required so that the shell completion suggests them automatically + for flagList, flagnameAndStatus := range oneRequiredGroupStatus { + set := 0 + + for _, isSet := range flagnameAndStatus { + if isSet { + set++ + } + } + + // None of the flags of the group are set, mark all flags in the group + // as required + if set == 0 { + for _, fName := range strings.Split(flagList, " ") { + _ = c.MarkFlagRequired(fName) + } + } + } + // If a flag that is mutually exclusive to others is present, we hide the other // flags of that group so the shell completion does not suggest them for flagList, flagnameAndStatus := range mutuallyExclusiveGroupStatus { diff --git a/flag_groups_test.go b/flag_groups_test.go index bf988d7..cffa855 100644 --- a/flag_groups_test.go +++ b/flag_groups_test.go @@ -43,13 +43,15 @@ func TestValidateFlagGroups(t *testing.T) { // Each test case uses a unique command from the function above. testcases := []struct { - desc string - flagGroupsRequired []string - flagGroupsExclusive []string - subCmdFlagGroupsRequired []string - subCmdFlagGroupsExclusive []string - args []string - expectErr string + desc string + flagGroupsRequired []string + flagGroupsOneRequired []string + flagGroupsExclusive []string + subCmdFlagGroupsRequired []string + subCmdFlagGroupsOneRequired []string + subCmdFlagGroupsExclusive []string + args []string + expectErr string }{ { desc: "No flags no problem", @@ -62,6 +64,11 @@ func TestValidateFlagGroups(t *testing.T) { flagGroupsRequired: []string{"a b c"}, args: []string{"--a=foo"}, expectErr: "if any flags in the group [a b c] are set they must all be set; missing [b c]", + }, { + desc: "One-required flag group not satisfied", + flagGroupsOneRequired: []string{"a b"}, + args: []string{"--c=foo"}, + expectErr: "at least one of the flags in the group [a b] is required", }, { desc: "Exclusive flag group not satisfied", flagGroupsExclusive: []string{"a b c"}, @@ -72,6 +79,11 @@ func TestValidateFlagGroups(t *testing.T) { flagGroupsRequired: []string{"a b c", "a d"}, args: []string{"--c=foo", "--d=foo"}, expectErr: `if any flags in the group [a b c] are set they must all be set; missing [a b]`, + }, { + desc: "Multiple one-required flag group not satisfied returns first error", + flagGroupsOneRequired: []string{"a b", "d e"}, + args: []string{"--c=foo", "--f=foo"}, + expectErr: `at least one of the flags in the group [a b] is required`, }, { desc: "Multiple exclusive flag group not satisfied returns first error", flagGroupsExclusive: []string{"a b c", "a d"}, @@ -82,32 +94,57 @@ func TestValidateFlagGroups(t *testing.T) { flagGroupsRequired: []string{"a d", "a b", "a c"}, args: []string{"--a=foo"}, expectErr: `if any flags in the group [a b] are set they must all be set; missing [b]`, + }, { + desc: "Validation of one-required groups occurs on groups in sorted order", + flagGroupsOneRequired: []string{"d e", "a b", "f g"}, + args: []string{"--c=foo"}, + expectErr: `at least one of the flags in the group [a b] is required`, }, { desc: "Validation of exclusive groups occurs on groups in sorted order", flagGroupsExclusive: []string{"a d", "a b", "a c"}, args: []string{"--a=foo", "--b=foo", "--c=foo"}, expectErr: `if any flags in the group [a b] are set none of the others can be; [a b] were all set`, }, { - desc: "Persistent flags utilize both features and can fail required groups", + desc: "Persistent flags utilize required and exclusive groups and can fail required groups", flagGroupsRequired: []string{"a e", "e f"}, flagGroupsExclusive: []string{"f g"}, args: []string{"--a=foo", "--f=foo", "--g=foo"}, expectErr: `if any flags in the group [a e] are set they must all be set; missing [e]`, }, { - desc: "Persistent flags utilize both features and can fail mutually exclusive groups", + desc: "Persistent flags utilize one-required and exclusive groups and can fail one-required groups", + flagGroupsOneRequired: []string{"a b", "e f"}, + flagGroupsExclusive: []string{"e f"}, + args: []string{"--e=foo"}, + expectErr: `at least one of the flags in the group [a b] is required`, + }, { + desc: "Persistent flags utilize required and exclusive groups and can fail mutually exclusive groups", flagGroupsRequired: []string{"a e", "e f"}, flagGroupsExclusive: []string{"f g"}, args: []string{"--a=foo", "--e=foo", "--f=foo", "--g=foo"}, expectErr: `if any flags in the group [f g] are set none of the others can be; [f g] were all set`, }, { - desc: "Persistent flags utilize both features and can pass", + desc: "Persistent flags utilize required and exclusive groups and can pass", flagGroupsRequired: []string{"a e", "e f"}, flagGroupsExclusive: []string{"f g"}, args: []string{"--a=foo", "--e=foo", "--f=foo"}, + }, { + desc: "Persistent flags utilize one-required and exclusive groups and can pass", + flagGroupsOneRequired: []string{"a e", "e f"}, + flagGroupsExclusive: []string{"f g"}, + args: []string{"--a=foo", "--e=foo", "--f=foo"}, }, { desc: "Subcmds can use required groups using inherited flags", subCmdFlagGroupsRequired: []string{"e subonly"}, args: []string{"subcmd", "--e=foo", "--subonly=foo"}, + }, { + desc: "Subcmds can use one-required groups using inherited flags", + subCmdFlagGroupsOneRequired: []string{"e subonly"}, + args: []string{"subcmd", "--e=foo", "--subonly=foo"}, + }, { + desc: "Subcmds can use one-required groups using inherited flags and fail one-required groups", + subCmdFlagGroupsOneRequired: []string{"e subonly"}, + args: []string{"subcmd"}, + expectErr: "at least one of the flags in the group [e subonly] is required", }, { desc: "Subcmds can use exclusive groups using inherited flags", subCmdFlagGroupsExclusive: []string{"e subonly"}, @@ -130,12 +167,18 @@ func TestValidateFlagGroups(t *testing.T) { for _, flagGroup := range tc.flagGroupsRequired { c.MarkFlagsRequiredTogether(strings.Split(flagGroup, " ")...) } + for _, flagGroup := range tc.flagGroupsOneRequired { + c.MarkFlagsOneRequired(strings.Split(flagGroup, " ")...) + } for _, flagGroup := range tc.flagGroupsExclusive { c.MarkFlagsMutuallyExclusive(strings.Split(flagGroup, " ")...) } for _, flagGroup := range tc.subCmdFlagGroupsRequired { sub.MarkFlagsRequiredTogether(strings.Split(flagGroup, " ")...) } + for _, flagGroup := range tc.subCmdFlagGroupsOneRequired { + sub.MarkFlagsOneRequired(strings.Split(flagGroup, " ")...) + } for _, flagGroup := range tc.subCmdFlagGroupsExclusive { sub.MarkFlagsMutuallyExclusive(strings.Split(flagGroup, " ")...) } diff --git a/site/content/user_guide.md b/site/content/user_guide.md index 0497143..56e8e44 100644 --- a/site/content/user_guide.md +++ b/site/content/user_guide.md @@ -349,7 +349,16 @@ rootCmd.Flags().BoolVar(&ofYaml, "yaml", false, "Output in YAML") rootCmd.MarkFlagsMutuallyExclusive("json", "yaml") ``` -In both of these cases: +If you want to require at least one flag from a group to be present, you can use `MarkFlagsOneRequired`. +This can be combined with `MarkFlagsMutuallyExclusive` to enforce exactly one flag from a given group: +```go +rootCmd.Flags().BoolVar(&ofJson, "json", false, "Output in JSON") +rootCmd.Flags().BoolVar(&ofYaml, "yaml", false, "Output in YAML") +rootCmd.MarkFlagsOneRequired("json", "yaml") +rootCmd.MarkFlagsMutuallyExclusive("json", "yaml") +``` + +In these cases: - both local and persistent flags can be used - **NOTE:** the group is only enforced on commands where every flag is defined - a flag may appear in multiple groups From 66b215ba186e6e28ccab74b9022149cc2236d863 Mon Sep 17 00:00:00 2001 From: Unai Martinez-Corral <38422348+umarcor@users.noreply.github.com> Date: Tue, 18 Jul 2023 15:51:36 +0200 Subject: [PATCH 02/45] golangci: enable 'unused' and disable deprecated replaced by it (#1983) --- .golangci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 2578d94..a618ec2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,7 +19,7 @@ linters: disable-all: true enable: #- bodyclose - - deadcode + # - deadcode ! deprecated since v1.49.0; replaced by 'unused' #- depguard #- dogsled #- dupl @@ -51,12 +51,12 @@ linters: #- rowserrcheck #- scopelint #- staticcheck - - structcheck + #- structcheck ! deprecated since v1.49.0; replaced by 'unused' #- stylecheck #- typecheck - unconvert #- unparam - #- unused - - varcheck + - unused + # - varcheck ! deprecated since v1.49.0; replaced by 'unused' #- whitespace fast: false From 60d056d1574d04c859638354bc9cacab70bab296 Mon Sep 17 00:00:00 2001 From: gocurr Date: Thu, 20 Jul 2023 23:27:44 +0800 Subject: [PATCH 03/45] doc: fix typo, Deperecated -> Deprecated (#2000) --- doc/man_docs_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/man_docs_test.go b/doc/man_docs_test.go index c111d45..a4435e6 100644 --- a/doc/man_docs_test.go +++ b/doc/man_docs_test.go @@ -150,7 +150,7 @@ func TestGenManSeeAlso(t *testing.T) { } } -func TestManPrintFlagsHidesShortDeperecated(t *testing.T) { +func TestManPrintFlagsHidesShortDeprecated(t *testing.T) { c := &cobra.Command{} c.Flags().StringP("foo", "f", "default", "Foo flag") assertNoErr(t, c.Flags().MarkShorthandDeprecated("foo", "don't use it no more")) From fd865a44e3c48afeb6a6dbddadb8a5519173e029 Mon Sep 17 00:00:00 2001 From: Jun Nishimura Date: Sun, 23 Jul 2023 20:31:55 +0900 Subject: [PATCH 04/45] minor corrections to unit tests (#2003) --- command_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/command_test.go b/command_test.go index 0212f5a..b0f5e86 100644 --- a/command_test.go +++ b/command_test.go @@ -438,7 +438,7 @@ func TestFlagLong(t *testing.T) { output, err := executeCommand(c, "--intf=7", "--sf=abc", "one", "--", "two") if output != "" { - t.Errorf("Unexpected output: %v", err) + t.Errorf("Unexpected output: %v", output) } if err != nil { t.Errorf("Unexpected error: %v", err) @@ -475,7 +475,7 @@ func TestFlagShort(t *testing.T) { output, err := executeCommand(c, "-i", "7", "-sabc", "one", "two") if output != "" { - t.Errorf("Unexpected output: %v", err) + t.Errorf("Unexpected output: %v", output) } if err != nil { t.Errorf("Unexpected error: %v", err) @@ -504,7 +504,7 @@ func TestChildFlag(t *testing.T) { output, err := executeCommand(rootCmd, "child", "-i7") if output != "" { - t.Errorf("Unexpected output: %v", err) + t.Errorf("Unexpected output: %v", output) } if err != nil { t.Errorf("Unexpected error: %v", err) From 4955da7c11ea14678cbca3a4016bd4b0b1c51f39 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Aug 2023 18:53:34 +0000 Subject: [PATCH 05/45] build(deps): bump golangci/golangci-lint-action from 3.6.0 to 3.7.0 (#2021) --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 498e3be..3a365c8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,7 +49,7 @@ jobs: - uses: actions/checkout@v3 - - uses: golangci/golangci-lint-action@v3.6.0 + - uses: golangci/golangci-lint-action@v3.7.0 with: version: latest args: --verbose From 285460dca6152bb86994fa4a9659c24ca4060e2f Mon Sep 17 00:00:00 2001 From: Unai Martinez-Corral <38422348+umarcor@users.noreply.github.com> Date: Tue, 29 Aug 2023 16:04:59 +0200 Subject: [PATCH 06/45] command: temporarily disable G602 due to securego/gosec#1005 (#2022) --- command.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/command.go b/command.go index 01f7c6f..e4945ab 100644 --- a/command.go +++ b/command.go @@ -752,7 +752,9 @@ func (c *Command) findNext(next string) *Command { } if len(matches) == 1 { - return matches[0] + // Temporarily disable gosec G602, which produces a false positive. + // See https://github.com/securego/gosec/issues/1005. + return matches[0] // #nosec G602 } return nil From c5dacb3ea4512e6766f86907dfa7014ceb9cb041 Mon Sep 17 00:00:00 2001 From: Nuno Adrego <55922671+nunoadrego@users.noreply.github.com> Date: Fri, 8 Sep 2023 01:30:51 +0100 Subject: [PATCH 07/45] ci: test golang 1.21 (#2024) --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3a365c8..6b4c165 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,7 +43,7 @@ jobs: - uses: actions/setup-go@v3 with: - go-version: '^1.20' + go-version: '^1.21' check-latest: true cache: true @@ -67,6 +67,7 @@ jobs: - 18 - 19 - 20 + - 21 name: '${{ matrix.platform }} | 1.${{ matrix.go }}.x' runs-on: ${{ matrix.platform }}-latest steps: From 0c72800b8dba637092b57a955ecee75949e79a73 Mon Sep 17 00:00:00 2001 From: Souma <101255979+5ouma@users.noreply.github.com> Date: Sat, 9 Sep 2023 02:29:06 +0900 Subject: [PATCH 08/45] Customizable error message prefix (#2023) --- command.go | 24 ++++++++++++++++++++++-- command_test.go | 33 +++++++++++++++++++++++++++++++++ site/content/user_guide.md | 6 ++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/command.go b/command.go index e4945ab..6866f7d 100644 --- a/command.go +++ b/command.go @@ -181,6 +181,9 @@ type Command struct { // versionTemplate is the version template defined by user. versionTemplate string + // errPrefix is the error message prefix defined by user. + errPrefix string + // inReader is a reader defined by the user that replaces stdin inReader io.Reader // outWriter is a writer defined by the user that replaces stdout @@ -346,6 +349,11 @@ func (c *Command) SetVersionTemplate(s string) { c.versionTemplate = s } +// SetErrPrefix sets error message prefix to be used. Application can use it to set custom prefix. +func (c *Command) SetErrPrefix(s string) { + c.errPrefix = s +} + // SetGlobalNormalizationFunc sets a normalization function to all flag sets and also to child commands. // The user should not have a cyclic dependency on commands. func (c *Command) SetGlobalNormalizationFunc(n func(f *flag.FlagSet, name string) flag.NormalizedName) { @@ -595,6 +603,18 @@ func (c *Command) VersionTemplate() string { ` } +// ErrPrefix return error message prefix for the command +func (c *Command) ErrPrefix() string { + if c.errPrefix != "" { + return c.errPrefix + } + + if c.HasParent() { + return c.parent.ErrPrefix() + } + return "Error:" +} + func hasNoOptDefVal(name string, fs *flag.FlagSet) bool { flag := fs.Lookup(name) if flag == nil { @@ -1050,7 +1070,7 @@ func (c *Command) ExecuteC() (cmd *Command, err error) { c = cmd } if !c.SilenceErrors { - c.PrintErrln("Error:", err.Error()) + c.PrintErrln(c.ErrPrefix(), err.Error()) c.PrintErrf("Run '%v --help' for usage.\n", c.CommandPath()) } return c, err @@ -1079,7 +1099,7 @@ func (c *Command) ExecuteC() (cmd *Command, err error) { // If root command has SilenceErrors flagged, // all subcommands should respect it if !cmd.SilenceErrors && !c.SilenceErrors { - c.PrintErrln("Error:", err.Error()) + c.PrintErrln(cmd.ErrPrefix(), err.Error()) } // If root command has SilenceUsage flagged, diff --git a/command_test.go b/command_test.go index b0f5e86..4afb7f7 100644 --- a/command_test.go +++ b/command_test.go @@ -1099,6 +1099,39 @@ func TestShorthandVersionTemplate(t *testing.T) { checkStringContains(t, output, "customized version: 1.0.0") } +func TestRootErrPrefixExecutedOnSubcommand(t *testing.T) { + rootCmd := &Command{Use: "root", Run: emptyRun} + rootCmd.SetErrPrefix("root error prefix:") + rootCmd.AddCommand(&Command{Use: "sub", Run: emptyRun}) + + output, err := executeCommand(rootCmd, "sub", "--unknown-flag") + if err == nil { + t.Errorf("Expected error") + } + + checkStringContains(t, output, "root error prefix: unknown flag: --unknown-flag") +} + +func TestRootAndSubErrPrefix(t *testing.T) { + rootCmd := &Command{Use: "root", Run: emptyRun} + subCmd := &Command{Use: "sub", Run: emptyRun} + rootCmd.AddCommand(subCmd) + rootCmd.SetErrPrefix("root error prefix:") + subCmd.SetErrPrefix("sub error prefix:") + + if output, err := executeCommand(rootCmd, "--unknown-root-flag"); err == nil { + t.Errorf("Expected error") + } else { + checkStringContains(t, output, "root error prefix: unknown flag: --unknown-root-flag") + } + + if output, err := executeCommand(rootCmd, "sub", "--unknown-sub-flag"); err == nil { + t.Errorf("Expected error") + } else { + checkStringContains(t, output, "sub error prefix: unknown flag: --unknown-sub-flag") + } +} + func TestVersionFlagExecutedOnSubcommand(t *testing.T) { rootCmd := &Command{Use: "root", Version: "1.0.0"} rootCmd.AddCommand(&Command{Use: "sub", Run: emptyRun}) diff --git a/site/content/user_guide.md b/site/content/user_guide.md index 56e8e44..93daadf 100644 --- a/site/content/user_guide.md +++ b/site/content/user_guide.md @@ -596,6 +596,12 @@ Running an application with the '--version' flag will print the version to stdou the version template. The template can be customized using the `cmd.SetVersionTemplate(s string)` function. +## Error Message Prefix + +Cobra prints an error message when receiving a non-nil error value. +The default error message is `Error: `. +The Prefix, `Error:` can be customized using the `cmd.SetErrPrefix(s string)` function. + ## PreRun and PostRun Hooks It is possible to run functions before or after the main `Run` function of your command. The `PersistentPreRun` and `PreRun` functions will be executed before `Run`. `PersistentPostRun` and `PostRun` will be executed after `Run`. The `Persistent*Run` functions will be inherited by children if they do not declare their own. These functions are run in the following order: From bd4d1655f6655f690f45d281fe698dacb9c7d750 Mon Sep 17 00:00:00 2001 From: Alexandru-Claudius Virtopeanu Date: Tue, 26 Sep 2023 03:04:25 +0300 Subject: [PATCH 09/45] feat: add getters for flag completions (#1943) --- completions.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/completions.go b/completions.go index ee38c4d..55a9f62 100644 --- a/completions.go +++ b/completions.go @@ -145,6 +145,25 @@ func (c *Command) RegisterFlagCompletionFunc(flagName string, f func(cmd *Comman return nil } +// GetFlagCompletion returns the completion function for the given flag, if available. +func GetFlagCompletion(flag *pflag.Flag) (func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective), bool) { + flagCompletionMutex.RLock() + defer flagCompletionMutex.RUnlock() + + completionFunc, exists := flagCompletionFunctions[flag] + return completionFunc, exists +} + +// GetFlagCompletionByName returns the completion function for the given flag in the command by name, if available. +func (c *Command) GetFlagCompletionByName(flagName string) (func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective), bool) { + flag := c.Flags().Lookup(flagName) + if flag == nil { + return nil, false + } + + return GetFlagCompletion(flag) +} + // Returns a string listing the different directive enabled in the specified parameter func (d ShellCompDirective) string() string { var directives []string From 95d8a1e45d7719c56dc017e075d3e6099deba85d Mon Sep 17 00:00:00 2001 From: Haoming Meng <41393704+Techming@users.noreply.github.com> Date: Mon, 9 Oct 2023 07:50:40 -0500 Subject: [PATCH 10/45] Add notes to doc on preRun and postRun condition (#2041) --- command.go | 2 ++ site/content/user_guide.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/command.go b/command.go index 6866f7d..909d858 100644 --- a/command.go +++ b/command.go @@ -115,6 +115,8 @@ type Command struct { // * PostRun() // * PersistentPostRun() // All functions get the same args, the arguments after the command name. + // The *PreRun and *PostRun functions will only be executed if the Run function of the current + // command has been declared. // // PersistentPreRun: children of this command will inherit and execute. PersistentPreRun func(cmd *Command, args []string) diff --git a/site/content/user_guide.md b/site/content/user_guide.md index 93daadf..55cc252 100644 --- a/site/content/user_guide.md +++ b/site/content/user_guide.md @@ -604,7 +604,7 @@ The Prefix, `Error:` can be customized using the `cmd.SetErrPrefix(s string)` fu ## PreRun and PostRun Hooks -It is possible to run functions before or after the main `Run` function of your command. The `PersistentPreRun` and `PreRun` functions will be executed before `Run`. `PersistentPostRun` and `PostRun` will be executed after `Run`. The `Persistent*Run` functions will be inherited by children if they do not declare their own. These functions are run in the following order: +It is possible to run functions before or after the main `Run` function of your command. The `PersistentPreRun` and `PreRun` functions will be executed before `Run`. `PersistentPostRun` and `PostRun` will be executed after `Run`. The `Persistent*Run` functions will be inherited by children if they do not declare their own. The `*PreRun` and `*PostRun` functions will only be executed if the `Run` function of the current command has been declared. These functions are run in the following order: - `PersistentPreRun` - `PreRun` From efe8fa3e4453e41d6419b26c9769a51e42825632 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 15 Oct 2023 11:16:50 +0000 Subject: [PATCH 11/45] build(deps): bump actions/setup-go from 3 to 4 (#1934) --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6b4c165..55085ea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v4 with: go-version: '^1.21' check-latest: true @@ -74,7 +74,7 @@ jobs: - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v4 with: go-version: 1.${{ matrix.go }}.x cache: true From 5c962a221e70fd6b12296e5d7075f28b422f98b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 10:50:33 +0000 Subject: [PATCH 12/45] build(deps): bump github.com/cpuguy83/go-md2man/v2 from 2.0.2 to 2.0.3 (#2047) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6361d74..a79e66a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/spf13/cobra go 1.15 require ( - github.com/cpuguy83/go-md2man/v2 v2.0.2 + github.com/cpuguy83/go-md2man/v2 v2.0.3 github.com/inconshreveable/mousetrap v1.1.0 github.com/spf13/pflag v1.0.5 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 5ccb69d..871c3a8 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= From 4cafa37bc4bb85633b4245aa118280fe5a9edcd5 Mon Sep 17 00:00:00 2001 From: vkhoroz Date: Sun, 22 Oct 2023 03:36:12 +0300 Subject: [PATCH 13/45] Allow running persistent run hooks of all parents (#2044) Currently, only one of the persistent pre-runs and post-runs is executed. It is always the first one found in the parents chain, starting at this command. Expected behavior is to execute all parents' persistent pre-runs and post-runs. Dependent projects implemented various workarounds for this: - manually building persistent hook chains (in every hook). - applying some kind of monkey-patching on top of Cobra. This change eliminates the necessity for such workarounds by allowing to set a global variable EnableTraverseRunHooks. Tickets: - https://github.com/spf13/cobra/issues/216 - https://github.com/spf13/cobra/issues/252 Signed-off-by: Volodymyr Khoroz --- cobra.go | 11 ++-- command.go | 28 ++++++++-- command_test.go | 106 ++++++++++++++++--------------------- site/content/user_guide.md | 4 ++ 4 files changed, 83 insertions(+), 66 deletions(-) diff --git a/cobra.go b/cobra.go index f23f509..a6b160c 100644 --- a/cobra.go +++ b/cobra.go @@ -43,9 +43,10 @@ var initializers []func() var finalizers []func() const ( - defaultPrefixMatching = false - defaultCommandSorting = true - defaultCaseInsensitive = false + defaultPrefixMatching = false + defaultCommandSorting = true + defaultCaseInsensitive = false + defaultTraverseRunHooks = false ) // EnablePrefixMatching allows setting automatic prefix matching. Automatic prefix matching can be a dangerous thing @@ -60,6 +61,10 @@ var EnableCommandSorting = defaultCommandSorting // EnableCaseInsensitive allows case-insensitive commands names. (case sensitive by default) var EnableCaseInsensitive = defaultCaseInsensitive +// EnableTraverseRunHooks executes persistent pre-run and post-run hooks from all parents. +// By default this is disabled, which means only the first run hook to be found is executed. +var EnableTraverseRunHooks = defaultTraverseRunHooks + // MousetrapHelpText enables an information splash screen on Windows // if the CLI is started from explorer.exe. // To disable the mousetrap, just set this variable to blank string (""). diff --git a/command.go b/command.go index 909d858..ae3e4e0 100644 --- a/command.go +++ b/command.go @@ -934,15 +934,31 @@ func (c *Command) execute(a []string) (err error) { return err } + parents := make([]*Command, 0, 5) for p := c; p != nil; p = p.Parent() { + if EnableTraverseRunHooks { + // When EnableTraverseRunHooks is set: + // - Execute all persistent pre-runs from the root parent till this command. + // - Execute all persistent post-runs from this command till the root parent. + parents = append([]*Command{p}, parents...) + } else { + // Otherwise, execute only the first found persistent hook. + parents = append(parents, p) + } + } + for _, p := range parents { if p.PersistentPreRunE != nil { if err := p.PersistentPreRunE(c, argWoFlags); err != nil { return err } - break + if !EnableTraverseRunHooks { + break + } } else if p.PersistentPreRun != nil { p.PersistentPreRun(c, argWoFlags) - break + if !EnableTraverseRunHooks { + break + } } } if c.PreRunE != nil { @@ -979,10 +995,14 @@ func (c *Command) execute(a []string) (err error) { if err := p.PersistentPostRunE(c, argWoFlags); err != nil { return err } - break + if !EnableTraverseRunHooks { + break + } } else if p.PersistentPostRun != nil { p.PersistentPostRun(c, argWoFlags) - break + if !EnableTraverseRunHooks { + break + } } } diff --git a/command_test.go b/command_test.go index 4afb7f7..4d8908d 100644 --- a/command_test.go +++ b/command_test.go @@ -1530,57 +1530,73 @@ func TestHooks(t *testing.T) { } func TestPersistentHooks(t *testing.T) { - var ( - parentPersPreArgs string - parentPreArgs string - parentRunArgs string - parentPostArgs string - parentPersPostArgs string - ) + EnableTraverseRunHooks = true + testPersistentHooks(t, []string{ + "parent PersistentPreRun", + "child PersistentPreRun", + "child PreRun", + "child Run", + "child PostRun", + "child PersistentPostRun", + "parent PersistentPostRun", + }) - var ( - childPersPreArgs string - childPreArgs string - childRunArgs string - childPostArgs string - childPersPostArgs string - ) + EnableTraverseRunHooks = false + testPersistentHooks(t, []string{ + "child PersistentPreRun", + "child PreRun", + "child Run", + "child PostRun", + "child PersistentPostRun", + }) +} + +func testPersistentHooks(t *testing.T, expectedHookRunOrder []string) { + var hookRunOrder []string + + validateHook := func(args []string, hookName string) { + hookRunOrder = append(hookRunOrder, hookName) + got := strings.Join(args, " ") + if onetwo != got { + t.Errorf("Expected %s %q, got %q", hookName, onetwo, got) + } + } parentCmd := &Command{ Use: "parent", PersistentPreRun: func(_ *Command, args []string) { - parentPersPreArgs = strings.Join(args, " ") + validateHook(args, "parent PersistentPreRun") }, PreRun: func(_ *Command, args []string) { - parentPreArgs = strings.Join(args, " ") + validateHook(args, "parent PreRun") }, Run: func(_ *Command, args []string) { - parentRunArgs = strings.Join(args, " ") + validateHook(args, "parent Run") }, PostRun: func(_ *Command, args []string) { - parentPostArgs = strings.Join(args, " ") + validateHook(args, "parent PostRun") }, PersistentPostRun: func(_ *Command, args []string) { - parentPersPostArgs = strings.Join(args, " ") + validateHook(args, "parent PersistentPostRun") }, } childCmd := &Command{ Use: "child", PersistentPreRun: func(_ *Command, args []string) { - childPersPreArgs = strings.Join(args, " ") + validateHook(args, "child PersistentPreRun") }, PreRun: func(_ *Command, args []string) { - childPreArgs = strings.Join(args, " ") + validateHook(args, "child PreRun") }, Run: func(_ *Command, args []string) { - childRunArgs = strings.Join(args, " ") + validateHook(args, "child Run") }, PostRun: func(_ *Command, args []string) { - childPostArgs = strings.Join(args, " ") + validateHook(args, "child PostRun") }, PersistentPostRun: func(_ *Command, args []string) { - childPersPostArgs = strings.Join(args, " ") + validateHook(args, "child PersistentPostRun") }, } parentCmd.AddCommand(childCmd) @@ -1593,41 +1609,13 @@ func TestPersistentHooks(t *testing.T) { t.Errorf("Unexpected error: %v", err) } - for _, v := range []struct { - name string - got string - }{ - // TODO: currently PersistentPreRun* defined in parent does not - // run if the matching child subcommand has PersistentPreRun. - // If the behavior changes (https://github.com/spf13/cobra/issues/252) - // this test must be fixed. - {"parentPersPreArgs", parentPersPreArgs}, - {"parentPreArgs", parentPreArgs}, - {"parentRunArgs", parentRunArgs}, - {"parentPostArgs", parentPostArgs}, - // TODO: currently PersistentPostRun* defined in parent does not - // run if the matching child subcommand has PersistentPostRun. - // If the behavior changes (https://github.com/spf13/cobra/issues/252) - // this test must be fixed. - {"parentPersPostArgs", parentPersPostArgs}, - } { - if v.got != "" { - t.Errorf("Expected blank %s, got %q", v.name, v.got) - } - } - - for _, v := range []struct { - name string - got string - }{ - {"childPersPreArgs", childPersPreArgs}, - {"childPreArgs", childPreArgs}, - {"childRunArgs", childRunArgs}, - {"childPostArgs", childPostArgs}, - {"childPersPostArgs", childPersPostArgs}, - } { - if v.got != onetwo { - t.Errorf("Expected %s %q, got %q", v.name, onetwo, v.got) + for idx, exp := range expectedHookRunOrder { + if len(hookRunOrder) > idx { + if act := hookRunOrder[idx]; act != exp { + t.Errorf("Expected %q at %d, got %q", exp, idx, act) + } + } else { + t.Errorf("Expected %q at %d, got nothing", exp, idx) } } } diff --git a/site/content/user_guide.md b/site/content/user_guide.md index 55cc252..4116e8d 100644 --- a/site/content/user_guide.md +++ b/site/content/user_guide.md @@ -687,6 +687,10 @@ Inside subCmd PostRun with args: [arg1 arg2] Inside subCmd PersistentPostRun with args: [arg1 arg2] ``` +By default, only the first persistent hook found in the command chain is executed. +That is why in the above output, the `rootCmd PersistentPostRun` was not called for a child command. +Set `EnableTraverseRunHooks` global variable to `true` if you want to execute all parents' persistent hooks. + ## Suggestions when "unknown command" happens Cobra will print automatic suggestions when "unknown command" errors happen. This allows Cobra to behave similarly to the `git` command when a typo happens. For example: From 8b1eba47616566fc4d258a93da48d5d8741865f0 Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Fri, 27 Oct 2023 06:23:45 -0400 Subject: [PATCH 14/45] Fix linter errors (#2052) When using golangci-lint v1.55.0 some new errors were being reported. Signed-off-by: Marc Khouzam --- command.go | 1 + doc/md_docs.go | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/command.go b/command.go index ae3e4e0..36494df 100644 --- a/command.go +++ b/command.go @@ -1446,6 +1446,7 @@ func (c *Command) UseLine() string { // DebugFlags used to determine which flags have been assigned to which commands // and which persist. +// nolint:goconst func (c *Command) DebugFlags() { c.Println("DebugFlags called on", c.Name()) var debugflags func(*Command) diff --git a/doc/md_docs.go b/doc/md_docs.go index c4a27c0..f98fe2a 100644 --- a/doc/md_docs.go +++ b/doc/md_docs.go @@ -27,6 +27,8 @@ import ( "github.com/spf13/cobra" ) +const markdownExtension = ".md" + func printOptions(buf *bytes.Buffer, cmd *cobra.Command, name string) error { flags := cmd.NonInheritedFlags() flags.SetOutput(buf) @@ -83,7 +85,7 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) if cmd.HasParent() { parent := cmd.Parent() pname := parent.CommandPath() - link := pname + ".md" + link := pname + markdownExtension link = strings.ReplaceAll(link, " ", "_") buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", pname, linkHandler(link), parent.Short)) cmd.VisitParents(func(c *cobra.Command) { @@ -101,7 +103,7 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) continue } cname := name + " " + child.Name() - link := cname + ".md" + link := cname + markdownExtension link = strings.ReplaceAll(link, " ", "_") buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", cname, linkHandler(link), child.Short)) } @@ -138,7 +140,7 @@ func GenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHa } } - basename := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + ".md" + basename := strings.ReplaceAll(cmd.CommandPath(), " ", "_") + markdownExtension filename := filepath.Join(dir, basename) f, err := os.Create(filename) if err != nil { From b711e8760b73c6aa1b4aa1bef3a26da5926f175d Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Sat, 28 Oct 2023 16:10:06 -0400 Subject: [PATCH 15/45] Don't complete --help flag when flag parsing disabled (#2061) Fixes #2060 When a command sets `DisableFlagParsing = true` it requests the responsibility of doing all the flag parsing. Therefore even the `--help/-f/--version/-v` flags should not be automatically completed by Cobra in such a case. Without this change the `--help/-h/--version/-v` flags can end up being completed twice for plugins: one time from cobra and one time from the plugin (which has set `DisableFlagParsing = true`). Signed-off-by: Marc Khouzam --- completions.go | 15 ++++++++++++--- completions_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/completions.go b/completions.go index 55a9f62..368b92a 100644 --- a/completions.go +++ b/completions.go @@ -302,9 +302,13 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi // These flags are normally added when `execute()` is called on `finalCmd`, // however, when doing completion, we don't call `finalCmd.execute()`. - // Let's add the --help and --version flag ourselves. - finalCmd.InitDefaultHelpFlag() - finalCmd.InitDefaultVersionFlag() + // Let's add the --help and --version flag ourselves but only if the finalCmd + // has not disabled flag parsing; if flag parsing is disabled, it is up to the + // finalCmd itself to handle the completion of *all* flags. + if !finalCmd.DisableFlagParsing { + finalCmd.InitDefaultHelpFlag() + finalCmd.InitDefaultVersionFlag() + } // Check if we are doing flag value completion before parsing the flags. // This is important because if we are completing a flag value, we need to also @@ -408,6 +412,11 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) { doCompleteFlags(flag) }) + // Try to complete non-inherited flags even if DisableFlagParsing==true. + // This allows programs to tell Cobra about flags for completion even + // if the actual parsing of flags is not done by Cobra. + // For instance, Helm uses this to provide flag name completion for + // some of its plugins. finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) { doCompleteFlags(flag) }) diff --git a/completions_test.go b/completions_test.go index 0172466..4c6f41b 100644 --- a/completions_test.go +++ b/completions_test.go @@ -2622,8 +2622,6 @@ func TestCompleteWithDisableFlagParsing(t *testing.T) { expected := strings.Join([]string{ "--persistent", "-p", - "--help", - "-h", "--nonPersistent", "-n", "--flag", @@ -3053,8 +3051,26 @@ func TestCompletionCobraFlags(t *testing.T) { return []string{"extra3"}, ShellCompDirectiveNoFileComp }, } + childCmd4 := &Command{ + Use: "child4", + Version: "1.1.1", + Run: emptyRun, + ValidArgsFunction: func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + return []string{"extra4"}, ShellCompDirectiveNoFileComp + }, + DisableFlagParsing: true, + } + childCmd5 := &Command{ + Use: "child5", + Version: "1.1.1", + Run: emptyRun, + ValidArgsFunction: func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + return []string{"extra5"}, ShellCompDirectiveNoFileComp + }, + DisableFlagParsing: true, + } - rootCmd.AddCommand(childCmd, childCmd2, childCmd3) + rootCmd.AddCommand(childCmd, childCmd2, childCmd3, childCmd4, childCmd5) _ = childCmd.Flags().Bool("bool", false, "A bool flag") _ = childCmd.MarkFlagRequired("bool") @@ -3066,6 +3082,10 @@ func TestCompletionCobraFlags(t *testing.T) { // Have a command that only adds its own -v flag _ = childCmd3.Flags().BoolP("verbose", "v", false, "Not a version flag") + // Have a command that DisablesFlagParsing but that also adds its own help and version flags + _ = childCmd5.Flags().BoolP("help", "h", false, "My own help") + _ = childCmd5.Flags().BoolP("version", "v", false, "My own version") + return rootCmd } @@ -3196,6 +3216,26 @@ func TestCompletionCobraFlags(t *testing.T) { ":4", "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n"), }, + { + desc: "no completion for --help/-h and --version/-v flags when DisableFlagParsing=true", + args: []string{"child4", "-"}, + expectedOutput: strings.Join([]string{ + "extra4", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n"), + }, + { + desc: "completions for program-defined --help/-h and --version/-v flags even when DisableFlagParsing=true", + args: []string{"child5", "-"}, + expectedOutput: strings.Join([]string{ + "--help", + "-h", + "--version", + "-v", + "extra5", + ":4", + "Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n"), + }, } for _, tc := range testcases { From 00b68a1c260eaf2b9bcb10a3178d36cec81548ca Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Sat, 28 Oct 2023 16:11:59 -0400 Subject: [PATCH 16/45] Add tests for flag completion registration (#2053) Different problems have been reported about flag completion registration. These two tests are the cases that were not being verified but had been mentioned as problematic. Ref: - https://github.com/spf13/cobra/issues/1320 - https://github.com/spf13/cobra/pull/1438#issuecomment-872928669 Signed-off-by: Marc Khouzam --- completions_test.go | 110 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/completions_test.go b/completions_test.go index 4c6f41b..7585f88 100644 --- a/completions_test.go +++ b/completions_test.go @@ -17,7 +17,9 @@ package cobra import ( "bytes" "context" + "fmt" "strings" + "sync" "testing" ) @@ -2040,6 +2042,114 @@ func TestFlagCompletionWorksRootCommandAddedAfterFlags(t *testing.T) { } } +func TestFlagCompletionForPersistentFlagsCalledFromSubCmd(t *testing.T) { + rootCmd := &Command{Use: "root", Run: emptyRun} + rootCmd.PersistentFlags().String("string", "", "test string flag") + _ = rootCmd.RegisterFlagCompletionFunc("string", func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + return []string{"myval"}, ShellCompDirectiveDefault + }) + + childCmd := &Command{ + Use: "child", + Run: emptyRun, + ValidArgsFunction: func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + return []string{"--validarg", "test"}, ShellCompDirectiveDefault + }, + } + childCmd.Flags().Bool("bool", false, "test bool flag") + rootCmd.AddCommand(childCmd) + + // Test that persistent flag completion works for the subcmd + output, err := executeCommand(rootCmd, ShellCompRequestCmd, "child", "--string", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := strings.Join([]string{ + "myval", + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } +} + +// This test tries to register flag completion concurrently to make sure the +// code handles concurrency properly. +// This was reported as a problem when tests are run concurrently: +// https://github.com/spf13/cobra/issues/1320 +// +// NOTE: this test can sometimes pass even if the code were to not handle +// concurrency properly. This is not great but the important part is that +// it should never fail. Therefore, if the tests fails sometimes, we will +// still be able to know there is a problem. +func TestFlagCompletionConcurrentRegistration(t *testing.T) { + rootCmd := &Command{Use: "root", Run: emptyRun} + const maxFlags = 50 + for i := 1; i < maxFlags; i += 2 { + flagName := fmt.Sprintf("flag%d", i) + rootCmd.Flags().String(flagName, "", fmt.Sprintf("test %s flag on root", flagName)) + } + + childCmd := &Command{ + Use: "child", + Run: emptyRun, + } + for i := 2; i <= maxFlags; i += 2 { + flagName := fmt.Sprintf("flag%d", i) + childCmd.Flags().String(flagName, "", fmt.Sprintf("test %s flag on child", flagName)) + } + + rootCmd.AddCommand(childCmd) + + // Register completion in different threads to test concurrency. + var wg sync.WaitGroup + for i := 1; i <= maxFlags; i++ { + index := i + flagName := fmt.Sprintf("flag%d", i) + wg.Add(1) + go func() { + defer wg.Done() + cmd := rootCmd + if index%2 == 0 { + cmd = childCmd + } + _ = cmd.RegisterFlagCompletionFunc(flagName, func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + return []string{fmt.Sprintf("flag%d", index)}, ShellCompDirectiveDefault + }) + }() + } + + wg.Wait() + + // Test that flag completion works for each flag + for i := 1; i <= 6; i++ { + var output string + var err error + flagName := fmt.Sprintf("flag%d", i) + + if i%2 == 1 { + output, err = executeCommand(rootCmd, ShellCompRequestCmd, "--"+flagName, "") + } else { + output, err = executeCommand(rootCmd, ShellCompRequestCmd, "child", "--"+flagName, "") + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := strings.Join([]string{ + flagName, + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + } +} + func TestFlagCompletionInGoWithDesc(t *testing.T) { rootCmd := &Command{ Use: "root", From 22953d88453ec9343b4a78b9d67400a3326f3138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 29 Oct 2023 20:06:51 +0200 Subject: [PATCH 17/45] Replace all non-alphanumerics in active help env var program prefix (#1940) * Replace all non-alphanumerics in active help env var program prefix There are other characters besides the dash that are fine in program names, but are problematic in environment variable names. These include (but are not limited to) period, space, and non-ASCII letters. * Another change in docs to mention non-ASCII-alphanumeric instead of just dash --- active_help.go | 10 +++++++--- site/content/active_help.md | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/active_help.go b/active_help.go index 2d02394..5f965e0 100644 --- a/active_help.go +++ b/active_help.go @@ -17,6 +17,7 @@ package cobra import ( "fmt" "os" + "regexp" "strings" ) @@ -29,6 +30,8 @@ const ( activeHelpGlobalDisable = "0" ) +var activeHelpEnvVarPrefixSubstRegexp = regexp.MustCompile(`[^A-Z0-9_]`) + // AppendActiveHelp adds the specified string to the specified array to be used as ActiveHelp. // Such strings will be processed by the completion script and will be shown as ActiveHelp // to the user. @@ -42,7 +45,7 @@ func AppendActiveHelp(compArray []string, activeHelpStr string) []string { // GetActiveHelpConfig returns the value of the ActiveHelp environment variable // _ACTIVE_HELP where is the name of the root command in upper -// case, with all - replaced by _. +// case, with all non-ASCII-alphanumeric characters replaced by `_`. // It will always return "0" if the global environment variable COBRA_ACTIVE_HELP // is set to "0". func GetActiveHelpConfig(cmd *Command) string { @@ -55,9 +58,10 @@ func GetActiveHelpConfig(cmd *Command) string { // activeHelpEnvVar returns the name of the program-specific ActiveHelp environment // variable. It has the format _ACTIVE_HELP where is the name of the -// root command in upper case, with all - replaced by _. +// root command in upper case, with all non-ASCII-alphanumeric characters replaced by `_`. func activeHelpEnvVar(name string) string { // This format should not be changed: users will be using it explicitly. activeHelpEnvVar := strings.ToUpper(fmt.Sprintf("%s%s", name, activeHelpEnvVarSuffix)) - return strings.ReplaceAll(activeHelpEnvVar, "-", "_") + activeHelpEnvVar = activeHelpEnvVarPrefixSubstRegexp.ReplaceAllString(activeHelpEnvVar, "_") + return activeHelpEnvVar } diff --git a/site/content/active_help.md b/site/content/active_help.md index 5e7f59a..d72acc7 100644 --- a/site/content/active_help.md +++ b/site/content/active_help.md @@ -92,7 +92,7 @@ Allowing to configure Active Help is entirely optional; you can use Active Help The way to configure Active Help is to use the program's Active Help environment variable. That variable is named `_ACTIVE_HELP` where `` is the name of your -program in uppercase with any `-` replaced by an `_`. The variable should be set by the user to whatever +program in uppercase with any non-ASCII-alphanumeric characters replaced by an `_`. The variable should be set by the user to whatever Active Help configuration values are supported by the program. For example, say `helm` has chosen to support three levels for Active Help: `on`, `off`, `local`. Then a user @@ -140,7 +140,7 @@ details for your users. Debugging your Active Help code is done in the same way as debugging your dynamic completion code, which is with Cobra's hidden `__complete` command. Please refer to [debugging shell completion](shell_completions.md#debugging) for details. -When debugging with the `__complete` command, if you want to specify different Active Help configurations, you should use the active help environment variable. That variable is named `_ACTIVE_HELP` where any `-` is replaced by an `_`. For example, we can test deactivating some Active Help as shown below: +When debugging with the `__complete` command, if you want to specify different Active Help configurations, you should use the active help environment variable. That variable is named `_ACTIVE_HELP` where any non-ASCII-alphanumeric characters are replaced by an `_`. For example, we can test deactivating some Active Help as shown below: ``` $ HELM_ACTIVE_HELP=1 bin/helm __complete install wordpress bitnami/h bitnami/haproxy From 48cea5c87b5299b68c3f5b7f2c67ea948717276f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 10:21:48 +0000 Subject: [PATCH 18/45] build(deps): bump actions/checkout from 3 to 4 (#2028) --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55085ea..a924532 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: >- docker run @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: @@ -47,7 +47,7 @@ jobs: check-latest: true cache: true - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: golangci/golangci-lint-action@v3.7.0 with: @@ -72,7 +72,7 @@ jobs: runs-on: ${{ matrix.platform }}-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: @@ -108,7 +108,7 @@ jobs: unzip mingw-w64-x86_64-go - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/cache@v3 with: From 890302a35f578311404a462b3cdd404f34db3720 Mon Sep 17 00:00:00 2001 From: Nir Soffer Date: Thu, 2 Nov 2023 14:15:26 +0200 Subject: [PATCH 19/45] Support usage as plugin for tools like kubectl (#2018) In this case the executable is `kubectl-plugin`, but we run it as: kubectl plugin And the help text should reflect the actual usage of the command. To create a plugin, add the cobra.CommandDisplayNameAnnotation: rootCmd := &cobra.Command{ Use: "plugin", Annotations: map[string]string{ cobra.CommandDisplayNameAnnotation: "kubectl plugin", } } Internally this change modifies CommandPath() for the root command to return the command display name instead of the command name. This is used for error messages, help text generation, and completions. CommandPath() is expected to have spaces and code using it already handle spaces (e.g replacing with _), so hopefully this does not break anything. Fixes: #2017 Signed-off-by: Nir Soffer --- command.go | 10 ++++++++-- command_test.go | 30 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/command.go b/command.go index 36494df..2fbe6c1 100644 --- a/command.go +++ b/command.go @@ -30,7 +30,10 @@ import ( flag "github.com/spf13/pflag" ) -const FlagSetByCobraAnnotation = "cobra_annotation_flag_set_by_cobra" +const ( + FlagSetByCobraAnnotation = "cobra_annotation_flag_set_by_cobra" + CommandDisplayNameAnnotation = "cobra_annotation_command_display_name" +) // FParseErrWhitelist configures Flag parse errors to be ignored type FParseErrWhitelist flag.ParseErrorsWhitelist @@ -99,7 +102,7 @@ type Command struct { Deprecated string // Annotations are key/value pairs that can be used by applications to identify or - // group commands. + // group commands or set special options. Annotations map[string]string // Version defines the version for this command. If this value is non-empty and the command does not @@ -1424,6 +1427,9 @@ func (c *Command) CommandPath() string { if c.HasParent() { return c.Parent().CommandPath() + " " + c.Name() } + if displayName, ok := c.Annotations[CommandDisplayNameAnnotation]; ok { + return displayName + } return c.Name() } diff --git a/command_test.go b/command_test.go index 4d8908d..9f686d6 100644 --- a/command_test.go +++ b/command_test.go @@ -366,6 +366,36 @@ func TestAliasPrefixMatching(t *testing.T) { EnablePrefixMatching = defaultPrefixMatching } +// TestPlugin checks usage as plugin for another command such as kubectl. The +// executable is `kubectl-plugin`, but we run it as `kubectl plugin`. The help +// text should reflect the way we run the command. +func TestPlugin(t *testing.T) { + rootCmd := &Command{ + Use: "plugin", + Args: NoArgs, + Annotations: map[string]string{ + CommandDisplayNameAnnotation: "kubectl plugin", + }, + } + + subCmd := &Command{Use: "sub [flags]", Args: NoArgs, Run: emptyRun} + rootCmd.AddCommand(subCmd) + + rootHelp, err := executeCommand(rootCmd, "-h") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + checkStringContains(t, rootHelp, "kubectl plugin [command]") + + childHelp, err := executeCommand(rootCmd, "sub", "-h") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + checkStringContains(t, childHelp, "kubectl plugin sub [flags]") +} + // TestChildSameName checks the correct behaviour of cobra in cases, // when an application with name "foo" and with subcommand "foo" // is executed with args "foo foo". From a0a6ae020bb3899ff0276067863e50523f897370 Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Thu, 2 Nov 2023 11:23:08 -0400 Subject: [PATCH 20/45] Improve API to get flag completion function (#2063) The new API is simpler and matches the `c.RegisterFlagCompletionFunc()` API. By removing the global function `GetFlagCompletion()` we are more future proof if we ever move from a global map of flag completion functions to something associated with the command. The commit also makes this API work with persistent flags by using `c.Flag(flagName)` instead of `c.Flags().Lookup(flagName)`. The commit also adds unit tests. Signed-off-by: Marc Khouzam --- completions.go | 19 ++++------ completions_test.go | 90 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 12 deletions(-) diff --git a/completions.go b/completions.go index 368b92a..b60f6b2 100644 --- a/completions.go +++ b/completions.go @@ -145,8 +145,13 @@ func (c *Command) RegisterFlagCompletionFunc(flagName string, f func(cmd *Comman return nil } -// GetFlagCompletion returns the completion function for the given flag, if available. -func GetFlagCompletion(flag *pflag.Flag) (func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective), bool) { +// GetFlagCompletionFunc returns the completion function for the given flag of the command, if available. +func (c *Command) GetFlagCompletionFunc(flagName string) (func(*Command, []string, string) ([]string, ShellCompDirective), bool) { + flag := c.Flag(flagName) + if flag == nil { + return nil, false + } + flagCompletionMutex.RLock() defer flagCompletionMutex.RUnlock() @@ -154,16 +159,6 @@ func GetFlagCompletion(flag *pflag.Flag) (func(cmd *Command, args []string, toCo return completionFunc, exists } -// GetFlagCompletionByName returns the completion function for the given flag in the command by name, if available. -func (c *Command) GetFlagCompletionByName(flagName string) (func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective), bool) { - flag := c.Flags().Lookup(flagName) - if flag == nil { - return nil, false - } - - return GetFlagCompletion(flag) -} - // Returns a string listing the different directive enabled in the specified parameter func (d ShellCompDirective) string() string { var directives []string diff --git a/completions_test.go b/completions_test.go index 7585f88..d5aee25 100644 --- a/completions_test.go +++ b/completions_test.go @@ -3427,3 +3427,93 @@ Completion ended with directive: ShellCompDirectiveNoFileComp }) } } + +func TestGetFlagCompletion(t *testing.T) { + rootCmd := &Command{Use: "root", Run: emptyRun} + + rootCmd.Flags().String("rootflag", "", "root flag") + _ = rootCmd.RegisterFlagCompletionFunc("rootflag", func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + return []string{"rootvalue"}, ShellCompDirectiveKeepOrder + }) + + rootCmd.PersistentFlags().String("persistentflag", "", "persistent flag") + _ = rootCmd.RegisterFlagCompletionFunc("persistentflag", func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + return []string{"persistentvalue"}, ShellCompDirectiveDefault + }) + + childCmd := &Command{Use: "child", Run: emptyRun} + + childCmd.Flags().String("childflag", "", "child flag") + _ = childCmd.RegisterFlagCompletionFunc("childflag", func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + return []string{"childvalue"}, ShellCompDirectiveNoFileComp | ShellCompDirectiveNoSpace + }) + + rootCmd.AddCommand(childCmd) + + testcases := []struct { + desc string + cmd *Command + flagName string + exists bool + comps []string + directive ShellCompDirective + }{ + { + desc: "get flag completion function for command", + cmd: rootCmd, + flagName: "rootflag", + exists: true, + comps: []string{"rootvalue"}, + directive: ShellCompDirectiveKeepOrder, + }, + { + desc: "get persistent flag completion function for command", + cmd: rootCmd, + flagName: "persistentflag", + exists: true, + comps: []string{"persistentvalue"}, + directive: ShellCompDirectiveDefault, + }, + { + desc: "get flag completion function for child command", + cmd: childCmd, + flagName: "childflag", + exists: true, + comps: []string{"childvalue"}, + directive: ShellCompDirectiveNoFileComp | ShellCompDirectiveNoSpace, + }, + { + desc: "get persistent flag completion function for child command", + cmd: childCmd, + flagName: "persistentflag", + exists: true, + comps: []string{"persistentvalue"}, + directive: ShellCompDirectiveDefault, + }, + { + desc: "cannot get flag completion function for local parent flag", + cmd: childCmd, + flagName: "rootflag", + exists: false, + }, + } + + for _, tc := range testcases { + t.Run(tc.desc, func(t *testing.T) { + compFunc, exists := tc.cmd.GetFlagCompletionFunc(tc.flagName) + if tc.exists != exists { + t.Errorf("Unexpected result looking for flag completion function") + } + + if exists { + comps, directive := compFunc(tc.cmd, []string{}, "") + if strings.Join(tc.comps, " ") != strings.Join(comps, " ") { + t.Errorf("Unexpected completions %q", comps) + } + if tc.directive != directive { + t.Errorf("Unexpected directive %q", directive) + } + } + }) + } +} From 283e32d8896fb1beb9d07c742ed16fa2c182ee65 Mon Sep 17 00:00:00 2001 From: SwagPack Date: Mon, 13 Nov 2023 22:25:06 +0530 Subject: [PATCH 21/45] Add LXC to the list of projects using cobra (#2071) --- site/content/projects_using_cobra.md | 1 + 1 file changed, 1 insertion(+) diff --git a/site/content/projects_using_cobra.md b/site/content/projects_using_cobra.md index 8a291eb..f85e9fa 100644 --- a/site/content/projects_using_cobra.md +++ b/site/content/projects_using_cobra.md @@ -30,6 +30,7 @@ - [Kubescape](https://github.com/kubescape/kubescape) - [KubeVirt](https://github.com/kubevirt/kubevirt) - [Linkerd](https://linkerd.io/) +- [LXC](https://github.com/canonical/lxd) - [Mattermost-server](https://github.com/mattermost/mattermost-server) - [Mercure](https://mercure.rocks/) - [Meroxa CLI](https://github.com/meroxa/cli) From 3d8ac432bdad89db04ab0890754b2444d7b4e1cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 23 Nov 2023 19:24:33 +0200 Subject: [PATCH 22/45] Micro-optimizations (#1957) * Avoid redundant string splits There likely isn't actually more than once to split in the source strings in these cases, but avoid doing so anyway as we're only interested in the first. * Avoid redundant completion output target evaluations The target is not to be changed while outputting completions, so resolve it only once. * Avoid redundant active help enablement evaluations The enablement state is not to be changed during completion output, so evaluate it only once. * Preallocate some slices and maps with known size * Avoid some unnecessary looping * Use strings.Builder to construct suggestions --- args.go | 4 ++-- bash_completions.go | 2 +- cobra.go | 2 -- command.go | 10 +++++----- completions.go | 20 ++++++++++---------- flag_groups.go | 10 +++++----- 6 files changed, 23 insertions(+), 25 deletions(-) diff --git a/args.go b/args.go index e79ec33..ed1e70c 100644 --- a/args.go +++ b/args.go @@ -52,9 +52,9 @@ func OnlyValidArgs(cmd *Command, args []string) error { if len(cmd.ValidArgs) > 0 { // Remove any description that may be included in ValidArgs. // A description is following a tab character. - var validArgs []string + validArgs := make([]string, 0, len(cmd.ValidArgs)) for _, v := range cmd.ValidArgs { - validArgs = append(validArgs, strings.Split(v, "\t")[0]) + validArgs = append(validArgs, strings.SplitN(v, "\t", 2)[0]) } for _, v := range args { if !stringInSlice(v, validArgs) { diff --git a/bash_completions.go b/bash_completions.go index 8a53151..be83570 100644 --- a/bash_completions.go +++ b/bash_completions.go @@ -621,7 +621,7 @@ func writeRequiredNouns(buf io.StringWriter, cmd *Command) { for _, value := range cmd.ValidArgs { // Remove any description that may be included following a tab character. // Descriptions are not supported by bash completion. - value = strings.Split(value, "\t")[0] + value = strings.SplitN(value, "\t", 2)[0] WriteStringAndCheck(buf, fmt.Sprintf(" must_have_one_noun+=(%q)\n", value)) } if cmd.ValidArgsFunction != nil { diff --git a/cobra.go b/cobra.go index a6b160c..e0b0947 100644 --- a/cobra.go +++ b/cobra.go @@ -193,8 +193,6 @@ func ld(s, t string, ignoreCase bool) int { 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] { diff --git a/command.go b/command.go index 2fbe6c1..11a3e9c 100644 --- a/command.go +++ b/command.go @@ -706,7 +706,7 @@ Loop: // This is not a flag or a flag value. Check to see if it matches what we're looking for, and if so, // return the args, excluding the one at this position. if s == x { - ret := []string{} + ret := make([]string, 0, len(args)-1) ret = append(ret, args[:pos]...) ret = append(ret, args[pos+1:]...) return ret @@ -754,14 +754,14 @@ func (c *Command) findSuggestions(arg string) string { if c.SuggestionsMinimumDistance <= 0 { c.SuggestionsMinimumDistance = 2 } - suggestionsString := "" + var sb strings.Builder if suggestions := c.SuggestionsFor(arg); len(suggestions) > 0 { - suggestionsString += "\n\nDid you mean this?\n" + sb.WriteString("\n\nDid you mean this?\n") for _, s := range suggestions { - suggestionsString += fmt.Sprintf("\t%v\n", s) + _, _ = fmt.Fprintf(&sb, "\t%v\n", s) } } - return suggestionsString + return sb.String() } func (c *Command) findNext(next string) *Command { diff --git a/completions.go b/completions.go index b60f6b2..b0e41df 100644 --- a/completions.go +++ b/completions.go @@ -212,23 +212,23 @@ func (c *Command) initCompleteCmd(args []string) { } noDescriptions := (cmd.CalledAs() == ShellCompNoDescRequestCmd) + noActiveHelp := GetActiveHelpConfig(finalCmd) == activeHelpGlobalDisable + out := finalCmd.OutOrStdout() for _, comp := range completions { - if GetActiveHelpConfig(finalCmd) == activeHelpGlobalDisable { - // Remove all activeHelp entries in this case - if strings.HasPrefix(comp, activeHelpMarker) { - continue - } + if noActiveHelp && strings.HasPrefix(comp, activeHelpMarker) { + // Remove all activeHelp entries if it's disabled. + continue } if noDescriptions { // Remove any description that may be included following a tab character. - comp = strings.Split(comp, "\t")[0] + comp = strings.SplitN(comp, "\t", 2)[0] } // Make sure we only write the first line to the output. // This is needed if a description contains a linebreak. // Otherwise the shell scripts will interpret the other lines as new flags // and could therefore provide a wrong completion. - comp = strings.Split(comp, "\n")[0] + comp = strings.SplitN(comp, "\n", 2)[0] // Finally trim the completion. This is especially important to get rid // of a trailing tab when there are no description following it. @@ -237,14 +237,14 @@ func (c *Command) initCompleteCmd(args []string) { // although there is no description). comp = strings.TrimSpace(comp) - // Print each possible completion to stdout for the completion script to consume. - fmt.Fprintln(finalCmd.OutOrStdout(), comp) + // Print each possible completion to the output for the completion script to consume. + fmt.Fprintln(out, comp) } // As the last printout, print the completion directive for the completion script to parse. // The directive integer must be that last character following a single colon (:). // The completion script expects : - fmt.Fprintf(finalCmd.OutOrStdout(), ":%d\n", directive) + fmt.Fprintf(out, ":%d\n", directive) // Print some helpful info to stderr for the user to understand. // Output from stderr must be ignored by the completion script. diff --git a/flag_groups.go b/flag_groups.go index 0671ec5..2be3b18 100644 --- a/flag_groups.go +++ b/flag_groups.go @@ -130,7 +130,7 @@ func processFlagForGroupAnnotation(flags *flag.FlagSet, pflag *flag.Flag, annota continue } - groupStatus[group] = map[string]bool{} + groupStatus[group] = make(map[string]bool, len(flagnames)) for _, name := range flagnames { groupStatus[group][name] = false } @@ -253,17 +253,17 @@ func (c *Command) enforceFlagGroupsForCompletion() { // If none of the flags of a one-required group are present, we make all the flags // of that group required so that the shell completion suggests them automatically for flagList, flagnameAndStatus := range oneRequiredGroupStatus { - set := 0 + isSet := false - for _, isSet := range flagnameAndStatus { + for _, isSet = range flagnameAndStatus { if isSet { - set++ + break } } // None of the flags of the group are set, mark all flags in the group // as required - if set == 0 { + if !isSet { for _, fName := range strings.Split(flagList, " ") { _ = c.MarkFlagRequired(fName) } From 236f3c0418a1912d32159ad9c0b74f3f03764f5d Mon Sep 17 00:00:00 2001 From: Marcus Kohlberg <78424526+marcuskohlberg@users.noreply.github.com> Date: Tue, 12 Dec 2023 17:26:29 +0100 Subject: [PATCH 23/45] Update projects_using_cobra.md (#2089) Add Encore to the list of projects using Cobra --- site/content/projects_using_cobra.md | 1 + 1 file changed, 1 insertion(+) diff --git a/site/content/projects_using_cobra.md b/site/content/projects_using_cobra.md index f85e9fa..73e7fea 100644 --- a/site/content/projects_using_cobra.md +++ b/site/content/projects_using_cobra.md @@ -12,6 +12,7 @@ - [Datree](https://github.com/datreeio/datree) - [Delve](https://github.com/derekparker/delve) - [Docker (distribution)](https://github.com/docker/distribution) +- [Encore](https://encore.dev) - [Etcd](https://etcd.io/) - [Gardener](https://github.com/gardener/gardenctl) - [Giant Swarm's gsctl](https://github.com/giantswarm/gsctl) From e63925d3216142ba2c8d7641d729c6c15f4433e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 17 Dec 2023 20:50:59 +0200 Subject: [PATCH 24/45] Add env variable to suppress completion descriptions on create (#1938) COBRA_COMPLETION_DESCRIPTIONS=0 or _COMPLETION_DESCRIPTIONS=0 can now be used to disable shell completion descriptions. --- active_help.go | 13 +- completions.go | 40 +++++- completions_test.go | 192 +++++++++++++++++++++++++++++ site/content/completions/_index.md | 3 + 4 files changed, 237 insertions(+), 11 deletions(-) diff --git a/active_help.go b/active_help.go index 5f965e0..25c30e3 100644 --- a/active_help.go +++ b/active_help.go @@ -17,21 +17,17 @@ package cobra import ( "fmt" "os" - "regexp" - "strings" ) const ( activeHelpMarker = "_activeHelp_ " // The below values should not be changed: programs will be using them explicitly // in their user documentation, and users will be using them explicitly. - activeHelpEnvVarSuffix = "_ACTIVE_HELP" - activeHelpGlobalEnvVar = "COBRA_ACTIVE_HELP" + activeHelpEnvVarSuffix = "ACTIVE_HELP" + activeHelpGlobalEnvVar = configEnvVarGlobalPrefix + "_" + activeHelpEnvVarSuffix activeHelpGlobalDisable = "0" ) -var activeHelpEnvVarPrefixSubstRegexp = regexp.MustCompile(`[^A-Z0-9_]`) - // AppendActiveHelp adds the specified string to the specified array to be used as ActiveHelp. // Such strings will be processed by the completion script and will be shown as ActiveHelp // to the user. @@ -60,8 +56,5 @@ func GetActiveHelpConfig(cmd *Command) string { // variable. It has the format _ACTIVE_HELP where is the name of the // root command in upper case, with all non-ASCII-alphanumeric characters replaced by `_`. func activeHelpEnvVar(name string) string { - // This format should not be changed: users will be using it explicitly. - activeHelpEnvVar := strings.ToUpper(fmt.Sprintf("%s%s", name, activeHelpEnvVarSuffix)) - activeHelpEnvVar = activeHelpEnvVarPrefixSubstRegexp.ReplaceAllString(activeHelpEnvVar, "_") - return activeHelpEnvVar + return configEnvVar(name, activeHelpEnvVarSuffix) } diff --git a/completions.go b/completions.go index b0e41df..ad7b6d0 100644 --- a/completions.go +++ b/completions.go @@ -17,6 +17,8 @@ package cobra import ( "fmt" "os" + "regexp" + "strconv" "strings" "sync" @@ -211,7 +213,12 @@ func (c *Command) initCompleteCmd(args []string) { // 2- Even without completions, we need to print the directive } - noDescriptions := (cmd.CalledAs() == ShellCompNoDescRequestCmd) + noDescriptions := cmd.CalledAs() == ShellCompNoDescRequestCmd + if !noDescriptions { + if doDescriptions, err := strconv.ParseBool(getEnvConfig(cmd, configEnvVarSuffixDescriptions)); err == nil { + noDescriptions = !doDescriptions + } + } noActiveHelp := GetActiveHelpConfig(finalCmd) == activeHelpGlobalDisable out := finalCmd.OutOrStdout() for _, comp := range completions { @@ -899,3 +906,34 @@ func CompError(msg string) { func CompErrorln(msg string) { CompError(fmt.Sprintf("%s\n", msg)) } + +// These values should not be changed: users will be using them explicitly. +const ( + configEnvVarGlobalPrefix = "COBRA" + configEnvVarSuffixDescriptions = "COMPLETION_DESCRIPTIONS" +) + +var configEnvVarPrefixSubstRegexp = regexp.MustCompile(`[^A-Z0-9_]`) + +// configEnvVar returns the name of the program-specific configuration environment +// variable. It has the format _ where is the name of the +// root command in upper case, with all non-ASCII-alphanumeric characters replaced by `_`. +func configEnvVar(name, suffix string) string { + // This format should not be changed: users will be using it explicitly. + v := strings.ToUpper(fmt.Sprintf("%s_%s", name, suffix)) + v = configEnvVarPrefixSubstRegexp.ReplaceAllString(v, "_") + return v +} + +// getEnvConfig returns the value of the configuration environment variable +// _ where is the name of the root command in upper +// case, with all non-ASCII-alphanumeric characters replaced by `_`. +// If the value is empty or not set, the value of the environment variable +// COBRA_ is returned instead. +func getEnvConfig(cmd *Command, suffix string) string { + v := os.Getenv(configEnvVar(cmd.Root().Name(), suffix)) + if v == "" { + v = os.Getenv(configEnvVar(configEnvVarGlobalPrefix, suffix)) + } + return v +} diff --git a/completions_test.go b/completions_test.go index d5aee25..df153fc 100644 --- a/completions_test.go +++ b/completions_test.go @@ -18,6 +18,7 @@ import ( "bytes" "context" "fmt" + "os" "strings" "sync" "testing" @@ -3517,3 +3518,194 @@ func TestGetFlagCompletion(t *testing.T) { }) } } + +func TestGetEnvConfig(t *testing.T) { + testCases := []struct { + desc string + use string + suffix string + cmdVar string + globalVar string + cmdVal string + globalVal string + expected string + }{ + { + desc: "Command envvar overrides global", + use: "root", + suffix: "test", + cmdVar: "ROOT_TEST", + globalVar: "COBRA_TEST", + cmdVal: "cmd", + globalVal: "global", + expected: "cmd", + }, + { + desc: "Missing/empty command envvar falls back to global", + use: "root", + suffix: "test", + cmdVar: "ROOT_TEST", + globalVar: "COBRA_TEST", + cmdVal: "", + globalVal: "global", + expected: "global", + }, + { + desc: "Missing/empty command and global envvars fall back to empty", + use: "root", + suffix: "test", + cmdVar: "ROOT_TEST", + globalVar: "COBRA_TEST", + cmdVal: "", + globalVal: "", + expected: "", + }, + { + desc: "Periods in command use transform to underscores in env var name", + use: "foo.bar", + suffix: "test", + cmdVar: "FOO_BAR_TEST", + globalVar: "COBRA_TEST", + cmdVal: "cmd", + globalVal: "global", + expected: "cmd", + }, + { + desc: "Dashes in command use transform to underscores in env var name", + use: "quux-BAZ", + suffix: "test", + cmdVar: "QUUX_BAZ_TEST", + globalVar: "COBRA_TEST", + cmdVal: "cmd", + globalVal: "global", + expected: "cmd", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + // Could make env handling cleaner with t.Setenv with Go >= 1.17 + err := os.Setenv(tc.cmdVar, tc.cmdVal) + defer func() { + assertNoErr(t, os.Unsetenv(tc.cmdVar)) + }() + assertNoErr(t, err) + err = os.Setenv(tc.globalVar, tc.globalVal) + defer func() { + assertNoErr(t, os.Unsetenv(tc.globalVar)) + }() + assertNoErr(t, err) + cmd := &Command{Use: tc.use} + got := getEnvConfig(cmd, tc.suffix) + if got != tc.expected { + t.Errorf("expected: %q, got: %q", tc.expected, got) + } + }) + } +} + +func TestDisableDescriptions(t *testing.T) { + rootCmd := &Command{ + Use: "root", + Run: emptyRun, + } + + childCmd := &Command{ + Use: "thechild", + Short: "The child command", + Run: emptyRun, + } + rootCmd.AddCommand(childCmd) + + specificDescriptionsEnvVar := configEnvVar(rootCmd.Name(), configEnvVarSuffixDescriptions) + globalDescriptionsEnvVar := configEnvVar(configEnvVarGlobalPrefix, configEnvVarSuffixDescriptions) + + const ( + descLineWithDescription = "first\tdescription" + descLineWithoutDescription = "first" + ) + childCmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + comps := []string{descLineWithDescription} + return comps, ShellCompDirectiveDefault + } + + testCases := []struct { + desc string + globalEnvValue string + specificEnvValue string + expectedLine string + }{ + { + "No env variables set", + "", + "", + descLineWithDescription, + }, + { + "Global value false", + "false", + "", + descLineWithoutDescription, + }, + { + "Specific value false", + "", + "false", + descLineWithoutDescription, + }, + { + "Both values false", + "false", + "false", + descLineWithoutDescription, + }, + { + "Both values true", + "true", + "true", + descLineWithDescription, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + if err := os.Setenv(specificDescriptionsEnvVar, tc.specificEnvValue); err != nil { + t.Errorf("Unexpected error setting %s: %v", specificDescriptionsEnvVar, err) + } + if err := os.Setenv(globalDescriptionsEnvVar, tc.globalEnvValue); err != nil { + t.Errorf("Unexpected error setting %s: %v", globalDescriptionsEnvVar, err) + } + + var run = func() { + output, err := executeCommand(rootCmd, ShellCompRequestCmd, "thechild", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := strings.Join([]string{ + tc.expectedLine, + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + } + + run() + + // For empty cases, test also unset state + if tc.specificEnvValue == "" { + if err := os.Unsetenv(specificDescriptionsEnvVar); err != nil { + t.Errorf("Unexpected error unsetting %s: %v", specificDescriptionsEnvVar, err) + } + run() + } + if tc.globalEnvValue == "" { + if err := os.Unsetenv(globalDescriptionsEnvVar); err != nil { + t.Errorf("Unexpected error unsetting %s: %v", globalDescriptionsEnvVar, err) + } + run() + } + }) + } +} diff --git a/site/content/completions/_index.md b/site/content/completions/_index.md index 4efad29..02257ad 100644 --- a/site/content/completions/_index.md +++ b/site/content/completions/_index.md @@ -393,6 +393,9 @@ $ source <(helm completion bash --no-descriptions) $ helm completion [tab][tab] bash fish powershell zsh ``` + +Setting the `_COMPLETION_DESCRIPTIONS` environment variable (falling back to `COBRA_COMPLETION_DESCRIPTIONS` if empty or not set) to a [falsey value](https://pkg.go.dev/strconv#ParseBool) achieves the same. `` is the name of your program with all non-ASCII-alphanumeric characters replaced by `_`. + ## Bash completions ### Dependencies From df547f5fc6ee86071f73c36c16afd885bb4e3f28 Mon Sep 17 00:00:00 2001 From: Nir Soffer Date: Sun, 12 Nov 2023 17:20:47 +0200 Subject: [PATCH 25/45] Fix help text for plugins When using `CommandDisplayNameAnnotation` we want to use it instead of the command name in `--help` message or in the default help command. With current code we get the wrong text in the --help usage text: Flags: -h, --help help for kubectl-plugin And in the long description of the default help command: $ kubectl cobraplugin help -h Help provides help for any command in the application. Simply type kubectl-plugin help [path to command] for full details. The issue was hidden since the test checked only the Usage line. Fixed by extracting a displayName() function and use it when creating FlagSet and when formatting the default help flag usage and the help command long description. Enhance the TestPlugin to check all the lines including the command name. --- command.go | 27 ++++++++++++++++----------- command_test.go | 13 ++++++++++++- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/command.go b/command.go index 11a3e9c..5c08c00 100644 --- a/command.go +++ b/command.go @@ -1187,10 +1187,11 @@ func (c *Command) InitDefaultHelpFlag() { c.mergePersistentFlags() if c.Flags().Lookup("help") == nil { usage := "help for " - if c.Name() == "" { + name := c.displayName() + if name == "" { usage += "this command" } else { - usage += c.Name() + usage += name } c.Flags().BoolP("help", "h", false, usage) _ = c.Flags().SetAnnotation("help", FlagSetByCobraAnnotation, []string{"true"}) @@ -1236,7 +1237,7 @@ func (c *Command) InitDefaultHelpCmd() { Use: "help [command]", Short: "Help about any command", Long: `Help provides help for any command in the application. -Simply type ` + c.Name() + ` help [path to command] for full details.`, +Simply type ` + c.displayName() + ` help [path to command] for full details.`, ValidArgsFunction: func(c *Command, args []string, toComplete string) ([]string, ShellCompDirective) { var completions []string cmd, _, e := c.Root().Find(args) @@ -1427,6 +1428,10 @@ func (c *Command) CommandPath() string { if c.HasParent() { return c.Parent().CommandPath() + " " + c.Name() } + return c.displayName() +} + +func (c *Command) displayName() string { if displayName, ok := c.Annotations[CommandDisplayNameAnnotation]; ok { return displayName } @@ -1642,7 +1647,7 @@ func (c *Command) GlobalNormalizationFunc() func(f *flag.FlagSet, name string) f // to this command (local and persistent declared here and by all parents). func (c *Command) Flags() *flag.FlagSet { if c.flags == nil { - c.flags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + c.flags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError) if c.flagErrorBuf == nil { c.flagErrorBuf = new(bytes.Buffer) } @@ -1656,7 +1661,7 @@ func (c *Command) Flags() *flag.FlagSet { func (c *Command) LocalNonPersistentFlags() *flag.FlagSet { persistentFlags := c.PersistentFlags() - out := flag.NewFlagSet(c.Name(), flag.ContinueOnError) + out := flag.NewFlagSet(c.displayName(), flag.ContinueOnError) c.LocalFlags().VisitAll(func(f *flag.Flag) { if persistentFlags.Lookup(f.Name) == nil { out.AddFlag(f) @@ -1670,7 +1675,7 @@ func (c *Command) LocalFlags() *flag.FlagSet { c.mergePersistentFlags() if c.lflags == nil { - c.lflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + c.lflags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError) if c.flagErrorBuf == nil { c.flagErrorBuf = new(bytes.Buffer) } @@ -1697,7 +1702,7 @@ func (c *Command) InheritedFlags() *flag.FlagSet { c.mergePersistentFlags() if c.iflags == nil { - c.iflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + c.iflags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError) if c.flagErrorBuf == nil { c.flagErrorBuf = new(bytes.Buffer) } @@ -1725,7 +1730,7 @@ func (c *Command) NonInheritedFlags() *flag.FlagSet { // PersistentFlags returns the persistent FlagSet specifically set in the current command. func (c *Command) PersistentFlags() *flag.FlagSet { if c.pflags == nil { - c.pflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + c.pflags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError) if c.flagErrorBuf == nil { c.flagErrorBuf = new(bytes.Buffer) } @@ -1738,9 +1743,9 @@ func (c *Command) PersistentFlags() *flag.FlagSet { func (c *Command) ResetFlags() { c.flagErrorBuf = new(bytes.Buffer) c.flagErrorBuf.Reset() - c.flags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + c.flags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError) c.flags.SetOutput(c.flagErrorBuf) - c.pflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + c.pflags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError) c.pflags.SetOutput(c.flagErrorBuf) c.lflags = nil @@ -1857,7 +1862,7 @@ func (c *Command) mergePersistentFlags() { // If c.parentsPflags == nil, it makes new. func (c *Command) updateParentsPflags() { if c.parentsPflags == nil { - c.parentsPflags = flag.NewFlagSet(c.Name(), flag.ContinueOnError) + c.parentsPflags = flag.NewFlagSet(c.displayName(), flag.ContinueOnError) c.parentsPflags.SetOutput(c.flagErrorBuf) c.parentsPflags.SortFlags = false } diff --git a/command_test.go b/command_test.go index 9f686d6..a30c836 100644 --- a/command_test.go +++ b/command_test.go @@ -371,7 +371,7 @@ func TestAliasPrefixMatching(t *testing.T) { // text should reflect the way we run the command. func TestPlugin(t *testing.T) { rootCmd := &Command{ - Use: "plugin", + Use: "kubectl-plugin", Args: NoArgs, Annotations: map[string]string{ CommandDisplayNameAnnotation: "kubectl plugin", @@ -387,6 +387,8 @@ func TestPlugin(t *testing.T) { } checkStringContains(t, rootHelp, "kubectl plugin [command]") + checkStringContains(t, rootHelp, "help for kubectl plugin") + checkStringContains(t, rootHelp, "kubectl plugin [command] --help") childHelp, err := executeCommand(rootCmd, "sub", "-h") if err != nil { @@ -394,6 +396,15 @@ func TestPlugin(t *testing.T) { } checkStringContains(t, childHelp, "kubectl plugin sub [flags]") + checkStringContains(t, childHelp, "help for sub") + + helpHelp, err := executeCommand(rootCmd, "help", "-h") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + checkStringContains(t, helpHelp, "kubectl plugin help [path to command]") + checkStringContains(t, helpHelp, "kubectl plugin help [command]") } // TestChildSameName checks the correct behaviour of cobra in cases, From a73b9c391a9489d20f5ee1480e75d3b99fc8c7e2 Mon Sep 17 00:00:00 2001 From: Nir Soffer Date: Sun, 12 Nov 2023 20:07:59 +0200 Subject: [PATCH 26/45] Fix help text for runnable plugin command When creating a plugin without sub commands, the help text included the command name (kubectl-plugin) instead of the display name (kubectl plugin): Usage: kubectl-plugin [flags] The issue is that the usage line for this case does not use the command path but the raw `Use` string, and this case was not tested. Add a test for this case and fix UsageLine() to replace the command name with the display name. Tested using https://github.com/nirs/kubernetes/tree/sample-cli-plugin-help --- command.go | 5 +++-- command_test.go | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/command.go b/command.go index 5c08c00..677dc9b 100644 --- a/command.go +++ b/command.go @@ -1441,10 +1441,11 @@ func (c *Command) displayName() string { // UseLine puts out the full usage for a given command (including parents). func (c *Command) UseLine() string { var useline string + use := strings.Replace(c.Use, c.Name(), c.displayName(), 1) if c.HasParent() { - useline = c.parent.CommandPath() + " " + c.Use + useline = c.parent.CommandPath() + " " + use } else { - useline = c.Use + useline = use } if c.DisableFlagsInUseLine { return useline diff --git a/command_test.go b/command_test.go index a30c836..b7d88e4 100644 --- a/command_test.go +++ b/command_test.go @@ -370,6 +370,26 @@ func TestAliasPrefixMatching(t *testing.T) { // executable is `kubectl-plugin`, but we run it as `kubectl plugin`. The help // text should reflect the way we run the command. func TestPlugin(t *testing.T) { + cmd := &Command{ + Use: "kubectl-plugin", + Args: NoArgs, + Annotations: map[string]string{ + CommandDisplayNameAnnotation: "kubectl plugin", + }, + Run: emptyRun, + } + + cmdHelp, err := executeCommand(cmd, "-h") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + checkStringContains(t, cmdHelp, "kubectl plugin [flags]") + checkStringContains(t, cmdHelp, "help for kubectl plugin") +} + +// TestPlugin checks usage as plugin with sub commands. +func TestPluginWithSubCommands(t *testing.T) { rootCmd := &Command{ Use: "kubectl-plugin", Args: NoArgs, From 41227856cd731aeeeb5a2f7203b3a4c16b81fc67 Mon Sep 17 00:00:00 2001 From: Nir Soffer Date: Sun, 12 Nov 2023 15:55:40 +0200 Subject: [PATCH 27/45] Document how to create a plugin Using the new CommandDisplayNameAnnotation annotation introduced in Cobra 1.8.0. --- site/content/user_guide.md | 54 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/site/content/user_guide.md b/site/content/user_guide.md index 4116e8d..3b42ef0 100644 --- a/site/content/user_guide.md +++ b/site/content/user_guide.md @@ -748,3 +748,57 @@ Read more about it in [Shell Completions](completions/_index.md). Cobra makes use of the shell-completion system to define a framework allowing you to provide Active Help to your users. Active Help are messages (hints, warnings, etc) printed as the program is being used. Read more about it in [Active Help](active_help.md). + +## Creating a plugin + +When creating a plugin for tools like *kubectl*, the executable is named +`kubectl-myplugin`, but it is used as `kubectl myplugin`. To fix help +messages and completions, annotate the root command with the +`cobra.CommandDisplayNameAnnotation` annotation. + +### Example kubectl plugin + +```go +package main + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func main() { + rootCmd := &cobra.Command{ + Use: "kubectl-myplugin", + Annotations: map[string]string{ + cobra.CommandDisplayNameAnnotation: "kubectl myplugin", + }, + } + subCmd := &cobra.Command{ + Use: "subcmd", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("kubectl myplugin subcmd") + }, + } + rootCmd.AddCommand(subCmd) + rootCmd.Execute() +} +``` + +Example run as a kubectl plugin: + +``` +$ kubectl myplugin +Usage: + kubectl myplugin [command] + +Available Commands: + completion Generate the autocompletion script for the specified shell + help Help about any command + subcmd + +Flags: + -h, --help help for kubectl myplugin + +Use "kubectl myplugin [command] --help" for more information about a command. +``` From c054701f6a5522c259dabfd206c54b41e1788519 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 10:06:36 +0000 Subject: [PATCH 28/45] build(deps): bump actions/setup-go from 4 to 5 Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5. - [Release notes](https://github.com/actions/setup-go/releases) - [Commits](https://github.com/actions/setup-go/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-go dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a924532..b0cec77 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: '^1.21' check-latest: true @@ -74,7 +74,7 @@ jobs: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version: 1.${{ matrix.go }}.x cache: true From 531ce793e3a44b949b1d0296a382f4f3a2c9da0d Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Sat, 23 Dec 2023 10:11:53 -0500 Subject: [PATCH 29/45] Remove extra actions/checkout Signed-off-by: Marc Khouzam --- .github/workflows/test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b0cec77..74ed331 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,8 +47,6 @@ jobs: check-latest: true cache: true - - uses: actions/checkout@v4 - - uses: golangci/golangci-lint-action@v3.7.0 with: version: latest From 199b7abe121caf41a163289041b13b0ca2418a72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 23 Dec 2023 14:52:45 -0500 Subject: [PATCH 30/45] build(deps): bump actions/labeler from 4 to 5 (#2086) * build(deps): bump actions/labeler from 4 to 5 Bumps [actions/labeler](https://github.com/actions/labeler) from 4 to 5. - [Release notes](https://github.com/actions/labeler/releases) - [Commits](https://github.com/actions/labeler/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/labeler dependency-type: direct:production update-type: version-update:semver-major ... * Update labeler configuration for v5 Signed-off-by: dependabot[bot] Signed-off-by: Marc Khouzam Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Marc Khouzam --- .github/labeler.yml | 19 +++++++++++++------ .github/workflows/labeler.yml | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 0f0bc3c..0db3be2 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,17 +1,24 @@ # changes to documentation generation -"area/docs-generation": doc/**/* +"area/docs-generation": +- changed-files: + - any-glob-to-any-file: 'doc/**' # changes to the core cobra command "area/cobra-command": -- any: ['./cobra.go', './cobra_test.go', './*command*.go'] +- changed-files: + - any-glob-to-any-file: ['./cobra.go', './cobra_test.go', './*command*.go'] # changes made to command flags/args -"area/flags": ./args*.go +"area/flags": +- changed-files: + - any-glob-to-any-file: './args*.go' # changes to Github workflows -"area/github": .github/**/* +"area/github": +- changed-files: + - any-glob-to-any-file: '.github/**' # changes to shell completions "area/shell-completion": - - ./*completions* - +- changed-files: + - any-glob-to-any-file: './*completions*' diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 17f451f..cc5c540 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -12,7 +12,7 @@ jobs: pull-requests: write # for actions/labeler to add labels to PRs runs-on: ubuntu-latest steps: - - uses: actions/labeler@v4 + - uses: actions/labeler@v5 with: repo-token: "${{ github.token }}" From cbcf75eab98095a631a917692f9667b55d3ccfd6 Mon Sep 17 00:00:00 2001 From: Case Wylie Date: Wed, 27 Dec 2023 21:16:29 -0500 Subject: [PATCH 31/45] [chore]: update projects using cobra (#2093) Signed-off-by: Case Wylie --- site/content/projects_using_cobra.md | 1 + 1 file changed, 1 insertion(+) diff --git a/site/content/projects_using_cobra.md b/site/content/projects_using_cobra.md index 73e7fea..75cc065 100644 --- a/site/content/projects_using_cobra.md +++ b/site/content/projects_using_cobra.md @@ -63,4 +63,5 @@ - [Vitess](https://vitess.io) - VMware's [Tanzu Community Edition](https://github.com/vmware-tanzu/community-edition) & [Tanzu Framework](https://github.com/vmware-tanzu/tanzu-framework) - [Werf](https://werf.io/) +- [Zarf](https://github.com/defenseunicorns/zarf) - [ZITADEL](https://github.com/zitadel/zitadel) From 0dec88e7931d4c5d5583e69b12e245741d9f1353 Mon Sep 17 00:00:00 2001 From: Denis <43725617+korovindenis@users.noreply.github.com> Date: Sat, 30 Dec 2023 16:40:15 +0300 Subject: [PATCH 32/45] Add tests for funcs in cobra.go (#2094) --- cobra_test.go | 182 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/cobra_test.go b/cobra_test.go index fbb07f9..2bba461 100644 --- a/cobra_test.go +++ b/cobra_test.go @@ -40,3 +40,185 @@ func TestAddTemplateFunctions(t *testing.T) { t.Errorf("Expected UsageString: %v\nGot: %v", expected, got) } } + +func TestLevenshteinDistance(t *testing.T) { + tests := []struct { + name string + s string + t string + ignoreCase bool + expected int + }{ + { + name: "Equal strings (case-sensitive)", + s: "hello", + t: "hello", + ignoreCase: false, + expected: 0, + }, + { + name: "Equal strings (case-insensitive)", + s: "Hello", + t: "hello", + ignoreCase: true, + expected: 0, + }, + { + name: "Different strings (case-sensitive)", + s: "kitten", + t: "sitting", + ignoreCase: false, + expected: 3, + }, + { + name: "Different strings (case-insensitive)", + s: "Kitten", + t: "Sitting", + ignoreCase: true, + expected: 3, + }, + { + name: "Empty strings", + s: "", + t: "", + ignoreCase: false, + expected: 0, + }, + { + name: "One empty string", + s: "abc", + t: "", + ignoreCase: false, + expected: 3, + }, + { + name: "Both empty strings", + s: "", + t: "", + ignoreCase: true, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Act + got := ld(tt.s, tt.t, tt.ignoreCase) + + // Assert + if got != tt.expected { + t.Errorf("Expected ld: %v\nGot: %v", tt.expected, got) + } + }) + } +} + +func TestStringInSlice(t *testing.T) { + tests := []struct { + name string + a string + list []string + expected bool + }{ + { + name: "String in slice (case-sensitive)", + a: "apple", + list: []string{"orange", "banana", "apple", "grape"}, + expected: true, + }, + { + name: "String not in slice (case-sensitive)", + a: "pear", + list: []string{"orange", "banana", "apple", "grape"}, + expected: false, + }, + { + name: "String in slice (case-insensitive)", + a: "APPLE", + list: []string{"orange", "banana", "apple", "grape"}, + expected: false, + }, + { + name: "Empty slice", + a: "apple", + list: []string{}, + expected: false, + }, + { + name: "Empty string", + a: "", + list: []string{"orange", "banana", "apple", "grape"}, + expected: false, + }, + { + name: "Empty strings match", + a: "", + list: []string{"orange", ""}, + expected: true, + }, + { + name: "Empty string in empty slice", + a: "", + list: []string{}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Act + got := stringInSlice(tt.a, tt.list) + + // Assert + if got != tt.expected { + t.Errorf("Expected stringInSlice: %v\nGot: %v", tt.expected, got) + } + }) + } +} + +func TestRpad(t *testing.T) { + tests := []struct { + name string + inputString string + padding int + expected string + }{ + { + name: "Padding required", + inputString: "Hello", + padding: 10, + expected: "Hello ", + }, + { + name: "No padding required", + inputString: "World", + padding: 5, + expected: "World", + }, + { + name: "Empty string", + inputString: "", + padding: 8, + expected: " ", + }, + { + name: "Zero padding", + inputString: "cobra", + padding: 0, + expected: "cobra", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Act + got := rpad(tt.inputString, tt.padding) + + // Assert + if got != tt.expected { + t.Errorf("Expected rpad: %v\nGot: %v", tt.expected, got) + } + }) + } +} From 4fb0a66a3436bd34b03b858c729404e99cd3124f Mon Sep 17 00:00:00 2001 From: Dmytro Milinevskyi Date: Sat, 6 Jan 2024 22:49:13 +0100 Subject: [PATCH 33/45] flags: clarify documentation that LocalFlags related function do not modify the state (#2064) --- command.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/command.go b/command.go index 677dc9b..b6f8f4b 100644 --- a/command.go +++ b/command.go @@ -154,8 +154,10 @@ type Command struct { // pflags contains persistent flags. pflags *flag.FlagSet // lflags contains local flags. + // This field does not represent internal state, it's used as a cache to optimise LocalFlags function call lflags *flag.FlagSet // iflags contains inherited flags. + // This field does not represent internal state, it's used as a cache to optimise InheritedFlags function call iflags *flag.FlagSet // parentsPflags is all persistent flags of cmd's parents. parentsPflags *flag.FlagSet @@ -1659,6 +1661,7 @@ func (c *Command) Flags() *flag.FlagSet { } // LocalNonPersistentFlags are flags specific to this command which will NOT persist to subcommands. +// This function does not modify the flags of the current command, it's purpose is to return the current state. func (c *Command) LocalNonPersistentFlags() *flag.FlagSet { persistentFlags := c.PersistentFlags() @@ -1672,6 +1675,7 @@ func (c *Command) LocalNonPersistentFlags() *flag.FlagSet { } // LocalFlags returns the local FlagSet specifically set in the current command. +// This function does not modify the flags of the current command, it's purpose is to return the current state. func (c *Command) LocalFlags() *flag.FlagSet { c.mergePersistentFlags() @@ -1699,6 +1703,7 @@ func (c *Command) LocalFlags() *flag.FlagSet { } // InheritedFlags returns all flags which were inherited from parent commands. +// This function does not modify the flags of the current command, it's purpose is to return the current state. func (c *Command) InheritedFlags() *flag.FlagSet { c.mergePersistentFlags() @@ -1724,6 +1729,7 @@ func (c *Command) InheritedFlags() *flag.FlagSet { } // NonInheritedFlags returns all flags which were not inherited from parent commands. +// This function does not modify the flags of the current command, it's purpose is to return the current state. func (c *Command) NonInheritedFlags() *flag.FlagSet { return c.LocalFlags() } From bcfcff729ecdeb4072ef6f2a9c0b5e4ca1853206 Mon Sep 17 00:00:00 2001 From: Radek Smid Date: Mon, 15 Jan 2024 14:38:50 +0100 Subject: [PATCH 34/45] Add Taikun CloudWorks to list of projects (#2098) --- site/content/projects_using_cobra.md | 1 + 1 file changed, 1 insertion(+) diff --git a/site/content/projects_using_cobra.md b/site/content/projects_using_cobra.md index 75cc065..5de771a 100644 --- a/site/content/projects_using_cobra.md +++ b/site/content/projects_using_cobra.md @@ -57,6 +57,7 @@ - [Scaleway CLI](https://github.com/scaleway/scaleway-cli) - [Sia](https://github.com/SiaFoundation/siad) - [Skaffold](https://skaffold.dev/) +- [Taikun](https://taikun.cloud/) - [Tendermint](https://github.com/tendermint/tendermint) - [Twitch CLI](https://github.com/twitchdev/twitch-cli) - [UpCloud CLI (`upctl`)](https://github.com/UpCloudLtd/upcloud-cli) From bd2655e76c8b499761dc8b664c8b04dbfd828354 Mon Sep 17 00:00:00 2001 From: montag451 Date: Fri, 8 Mar 2024 01:36:58 +0100 Subject: [PATCH 35/45] Add Incus to the list of projects using Cobra (#2118) --- site/content/projects_using_cobra.md | 1 + 1 file changed, 1 insertion(+) diff --git a/site/content/projects_using_cobra.md b/site/content/projects_using_cobra.md index 5de771a..52e4e80 100644 --- a/site/content/projects_using_cobra.md +++ b/site/content/projects_using_cobra.md @@ -24,6 +24,7 @@ - [GoReleaser](https://goreleaser.com) - [Helm](https://helm.sh) - [Hugo](https://gohugo.io) +- [Incus](https://linuxcontainers.org/incus/) - [Infracost](https://github.com/infracost/infracost) - [Istio](https://istio.io) - [Kool](https://github.com/kool-dev/kool) From f34069ccf5d77bdc4799389a1700d8f7b3e38cf3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 19:35:05 -0600 Subject: [PATCH 36/45] build(deps): bump golangci/golangci-lint-action from 3.7.0 to 4.0.0 (#2108) Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3.7.0 to 4.0.0. - [Release notes](https://github.com/golangci/golangci-lint-action/releases) - [Commits](https://github.com/golangci/golangci-lint-action/compare/v3.7.0...v4.0.0) --- updated-dependencies: - dependency-name: golangci/golangci-lint-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 74ed331..d6b28e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,7 +47,7 @@ jobs: check-latest: true cache: true - - uses: golangci/golangci-lint-action@v3.7.0 + - uses: golangci/golangci-lint-action@v4.0.0 with: version: latest args: --verbose From a30cee5e5ab0949cc888ef00ae6aee24e091e042 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 19:36:37 -0600 Subject: [PATCH 37/45] build(deps): bump actions/cache from 3 to 4 (#2102) Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d6b28e2..8a5115b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -108,7 +108,7 @@ jobs: - uses: actions/checkout@v4 - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: ~/go/pkg/mod key: ${{ runner.os }}-${{ matrix.go }}-${{ hashFiles('**/go.sum') }} From c69ae4c36b134dd69e5ab9d3d6b9f571ca5afe1e Mon Sep 17 00:00:00 2001 From: damas <19289022+cyrilico@users.noreply.github.com> Date: Tue, 12 Mar 2024 10:40:01 +0000 Subject: [PATCH 38/45] ci: test golang 1.22 (#2113) --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a5115b..21f81e0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,7 +43,7 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: '^1.21' + go-version: '^1.22' check-latest: true cache: true @@ -66,6 +66,7 @@ jobs: - 19 - 20 - 21 + - 22 name: '${{ matrix.platform }} | 1.${{ matrix.go }}.x' runs-on: ${{ matrix.platform }}-latest steps: From 1f80fa2e23cc550c131e8a54dc72d11b265c6fcf Mon Sep 17 00:00:00 2001 From: racerole <148756161+racerole@users.noreply.github.com> Date: Tue, 12 Mar 2024 18:40:19 +0800 Subject: [PATCH 39/45] chore: remove repetitive words (#2122) Signed-off-by: racerole --- doc/md_docs.go | 2 +- doc/rest_docs.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/md_docs.go b/doc/md_docs.go index f98fe2a..1259222 100644 --- a/doc/md_docs.go +++ b/doc/md_docs.go @@ -128,7 +128,7 @@ func GenMarkdownTree(cmd *cobra.Command, dir string) error { return GenMarkdownTreeCustom(cmd, dir, emptyStr, identity) } -// GenMarkdownTreeCustom is the the same as GenMarkdownTree, but +// GenMarkdownTreeCustom is the same as GenMarkdownTree, but // with custom filePrepender and linkHandler. func GenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandler func(string) string) error { for _, c := range cmd.Commands() { diff --git a/doc/rest_docs.go b/doc/rest_docs.go index 2cca6fd..c33acc2 100644 --- a/doc/rest_docs.go +++ b/doc/rest_docs.go @@ -140,7 +140,7 @@ func GenReSTTree(cmd *cobra.Command, dir string) error { return GenReSTTreeCustom(cmd, dir, emptyStr, defaultLinkHandler) } -// GenReSTTreeCustom is the the same as GenReSTTree, but +// GenReSTTreeCustom is the same as GenReSTTree, but // with custom filePrepender and linkHandler. func GenReSTTreeCustom(cmd *cobra.Command, dir string, filePrepender func(string) string, linkHandler func(string, string) string) error { for _, c := range cmd.Commands() { From bd914e58d69d65e494b45bdb40e90ca816b92fcc Mon Sep 17 00:00:00 2001 From: Pedro Mota Date: Tue, 12 Mar 2024 07:42:46 -0300 Subject: [PATCH 40/45] fix: remove deprecated io/ioutils package (#2120) ioutils.ReadAll is deprecated since Go 1.16. This commit replaces it with io.ReadAll. See https://pkg.go.dev/io/ioutil\#ReadAll for reference Issue #2119 --- command_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/command_test.go b/command_test.go index b7d88e4..db33692 100644 --- a/command_test.go +++ b/command_test.go @@ -18,7 +18,7 @@ import ( "bytes" "context" "fmt" - "io/ioutil" + "io" "os" "reflect" "strings" @@ -2092,12 +2092,12 @@ func TestCommandPrintRedirection(t *testing.T) { t.Error(err) } - gotErrBytes, err := ioutil.ReadAll(errBuff) + gotErrBytes, err := io.ReadAll(errBuff) if err != nil { t.Error(err) } - gotOutBytes, err := ioutil.ReadAll(outBuff) + gotOutBytes, err := io.ReadAll(outBuff) if err != nil { t.Error(err) } From 6b5f577ebce858ee70fcdd1f062ea3af4b1c03ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 1 Apr 2024 12:42:08 +0000 Subject: [PATCH 41/45] More linting (#2099) * Address gocritic findings, enable it * Enable gosimple, no new findings to address --- .golangci.yml | 4 ++-- bash_completions.go | 21 +++++++++------------ command_test.go | 2 +- doc/man_docs.go | 2 +- doc/util.go | 2 +- powershell_completions.go | 4 ++-- 6 files changed, 16 insertions(+), 19 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index a618ec2..22ae622 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -29,7 +29,7 @@ linters: - gas #- gochecknoinits - goconst - #- gocritic + - gocritic #- gocyclo #- gofmt - goimports @@ -37,7 +37,7 @@ linters: #- gomnd #- goprintffuncname #- gosec - #- gosimple + - gosimple - govet - ineffassign - interfacer diff --git a/bash_completions.go b/bash_completions.go index be83570..f4d198c 100644 --- a/bash_completions.go +++ b/bash_completions.go @@ -597,19 +597,16 @@ func writeRequiredFlag(buf io.StringWriter, cmd *Command) { if nonCompletableFlag(flag) { return } - for key := range flag.Annotations { - switch key { - case BashCompOneRequiredFlag: - format := " must_have_one_flag+=(\"--%s" - if flag.Value.Type() != "bool" { - format += "=" - } - format += cbn - WriteStringAndCheck(buf, fmt.Sprintf(format, flag.Name)) + if _, ok := flag.Annotations[BashCompOneRequiredFlag]; ok { + format := " must_have_one_flag+=(\"--%s" + if flag.Value.Type() != "bool" { + format += "=" + } + format += cbn + WriteStringAndCheck(buf, fmt.Sprintf(format, flag.Name)) - if len(flag.Shorthand) > 0 { - WriteStringAndCheck(buf, fmt.Sprintf(" must_have_one_flag+=(\"-%s"+cbn, flag.Shorthand)) - } + if len(flag.Shorthand) > 0 { + WriteStringAndCheck(buf, fmt.Sprintf(" must_have_one_flag+=(\"-%s"+cbn, flag.Shorthand)) } } }) diff --git a/command_test.go b/command_test.go index db33692..9ce7a52 100644 --- a/command_test.go +++ b/command_test.go @@ -2777,7 +2777,7 @@ func TestFind(t *testing.T) { func TestUnknownFlagShouldReturnSameErrorRegardlessOfArgPosition(t *testing.T) { testCases := [][]string{ - //{"--unknown", "--namespace", "foo", "child", "--bar"}, // FIXME: This test case fails, returning the error `unknown command "foo" for "root"` instead of the expected error `unknown flag: --unknown` + // {"--unknown", "--namespace", "foo", "child", "--bar"}, // FIXME: This test case fails, returning the error `unknown command "foo" for "root"` instead of the expected error `unknown flag: --unknown` {"--namespace", "foo", "--unknown", "child", "--bar"}, {"--namespace", "foo", "child", "--unknown", "--bar"}, {"--namespace", "foo", "child", "--bar", "--unknown"}, diff --git a/doc/man_docs.go b/doc/man_docs.go index b8c15ce..2138f24 100644 --- a/doc/man_docs.go +++ b/doc/man_docs.go @@ -133,7 +133,7 @@ func fillHeader(header *GenManHeader, name string, disableAutoGen bool) error { } header.Date = &now } - header.date = (*header.Date).Format("Jan 2006") + header.date = header.Date.Format("Jan 2006") if header.Source == "" && !disableAutoGen { header.Source = "Auto generated by spf13/cobra" } diff --git a/doc/util.go b/doc/util.go index 0aaa07a..4de4cee 100644 --- a/doc/util.go +++ b/doc/util.go @@ -40,7 +40,7 @@ func hasSeeAlso(cmd *cobra.Command) bool { // that do not contain \n. func forceMultiLine(s string) string { if len(s) > 60 && !strings.Contains(s, "\n") { - s = s + "\n" + s += "\n" } return s } diff --git a/powershell_completions.go b/powershell_completions.go index 5519519..a830b7b 100644 --- a/powershell_completions.go +++ b/powershell_completions.go @@ -28,8 +28,8 @@ import ( func genPowerShellComp(buf io.StringWriter, name string, includeDesc bool) { // Variables should not contain a '-' or ':' character nameForVar := name - nameForVar = strings.Replace(nameForVar, "-", "_", -1) - nameForVar = strings.Replace(nameForVar, ":", "_", -1) + nameForVar = strings.ReplaceAll(nameForVar, "-", "_") + nameForVar = strings.ReplaceAll(nameForVar, ":", "_") compCmd := ShellCompRequestCmd if !includeDesc { From 0fc86c2ffd0326b6f6ed5fa36803d26993655c08 Mon Sep 17 00:00:00 2001 From: Xinwei Xiong <3293172751NSS@gmail.com> Date: Mon, 8 Apr 2024 18:47:35 +0800 Subject: [PATCH 42/45] docs: update user guide (#2128) --- site/content/user_guide.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/site/content/user_guide.md b/site/content/user_guide.md index 3b42ef0..93e87d6 100644 --- a/site/content/user_guide.md +++ b/site/content/user_guide.md @@ -3,7 +3,7 @@ While you are welcome to provide your own organization, typically a Cobra-based application will follow the following organizational structure: -``` +```test ▾ appName/ ▾ cmd/ add.go @@ -301,6 +301,7 @@ command := cobra.Command{ ### Bind Flags with Config You can also bind your flags with [viper](https://github.com/spf13/viper): + ```go var author string @@ -320,12 +321,14 @@ More in [viper documentation](https://github.com/spf13/viper#working-with-flags) Flags are optional by default. If instead you wish your command to report an error when a flag has not been set, mark it as required: + ```go rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)") rootCmd.MarkFlagRequired("region") ``` Or, for persistent flags: + ```go rootCmd.PersistentFlags().StringVarP(&Region, "region", "r", "", "AWS region (required)") rootCmd.MarkPersistentFlagRequired("region") @@ -335,6 +338,7 @@ rootCmd.MarkPersistentFlagRequired("region") If you have different flags that must be provided together (e.g. if they provide the `--username` flag they MUST provide the `--password` flag as well) then Cobra can enforce that requirement: + ```go rootCmd.Flags().StringVarP(&u, "username", "u", "", "Username (required if password is set)") rootCmd.Flags().StringVarP(&pw, "password", "p", "", "Password (required if username is set)") @@ -343,6 +347,7 @@ rootCmd.MarkFlagsRequiredTogether("username", "password") You can also prevent different flags from being provided together if they represent mutually exclusive options such as specifying an output format as either `--json` or `--yaml` but never both: + ```go rootCmd.Flags().BoolVar(&ofJson, "json", false, "Output in JSON") rootCmd.Flags().BoolVar(&ofYaml, "yaml", false, "Output in YAML") @@ -351,6 +356,7 @@ rootCmd.MarkFlagsMutuallyExclusive("json", "yaml") If you want to require at least one flag from a group to be present, you can use `MarkFlagsOneRequired`. This can be combined with `MarkFlagsMutuallyExclusive` to enforce exactly one flag from a given group: + ```go rootCmd.Flags().BoolVar(&ofJson, "json", false, "Output in JSON") rootCmd.Flags().BoolVar(&ofYaml, "yaml", false, "Output in YAML") @@ -428,7 +434,7 @@ by not providing a 'Run' for the 'rootCmd'. We have only defined one flag for a single command. -More documentation about flags is available at https://github.com/spf13/pflag +More documentation about flags is available at https://github.com/spf13/pflag. ```go package main @@ -722,7 +728,7 @@ command.SuggestionsMinimumDistance = 1 You can also explicitly set names for which a given command will be suggested using the `SuggestFor` attribute. This allows suggestions for strings that are not close in terms of string distance, but make sense in your set of commands but for which you don't want aliases. Example: -``` +```bash $ kubectl remove Error: unknown command "remove" for "kubectl" @@ -787,7 +793,7 @@ func main() { Example run as a kubectl plugin: -``` +```bash $ kubectl myplugin Usage: kubectl myplugin [command] From 5a1acea3210649f3d70002818ec04b09f6347062 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Apr 2024 02:21:03 +0000 Subject: [PATCH 43/45] build(deps): bump github.com/cpuguy83/go-md2man/v2 from 2.0.3 to 2.0.4 (#2127) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a79e66a..8c80da0 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/spf13/cobra go 1.15 require ( - github.com/cpuguy83/go-md2man/v2 v2.0.3 + github.com/cpuguy83/go-md2man/v2 v2.0.4 github.com/inconshreveable/mousetrap v1.1.0 github.com/spf13/pflag v1.0.5 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 871c3a8..ab40b43 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= From 5c2c1d627d35a00153764a3d37400efc66eaca1c Mon Sep 17 00:00:00 2001 From: Nir Soffer Date: Sat, 18 May 2024 16:41:31 +0300 Subject: [PATCH 44/45] Consistent annotation names (#2140) Add `Annotation` suffix to the private annotations to allow nicer code using the constants. For example one can use the current annotation names as a temporary variable instead of unclear shortcut. Instead of this: rag := flagsFromAnnotation(c, f, requiredAsGroup) me := flagsFromAnnotation(c, f, mutuallyExclusive) or := flagsFromAnnotation(c, f, oneRequired) We can use now: requiredAsGrop := flagsFromAnnotation(c, f, requiredAsGroupAnnotation) mutuallyExclusive := flagsFromAnnotation(c, f, mutuallyExclusiveAnnotation) oneRequired := flagsFromAnnotation(c, f, oneRequiredAnnotation) Example taken from #2105. --- flag_groups.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/flag_groups.go b/flag_groups.go index 2be3b18..560612f 100644 --- a/flag_groups.go +++ b/flag_groups.go @@ -23,9 +23,9 @@ import ( ) const ( - requiredAsGroup = "cobra_annotation_required_if_others_set" - oneRequired = "cobra_annotation_one_required" - mutuallyExclusive = "cobra_annotation_mutually_exclusive" + requiredAsGroupAnnotation = "cobra_annotation_required_if_others_set" + oneRequiredAnnotation = "cobra_annotation_one_required" + mutuallyExclusiveAnnotation = "cobra_annotation_mutually_exclusive" ) // MarkFlagsRequiredTogether marks the given flags with annotations so that Cobra errors @@ -37,7 +37,7 @@ func (c *Command) MarkFlagsRequiredTogether(flagNames ...string) { if f == nil { panic(fmt.Sprintf("Failed to find flag %q and mark it as being required in a flag group", v)) } - if err := c.Flags().SetAnnotation(v, requiredAsGroup, append(f.Annotations[requiredAsGroup], strings.Join(flagNames, " "))); err != nil { + if err := c.Flags().SetAnnotation(v, requiredAsGroupAnnotation, append(f.Annotations[requiredAsGroupAnnotation], strings.Join(flagNames, " "))); err != nil { // Only errs if the flag isn't found. panic(err) } @@ -53,7 +53,7 @@ func (c *Command) MarkFlagsOneRequired(flagNames ...string) { if f == nil { panic(fmt.Sprintf("Failed to find flag %q and mark it as being in a one-required flag group", v)) } - if err := c.Flags().SetAnnotation(v, oneRequired, append(f.Annotations[oneRequired], strings.Join(flagNames, " "))); err != nil { + if err := c.Flags().SetAnnotation(v, oneRequiredAnnotation, append(f.Annotations[oneRequiredAnnotation], strings.Join(flagNames, " "))); err != nil { // Only errs if the flag isn't found. panic(err) } @@ -70,7 +70,7 @@ func (c *Command) MarkFlagsMutuallyExclusive(flagNames ...string) { panic(fmt.Sprintf("Failed to find flag %q and mark it as being in a mutually exclusive flag group", v)) } // Each time this is called is a single new entry; this allows it to be a member of multiple groups if needed. - if err := c.Flags().SetAnnotation(v, mutuallyExclusive, append(f.Annotations[mutuallyExclusive], strings.Join(flagNames, " "))); err != nil { + if err := c.Flags().SetAnnotation(v, mutuallyExclusiveAnnotation, append(f.Annotations[mutuallyExclusiveAnnotation], strings.Join(flagNames, " "))); err != nil { panic(err) } } @@ -91,9 +91,9 @@ func (c *Command) ValidateFlagGroups() error { oneRequiredGroupStatus := map[string]map[string]bool{} mutuallyExclusiveGroupStatus := map[string]map[string]bool{} flags.VisitAll(func(pflag *flag.Flag) { - processFlagForGroupAnnotation(flags, pflag, requiredAsGroup, groupStatus) - processFlagForGroupAnnotation(flags, pflag, oneRequired, oneRequiredGroupStatus) - processFlagForGroupAnnotation(flags, pflag, mutuallyExclusive, mutuallyExclusiveGroupStatus) + processFlagForGroupAnnotation(flags, pflag, requiredAsGroupAnnotation, groupStatus) + processFlagForGroupAnnotation(flags, pflag, oneRequiredAnnotation, oneRequiredGroupStatus) + processFlagForGroupAnnotation(flags, pflag, mutuallyExclusiveAnnotation, mutuallyExclusiveGroupStatus) }) if err := validateRequiredFlagGroups(groupStatus); err != nil { @@ -232,9 +232,9 @@ func (c *Command) enforceFlagGroupsForCompletion() { oneRequiredGroupStatus := map[string]map[string]bool{} mutuallyExclusiveGroupStatus := map[string]map[string]bool{} c.Flags().VisitAll(func(pflag *flag.Flag) { - processFlagForGroupAnnotation(flags, pflag, requiredAsGroup, groupStatus) - processFlagForGroupAnnotation(flags, pflag, oneRequired, oneRequiredGroupStatus) - processFlagForGroupAnnotation(flags, pflag, mutuallyExclusive, mutuallyExclusiveGroupStatus) + processFlagForGroupAnnotation(flags, pflag, requiredAsGroupAnnotation, groupStatus) + processFlagForGroupAnnotation(flags, pflag, oneRequiredAnnotation, oneRequiredGroupStatus) + processFlagForGroupAnnotation(flags, pflag, mutuallyExclusiveAnnotation, mutuallyExclusiveGroupStatus) }) // If a flag that is part of a group is present, we make all the other flags From 8003b74a10ef0d0d84fe3c408d3939d86fdeb210 Mon Sep 17 00:00:00 2001 From: Nir Soffer Date: Sun, 19 May 2024 05:12:02 +0300 Subject: [PATCH 45/45] Remove fully inactivated linters (#2148) * Remove fully inactivated linters Currently golangci-lint fails with these errors: ERRO [linters_context] golint: This linter is fully inactivated: it will not produce any reports. ERRO [linters_context] interfacer: This linter is fully inactivated: it will not produce any reports. ERRO [linters_context] maligned: This linter is fully inactivated: it will not produce any reports. I could not find any docs explaining what "fully inactivated" mean, but based this PR[1] it seems that these linters do nothing now. Removing the linters fixes this issue without changing linting, as they did not produce any report. Looking in the linters docs[2] I did not find a replacement for "interfacer" and "malinged" linters. "stylecheck" seems to be a replacement for "golint", but we need to fix the code to enable it. [1] https://github.com/golangci/golangci-lint/pull/4436 [2] https://golangci-lint.run/usage/linters/ * Add stylecheck linter, replacement for golint This revealed 2 capitalized error messages. https://golangci-lint.run/usage/linters/#stylecheck --- .golangci.yml | 5 +---- command.go | 2 +- completions.go | 2 +- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 22ae622..7b6d302 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -33,16 +33,13 @@ linters: #- gocyclo #- gofmt - goimports - - golint #- gomnd #- goprintffuncname #- gosec - gosimple - govet - ineffassign - - interfacer #- lll - - maligned - megacheck #- misspell #- nakedret @@ -52,7 +49,7 @@ linters: #- scopelint #- staticcheck #- structcheck ! deprecated since v1.49.0; replaced by 'unused' - #- stylecheck + - stylecheck #- typecheck - unconvert #- unparam diff --git a/command.go b/command.go index b6f8f4b..b31e22c 100644 --- a/command.go +++ b/command.go @@ -875,7 +875,7 @@ func (c *Command) ArgsLenAtDash() int { func (c *Command) execute(a []string) (err error) { if c == nil { - return fmt.Errorf("Called Execute() on a nil Command") + return fmt.Errorf("called Execute() on a nil Command") } if len(c.Deprecated) > 0 { diff --git a/completions.go b/completions.go index ad7b6d0..c0c08b0 100644 --- a/completions.go +++ b/completions.go @@ -298,7 +298,7 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi } if err != nil { // Unable to find the real command. E.g., someInvalidCmd - return c, []string{}, ShellCompDirectiveDefault, fmt.Errorf("Unable to find a command for arguments: %v", trimmedArgs) + return c, []string{}, ShellCompDirectiveDefault, fmt.Errorf("unable to find a command for arguments: %v", trimmedArgs) } finalCmd.ctx = c.ctx