Merge branch 'spf13:main' into patch-1

This commit is contained in:
Mavaddat Javid 2023-10-30 11:03:27 -04:00 committed by GitHub
commit 8b4835d0a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 631 additions and 144 deletions

View file

@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- run: >- - run: >-
docker run docker run
@ -39,17 +39,17 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-go@v3 - uses: actions/setup-go@v4
with: with:
go-version: '^1.20' go-version: '^1.21'
check-latest: true check-latest: true
cache: true cache: true
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: golangci/golangci-lint-action@v3.5.0 - uses: golangci/golangci-lint-action@v3.7.0
with: with:
version: latest version: latest
args: --verbose args: --verbose
@ -67,13 +67,14 @@ jobs:
- 18 - 18
- 19 - 19
- 20 - 20
- 21
name: '${{ matrix.platform }} | 1.${{ matrix.go }}.x' name: '${{ matrix.platform }} | 1.${{ matrix.go }}.x'
runs-on: ${{ matrix.platform }}-latest runs-on: ${{ matrix.platform }}-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/setup-go@v3 - uses: actions/setup-go@v4
with: with:
go-version: 1.${{ matrix.go }}.x go-version: 1.${{ matrix.go }}.x
cache: true cache: true
@ -107,7 +108,7 @@ jobs:
unzip unzip
mingw-w64-x86_64-go mingw-w64-x86_64-go
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: actions/cache@v3 - uses: actions/cache@v3
with: with:

View file

@ -19,7 +19,7 @@ linters:
disable-all: true disable-all: true
enable: enable:
#- bodyclose #- bodyclose
- deadcode # - deadcode ! deprecated since v1.49.0; replaced by 'unused'
#- depguard #- depguard
#- dogsled #- dogsled
#- dupl #- dupl
@ -51,12 +51,12 @@ linters:
#- rowserrcheck #- rowserrcheck
#- scopelint #- scopelint
#- staticcheck #- staticcheck
- structcheck #- structcheck ! deprecated since v1.49.0; replaced by 'unused'
#- stylecheck #- stylecheck
#- typecheck #- typecheck
- unconvert - unconvert
#- unparam #- unparam
#- unused - unused
- varcheck # - varcheck ! deprecated since v1.49.0; replaced by 'unused'
#- whitespace #- whitespace
fast: false fast: false

View file

@ -4,7 +4,7 @@ Cobra is a library for creating powerful modern CLI applications.
Cobra is used in many Go projects such as [Kubernetes](https://kubernetes.io/), Cobra is used in many Go projects such as [Kubernetes](https://kubernetes.io/),
[Hugo](https://gohugo.io), and [GitHub CLI](https://github.com/cli/cli) to [Hugo](https://gohugo.io), and [GitHub CLI](https://github.com/cli/cli) to
name a few. [This list](./projects_using_cobra.md) contains a more extensive list of projects using Cobra. name a few. [This list](site/content/projects_using_cobra.md) contains a more extensive list of projects using Cobra.
[![](https://img.shields.io/github/actions/workflow/status/spf13/cobra/test.yml?branch=main&longCache=true&label=Test&logo=github%20actions&logoColor=fff)](https://github.com/spf13/cobra/actions?query=workflow%3ATest) [![](https://img.shields.io/github/actions/workflow/status/spf13/cobra/test.yml?branch=main&longCache=true&label=Test&logo=github%20actions&logoColor=fff)](https://github.com/spf13/cobra/actions?query=workflow%3ATest)
[![Go Reference](https://pkg.go.dev/badge/github.com/spf13/cobra.svg)](https://pkg.go.dev/github.com/spf13/cobra) [![Go Reference](https://pkg.go.dev/badge/github.com/spf13/cobra.svg)](https://pkg.go.dev/github.com/spf13/cobra)
@ -105,8 +105,8 @@ go install github.com/spf13/cobra-cli@latest
For complete details on using the Cobra-CLI generator, please read [The Cobra Generator README](https://github.com/spf13/cobra-cli/blob/main/README.md) For complete details on using the Cobra-CLI generator, please read [The Cobra Generator README](https://github.com/spf13/cobra-cli/blob/main/README.md)
For complete details on using the Cobra library, please read the [The Cobra User Guide](user_guide.md). For complete details on using the Cobra library, please read the [The Cobra User Guide](site/content/user_guide.md).
# License # License
Cobra is released under the Apache 2.0 license. See [LICENSE.txt](https://github.com/spf13/cobra/blob/master/LICENSE.txt) Cobra is released under the Apache 2.0 license. See [LICENSE.txt](LICENSE.txt)

View file

@ -17,6 +17,7 @@ package cobra
import ( import (
"fmt" "fmt"
"os" "os"
"regexp"
"strings" "strings"
) )
@ -29,6 +30,8 @@ const (
activeHelpGlobalDisable = "0" activeHelpGlobalDisable = "0"
) )
var activeHelpEnvVarPrefixSubstRegexp = regexp.MustCompile(`[^A-Z0-9_]`)
// AppendActiveHelp adds the specified string to the specified array to be used as ActiveHelp. // 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 // Such strings will be processed by the completion script and will be shown as ActiveHelp
// to the user. // to the user.
@ -42,7 +45,7 @@ func AppendActiveHelp(compArray []string, activeHelpStr string) []string {
// GetActiveHelpConfig returns the value of the ActiveHelp environment variable // GetActiveHelpConfig returns the value of the ActiveHelp environment variable
// <PROGRAM>_ACTIVE_HELP where <PROGRAM> is the name of the root command in upper // <PROGRAM>_ACTIVE_HELP where <PROGRAM> 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 // It will always return "0" if the global environment variable COBRA_ACTIVE_HELP
// is set to "0". // is set to "0".
func GetActiveHelpConfig(cmd *Command) string { func GetActiveHelpConfig(cmd *Command) string {
@ -55,9 +58,10 @@ func GetActiveHelpConfig(cmd *Command) string {
// activeHelpEnvVar returns the name of the program-specific ActiveHelp environment // activeHelpEnvVar returns the name of the program-specific ActiveHelp environment
// variable. It has the format <PROGRAM>_ACTIVE_HELP where <PROGRAM> is the name of the // variable. It has the format <PROGRAM>_ACTIVE_HELP where <PROGRAM> 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 { func activeHelpEnvVar(name string) string {
// This format should not be changed: users will be using it explicitly. // This format should not be changed: users will be using it explicitly.
activeHelpEnvVar := strings.ToUpper(fmt.Sprintf("%s%s", name, activeHelpEnvVarSuffix)) activeHelpEnvVar := strings.ToUpper(fmt.Sprintf("%s%s", name, activeHelpEnvVarSuffix))
return strings.ReplaceAll(activeHelpEnvVar, "-", "_") activeHelpEnvVar = activeHelpEnvVarPrefixSubstRegexp.ReplaceAllString(activeHelpEnvVar, "_")
return activeHelpEnvVar
} }

View file

@ -43,9 +43,10 @@ var initializers []func()
var finalizers []func() var finalizers []func()
const ( const (
defaultPrefixMatching = false defaultPrefixMatching = false
defaultCommandSorting = true defaultCommandSorting = true
defaultCaseInsensitive = false defaultCaseInsensitive = false
defaultTraverseRunHooks = false
) )
// EnablePrefixMatching allows setting automatic prefix matching. Automatic prefix matching can be a dangerous thing // 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) // EnableCaseInsensitive allows case-insensitive commands names. (case sensitive by default)
var EnableCaseInsensitive = defaultCaseInsensitive 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 // MousetrapHelpText enables an information splash screen on Windows
// if the CLI is started from explorer.exe. // if the CLI is started from explorer.exe.
// To disable the mousetrap, just set this variable to blank string (""). // To disable the mousetrap, just set this variable to blank string ("").

View file

@ -115,6 +115,8 @@ type Command struct {
// * PostRun() // * PostRun()
// * PersistentPostRun() // * PersistentPostRun()
// All functions get the same args, the arguments after the command name. // 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: children of this command will inherit and execute.
PersistentPreRun func(cmd *Command, args []string) PersistentPreRun func(cmd *Command, args []string)
@ -181,6 +183,9 @@ type Command struct {
// versionTemplate is the version template defined by user. // versionTemplate is the version template defined by user.
versionTemplate string 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 is a reader defined by the user that replaces stdin
inReader io.Reader inReader io.Reader
// outWriter is a writer defined by the user that replaces stdout // outWriter is a writer defined by the user that replaces stdout
@ -346,6 +351,11 @@ func (c *Command) SetVersionTemplate(s string) {
c.versionTemplate = s 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. // SetGlobalNormalizationFunc sets a normalization function to all flag sets and also to child commands.
// The user should not have a cyclic dependency on commands. // The user should not have a cyclic dependency on commands.
func (c *Command) SetGlobalNormalizationFunc(n func(f *flag.FlagSet, name string) flag.NormalizedName) { func (c *Command) SetGlobalNormalizationFunc(n func(f *flag.FlagSet, name string) flag.NormalizedName) {
@ -595,6 +605,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 { func hasNoOptDefVal(name string, fs *flag.FlagSet) bool {
flag := fs.Lookup(name) flag := fs.Lookup(name)
if flag == nil { if flag == nil {
@ -752,7 +774,9 @@ func (c *Command) findNext(next string) *Command {
} }
if len(matches) == 1 { 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 return nil
@ -910,15 +934,31 @@ func (c *Command) execute(a []string) (err error) {
return err return err
} }
parents := make([]*Command, 0, 5)
for p := c; p != nil; p = p.Parent() { 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 p.PersistentPreRunE != nil {
if err := p.PersistentPreRunE(c, argWoFlags); err != nil { if err := p.PersistentPreRunE(c, argWoFlags); err != nil {
return err return err
} }
break if !EnableTraverseRunHooks {
break
}
} else if p.PersistentPreRun != nil { } else if p.PersistentPreRun != nil {
p.PersistentPreRun(c, argWoFlags) p.PersistentPreRun(c, argWoFlags)
break if !EnableTraverseRunHooks {
break
}
} }
} }
if c.PreRunE != nil { if c.PreRunE != nil {
@ -955,10 +995,14 @@ func (c *Command) execute(a []string) (err error) {
if err := p.PersistentPostRunE(c, argWoFlags); err != nil { if err := p.PersistentPostRunE(c, argWoFlags); err != nil {
return err return err
} }
break if !EnableTraverseRunHooks {
break
}
} else if p.PersistentPostRun != nil { } else if p.PersistentPostRun != nil {
p.PersistentPostRun(c, argWoFlags) p.PersistentPostRun(c, argWoFlags)
break if !EnableTraverseRunHooks {
break
}
} }
} }
@ -1048,7 +1092,7 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
c = cmd c = cmd
} }
if !c.SilenceErrors { if !c.SilenceErrors {
c.PrintErrln("Error:", err.Error()) c.PrintErrln(c.ErrPrefix(), err.Error())
c.PrintErrf("Run '%v --help' for usage.\n", c.CommandPath()) c.PrintErrf("Run '%v --help' for usage.\n", c.CommandPath())
} }
return c, err return c, err
@ -1077,7 +1121,7 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
// If root command has SilenceErrors flagged, // If root command has SilenceErrors flagged,
// all subcommands should respect it // all subcommands should respect it
if !cmd.SilenceErrors && !c.SilenceErrors { if !cmd.SilenceErrors && !c.SilenceErrors {
c.PrintErrln("Error:", err.Error()) c.PrintErrln(cmd.ErrPrefix(), err.Error())
} }
// If root command has SilenceUsage flagged, // If root command has SilenceUsage flagged,
@ -1402,6 +1446,7 @@ func (c *Command) UseLine() string {
// DebugFlags used to determine which flags have been assigned to which commands // DebugFlags used to determine which flags have been assigned to which commands
// and which persist. // and which persist.
// nolint:goconst
func (c *Command) DebugFlags() { func (c *Command) DebugFlags() {
c.Println("DebugFlags called on", c.Name()) c.Println("DebugFlags called on", c.Name())
var debugflags func(*Command) var debugflags func(*Command)

View file

@ -438,7 +438,7 @@ func TestFlagLong(t *testing.T) {
output, err := executeCommand(c, "--intf=7", "--sf=abc", "one", "--", "two") output, err := executeCommand(c, "--intf=7", "--sf=abc", "one", "--", "two")
if output != "" { if output != "" {
t.Errorf("Unexpected output: %v", err) t.Errorf("Unexpected output: %v", output)
} }
if err != nil { if err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
@ -475,7 +475,7 @@ func TestFlagShort(t *testing.T) {
output, err := executeCommand(c, "-i", "7", "-sabc", "one", "two") output, err := executeCommand(c, "-i", "7", "-sabc", "one", "two")
if output != "" { if output != "" {
t.Errorf("Unexpected output: %v", err) t.Errorf("Unexpected output: %v", output)
} }
if err != nil { if err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
@ -504,7 +504,7 @@ func TestChildFlag(t *testing.T) {
output, err := executeCommand(rootCmd, "child", "-i7") output, err := executeCommand(rootCmd, "child", "-i7")
if output != "" { if output != "" {
t.Errorf("Unexpected output: %v", err) t.Errorf("Unexpected output: %v", output)
} }
if err != nil { if err != nil {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
@ -1099,6 +1099,39 @@ func TestShorthandVersionTemplate(t *testing.T) {
checkStringContains(t, output, "customized version: 1.0.0") 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) { func TestVersionFlagExecutedOnSubcommand(t *testing.T) {
rootCmd := &Command{Use: "root", Version: "1.0.0"} rootCmd := &Command{Use: "root", Version: "1.0.0"}
rootCmd.AddCommand(&Command{Use: "sub", Run: emptyRun}) rootCmd.AddCommand(&Command{Use: "sub", Run: emptyRun})
@ -1497,57 +1530,73 @@ func TestHooks(t *testing.T) {
} }
func TestPersistentHooks(t *testing.T) { func TestPersistentHooks(t *testing.T) {
var ( EnableTraverseRunHooks = true
parentPersPreArgs string testPersistentHooks(t, []string{
parentPreArgs string "parent PersistentPreRun",
parentRunArgs string "child PersistentPreRun",
parentPostArgs string "child PreRun",
parentPersPostArgs string "child Run",
) "child PostRun",
"child PersistentPostRun",
"parent PersistentPostRun",
})
var ( EnableTraverseRunHooks = false
childPersPreArgs string testPersistentHooks(t, []string{
childPreArgs string "child PersistentPreRun",
childRunArgs string "child PreRun",
childPostArgs string "child Run",
childPersPostArgs string "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{ parentCmd := &Command{
Use: "parent", Use: "parent",
PersistentPreRun: func(_ *Command, args []string) { PersistentPreRun: func(_ *Command, args []string) {
parentPersPreArgs = strings.Join(args, " ") validateHook(args, "parent PersistentPreRun")
}, },
PreRun: func(_ *Command, args []string) { PreRun: func(_ *Command, args []string) {
parentPreArgs = strings.Join(args, " ") validateHook(args, "parent PreRun")
}, },
Run: func(_ *Command, args []string) { Run: func(_ *Command, args []string) {
parentRunArgs = strings.Join(args, " ") validateHook(args, "parent Run")
}, },
PostRun: func(_ *Command, args []string) { PostRun: func(_ *Command, args []string) {
parentPostArgs = strings.Join(args, " ") validateHook(args, "parent PostRun")
}, },
PersistentPostRun: func(_ *Command, args []string) { PersistentPostRun: func(_ *Command, args []string) {
parentPersPostArgs = strings.Join(args, " ") validateHook(args, "parent PersistentPostRun")
}, },
} }
childCmd := &Command{ childCmd := &Command{
Use: "child", Use: "child",
PersistentPreRun: func(_ *Command, args []string) { PersistentPreRun: func(_ *Command, args []string) {
childPersPreArgs = strings.Join(args, " ") validateHook(args, "child PersistentPreRun")
}, },
PreRun: func(_ *Command, args []string) { PreRun: func(_ *Command, args []string) {
childPreArgs = strings.Join(args, " ") validateHook(args, "child PreRun")
}, },
Run: func(_ *Command, args []string) { Run: func(_ *Command, args []string) {
childRunArgs = strings.Join(args, " ") validateHook(args, "child Run")
}, },
PostRun: func(_ *Command, args []string) { PostRun: func(_ *Command, args []string) {
childPostArgs = strings.Join(args, " ") validateHook(args, "child PostRun")
}, },
PersistentPostRun: func(_ *Command, args []string) { PersistentPostRun: func(_ *Command, args []string) {
childPersPostArgs = strings.Join(args, " ") validateHook(args, "child PersistentPostRun")
}, },
} }
parentCmd.AddCommand(childCmd) parentCmd.AddCommand(childCmd)
@ -1560,41 +1609,13 @@ func TestPersistentHooks(t *testing.T) {
t.Errorf("Unexpected error: %v", err) t.Errorf("Unexpected error: %v", err)
} }
for _, v := range []struct { for idx, exp := range expectedHookRunOrder {
name string if len(hookRunOrder) > idx {
got string if act := hookRunOrder[idx]; act != exp {
}{ t.Errorf("Expected %q at %d, got %q", exp, idx, act)
// TODO: currently PersistentPreRun* defined in parent does not }
// run if the matching child subcommand has PersistentPreRun. } else {
// If the behavior changes (https://github.com/spf13/cobra/issues/252) t.Errorf("Expected %q at %d, got nothing", exp, idx)
// 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)
} }
} }
} }

View file

@ -145,6 +145,25 @@ func (c *Command) RegisterFlagCompletionFunc(flagName string, f func(cmd *Comman
return nil 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 // Returns a string listing the different directive enabled in the specified parameter
func (d ShellCompDirective) string() string { func (d ShellCompDirective) string() string {
var directives []string var directives []string
@ -283,9 +302,13 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
// These flags are normally added when `execute()` is called on `finalCmd`, // These flags are normally added when `execute()` is called on `finalCmd`,
// however, when doing completion, we don't call `finalCmd.execute()`. // however, when doing completion, we don't call `finalCmd.execute()`.
// Let's add the --help and --version flag ourselves. // Let's add the --help and --version flag ourselves but only if the finalCmd
finalCmd.InitDefaultHelpFlag() // has not disabled flag parsing; if flag parsing is disabled, it is up to the
finalCmd.InitDefaultVersionFlag() // 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. // 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 // This is important because if we are completing a flag value, we need to also
@ -389,6 +412,11 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi
finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) { finalCmd.InheritedFlags().VisitAll(func(flag *pflag.Flag) {
doCompleteFlags(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) { finalCmd.NonInheritedFlags().VisitAll(func(flag *pflag.Flag) {
doCompleteFlags(flag) doCompleteFlags(flag)
}) })

View file

@ -17,7 +17,9 @@ package cobra
import ( import (
"bytes" "bytes"
"context" "context"
"fmt"
"strings" "strings"
"sync"
"testing" "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) { func TestFlagCompletionInGoWithDesc(t *testing.T) {
rootCmd := &Command{ rootCmd := &Command{
Use: "root", Use: "root",
@ -2622,8 +2732,6 @@ func TestCompleteWithDisableFlagParsing(t *testing.T) {
expected := strings.Join([]string{ expected := strings.Join([]string{
"--persistent", "--persistent",
"-p", "-p",
"--help",
"-h",
"--nonPersistent", "--nonPersistent",
"-n", "-n",
"--flag", "--flag",
@ -2830,6 +2938,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) { func TestCompletionForMutuallyExclusiveFlags(t *testing.T) {
getCmd := func() *Command { getCmd := func() *Command {
rootCmd := &Command{ rootCmd := &Command{
@ -2955,8 +3161,26 @@ func TestCompletionCobraFlags(t *testing.T) {
return []string{"extra3"}, ShellCompDirectiveNoFileComp 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.Flags().Bool("bool", false, "A bool flag")
_ = childCmd.MarkFlagRequired("bool") _ = childCmd.MarkFlagRequired("bool")
@ -2968,6 +3192,10 @@ func TestCompletionCobraFlags(t *testing.T) {
// Have a command that only adds its own -v flag // Have a command that only adds its own -v flag
_ = childCmd3.Flags().BoolP("verbose", "v", false, "Not a version 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 return rootCmd
} }
@ -3098,6 +3326,26 @@ func TestCompletionCobraFlags(t *testing.T) {
":4", ":4",
"Completion ended with directive: ShellCompDirectiveNoFileComp", ""}, "\n"), "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 { for _, tc := range testcases {

View file

@ -150,7 +150,7 @@ func TestGenManSeeAlso(t *testing.T) {
} }
} }
func TestManPrintFlagsHidesShortDeperecated(t *testing.T) { func TestManPrintFlagsHidesShortDeprecated(t *testing.T) {
c := &cobra.Command{} c := &cobra.Command{}
c.Flags().StringP("foo", "f", "default", "Foo flag") c.Flags().StringP("foo", "f", "default", "Foo flag")
assertNoErr(t, c.Flags().MarkShorthandDeprecated("foo", "don't use it no more")) assertNoErr(t, c.Flags().MarkShorthandDeprecated("foo", "don't use it no more"))

View file

@ -27,6 +27,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
const markdownExtension = ".md"
func printOptions(buf *bytes.Buffer, cmd *cobra.Command, name string) error { func printOptions(buf *bytes.Buffer, cmd *cobra.Command, name string) error {
flags := cmd.NonInheritedFlags() flags := cmd.NonInheritedFlags()
flags.SetOutput(buf) flags.SetOutput(buf)
@ -83,7 +85,7 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string)
if cmd.HasParent() { if cmd.HasParent() {
parent := cmd.Parent() parent := cmd.Parent()
pname := parent.CommandPath() pname := parent.CommandPath()
link := pname + ".md" link := pname + markdownExtension
link = strings.ReplaceAll(link, " ", "_") link = strings.ReplaceAll(link, " ", "_")
buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", pname, linkHandler(link), parent.Short)) buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", pname, linkHandler(link), parent.Short))
cmd.VisitParents(func(c *cobra.Command) { cmd.VisitParents(func(c *cobra.Command) {
@ -101,7 +103,7 @@ func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string)
continue continue
} }
cname := name + " " + child.Name() cname := name + " " + child.Name()
link := cname + ".md" link := cname + markdownExtension
link = strings.ReplaceAll(link, " ", "_") link = strings.ReplaceAll(link, " ", "_")
buf.WriteString(fmt.Sprintf("* [%s](%s)\t - %s\n", cname, linkHandler(link), child.Short)) 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) filename := filepath.Join(dir, basename)
f, err := os.Create(filename) f, err := os.Create(filename)
if err != nil { if err != nil {

View file

@ -1,4 +0,0 @@
## Generating Fish Completions For Your cobra.Command
Please refer to [Shell Completions](shell_completions.md) for details.

View file

@ -24,6 +24,7 @@ import (
const ( const (
requiredAsGroup = "cobra_annotation_required_if_others_set" requiredAsGroup = "cobra_annotation_required_if_others_set"
oneRequired = "cobra_annotation_one_required"
mutuallyExclusive = "cobra_annotation_mutually_exclusive" 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 // 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. // if the command is invoked with more than one flag from the given set of flags.
func (c *Command) MarkFlagsMutuallyExclusive(flagNames ...string) { 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. // first error encountered.
func (c *Command) ValidateFlagGroups() error { func (c *Command) ValidateFlagGroups() error {
if c.DisableFlagParsing { if c.DisableFlagParsing {
@ -71,15 +88,20 @@ func (c *Command) ValidateFlagGroups() error {
// groupStatus format is the list of flags as a unique ID, // 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. // then a map of each flag name and whether it is set or not.
groupStatus := map[string]map[string]bool{} groupStatus := map[string]map[string]bool{}
oneRequiredGroupStatus := map[string]map[string]bool{}
mutuallyExclusiveGroupStatus := map[string]map[string]bool{} mutuallyExclusiveGroupStatus := map[string]map[string]bool{}
flags.VisitAll(func(pflag *flag.Flag) { flags.VisitAll(func(pflag *flag.Flag) {
processFlagForGroupAnnotation(flags, pflag, requiredAsGroup, groupStatus) processFlagForGroupAnnotation(flags, pflag, requiredAsGroup, groupStatus)
processFlagForGroupAnnotation(flags, pflag, oneRequired, oneRequiredGroupStatus)
processFlagForGroupAnnotation(flags, pflag, mutuallyExclusive, mutuallyExclusiveGroupStatus) processFlagForGroupAnnotation(flags, pflag, mutuallyExclusive, mutuallyExclusiveGroupStatus)
}) })
if err := validateRequiredFlagGroups(groupStatus); err != nil { if err := validateRequiredFlagGroups(groupStatus); err != nil {
return err return err
} }
if err := validateOneRequiredFlagGroups(oneRequiredGroupStatus); err != nil {
return err
}
if err := validateExclusiveFlagGroups(mutuallyExclusiveGroupStatus); err != nil { if err := validateExclusiveFlagGroups(mutuallyExclusiveGroupStatus); err != nil {
return err return err
} }
@ -142,6 +164,27 @@ func validateRequiredFlagGroups(data map[string]map[string]bool) error {
return nil 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 { func validateExclusiveFlagGroups(data map[string]map[string]bool) error {
keys := sortedKeys(data) keys := sortedKeys(data)
for _, flagList := range keys { for _, flagList := range keys {
@ -176,6 +219,7 @@ func sortedKeys(m map[string]map[string]bool) []string {
// enforceFlagGroupsForCompletion will do the following: // enforceFlagGroupsForCompletion will do the following:
// - when a flag in a group is present, other flags in the group will be marked required // - 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 // - 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 // This allows the standard completion logic to behave appropriately for flag groups
func (c *Command) enforceFlagGroupsForCompletion() { func (c *Command) enforceFlagGroupsForCompletion() {
@ -185,9 +229,11 @@ func (c *Command) enforceFlagGroupsForCompletion() {
flags := c.Flags() flags := c.Flags()
groupStatus := map[string]map[string]bool{} groupStatus := map[string]map[string]bool{}
oneRequiredGroupStatus := map[string]map[string]bool{}
mutuallyExclusiveGroupStatus := map[string]map[string]bool{} mutuallyExclusiveGroupStatus := map[string]map[string]bool{}
c.Flags().VisitAll(func(pflag *flag.Flag) { c.Flags().VisitAll(func(pflag *flag.Flag) {
processFlagForGroupAnnotation(flags, pflag, requiredAsGroup, groupStatus) processFlagForGroupAnnotation(flags, pflag, requiredAsGroup, groupStatus)
processFlagForGroupAnnotation(flags, pflag, oneRequired, oneRequiredGroupStatus)
processFlagForGroupAnnotation(flags, pflag, mutuallyExclusive, mutuallyExclusiveGroupStatus) 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 // 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 // flags of that group so the shell completion does not suggest them
for flagList, flagnameAndStatus := range mutuallyExclusiveGroupStatus { for flagList, flagnameAndStatus := range mutuallyExclusiveGroupStatus {

View file

@ -43,13 +43,15 @@ func TestValidateFlagGroups(t *testing.T) {
// Each test case uses a unique command from the function above. // Each test case uses a unique command from the function above.
testcases := []struct { testcases := []struct {
desc string desc string
flagGroupsRequired []string flagGroupsRequired []string
flagGroupsExclusive []string flagGroupsOneRequired []string
subCmdFlagGroupsRequired []string flagGroupsExclusive []string
subCmdFlagGroupsExclusive []string subCmdFlagGroupsRequired []string
args []string subCmdFlagGroupsOneRequired []string
expectErr string subCmdFlagGroupsExclusive []string
args []string
expectErr string
}{ }{
{ {
desc: "No flags no problem", desc: "No flags no problem",
@ -62,6 +64,11 @@ func TestValidateFlagGroups(t *testing.T) {
flagGroupsRequired: []string{"a b c"}, flagGroupsRequired: []string{"a b c"},
args: []string{"--a=foo"}, args: []string{"--a=foo"},
expectErr: "if any flags in the group [a b c] are set they must all be set; missing [b c]", 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", desc: "Exclusive flag group not satisfied",
flagGroupsExclusive: []string{"a b c"}, flagGroupsExclusive: []string{"a b c"},
@ -72,6 +79,11 @@ func TestValidateFlagGroups(t *testing.T) {
flagGroupsRequired: []string{"a b c", "a d"}, flagGroupsRequired: []string{"a b c", "a d"},
args: []string{"--c=foo", "--d=foo"}, 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]`, 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", desc: "Multiple exclusive flag group not satisfied returns first error",
flagGroupsExclusive: []string{"a b c", "a d"}, flagGroupsExclusive: []string{"a b c", "a d"},
@ -82,32 +94,57 @@ func TestValidateFlagGroups(t *testing.T) {
flagGroupsRequired: []string{"a d", "a b", "a c"}, flagGroupsRequired: []string{"a d", "a b", "a c"},
args: []string{"--a=foo"}, args: []string{"--a=foo"},
expectErr: `if any flags in the group [a b] are set they must all be set; missing [b]`, 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", desc: "Validation of exclusive groups occurs on groups in sorted order",
flagGroupsExclusive: []string{"a d", "a b", "a c"}, flagGroupsExclusive: []string{"a d", "a b", "a c"},
args: []string{"--a=foo", "--b=foo", "--c=foo"}, 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`, 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"}, flagGroupsRequired: []string{"a e", "e f"},
flagGroupsExclusive: []string{"f g"}, flagGroupsExclusive: []string{"f g"},
args: []string{"--a=foo", "--f=foo", "--g=foo"}, 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]`, 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"}, flagGroupsRequired: []string{"a e", "e f"},
flagGroupsExclusive: []string{"f g"}, flagGroupsExclusive: []string{"f g"},
args: []string{"--a=foo", "--e=foo", "--f=foo", "--g=foo"}, 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`, 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"}, flagGroupsRequired: []string{"a e", "e f"},
flagGroupsExclusive: []string{"f g"}, flagGroupsExclusive: []string{"f g"},
args: []string{"--a=foo", "--e=foo", "--f=foo"}, 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", desc: "Subcmds can use required groups using inherited flags",
subCmdFlagGroupsRequired: []string{"e subonly"}, subCmdFlagGroupsRequired: []string{"e subonly"},
args: []string{"subcmd", "--e=foo", "--subonly=foo"}, 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", desc: "Subcmds can use exclusive groups using inherited flags",
subCmdFlagGroupsExclusive: []string{"e subonly"}, subCmdFlagGroupsExclusive: []string{"e subonly"},
@ -130,12 +167,18 @@ func TestValidateFlagGroups(t *testing.T) {
for _, flagGroup := range tc.flagGroupsRequired { for _, flagGroup := range tc.flagGroupsRequired {
c.MarkFlagsRequiredTogether(strings.Split(flagGroup, " ")...) c.MarkFlagsRequiredTogether(strings.Split(flagGroup, " ")...)
} }
for _, flagGroup := range tc.flagGroupsOneRequired {
c.MarkFlagsOneRequired(strings.Split(flagGroup, " ")...)
}
for _, flagGroup := range tc.flagGroupsExclusive { for _, flagGroup := range tc.flagGroupsExclusive {
c.MarkFlagsMutuallyExclusive(strings.Split(flagGroup, " ")...) c.MarkFlagsMutuallyExclusive(strings.Split(flagGroup, " ")...)
} }
for _, flagGroup := range tc.subCmdFlagGroupsRequired { for _, flagGroup := range tc.subCmdFlagGroupsRequired {
sub.MarkFlagsRequiredTogether(strings.Split(flagGroup, " ")...) sub.MarkFlagsRequiredTogether(strings.Split(flagGroup, " ")...)
} }
for _, flagGroup := range tc.subCmdFlagGroupsOneRequired {
sub.MarkFlagsOneRequired(strings.Split(flagGroup, " ")...)
}
for _, flagGroup := range tc.subCmdFlagGroupsExclusive { for _, flagGroup := range tc.subCmdFlagGroupsExclusive {
sub.MarkFlagsMutuallyExclusive(strings.Split(flagGroup, " ")...) sub.MarkFlagsMutuallyExclusive(strings.Split(flagGroup, " ")...)
} }

2
go.mod
View file

@ -3,7 +3,7 @@ module github.com/spf13/cobra
go 1.15 go 1.15
require ( 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/inconshreveable/mousetrap v1.1.0
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1

4
go.sum
View file

@ -1,5 +1,5 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=

View file

@ -1,3 +0,0 @@
# Generating PowerShell Completions For Your Own cobra.Command
Please refer to [Shell Completions](shell_completions.md#powershell-completions) for details.

View file

@ -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 The way to configure Active Help is to use the program's Active Help environment
variable. That variable is named `<PROGRAM>_ACTIVE_HELP` where `<PROGRAM>` is the name of your variable. That variable is named `<PROGRAM>_ACTIVE_HELP` where `<PROGRAM>` 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. 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 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. 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 `<PROGRAM>_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 `<PROGRAM>_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<ENTER> $ HELM_ACTIVE_HELP=1 bin/helm __complete install wordpress bitnami/h<ENTER>
bitnami/haproxy bitnami/haproxy

View file

@ -416,7 +416,7 @@ completion firstcommand secondcommand
### Bash legacy dynamic completions ### Bash legacy dynamic completions
For backward compatibility, Cobra still supports its bash legacy dynamic completion solution. For backward compatibility, Cobra still supports its bash legacy dynamic completion solution.
Please refer to [Bash Completions](bash_completions.md) for details. Please refer to [Bash Completions](bash.md) for details.
### Bash completion V2 ### Bash completion V2
@ -425,7 +425,7 @@ Cobra provides two versions for bash completion. The original bash completion (
A new V2 bash completion version is also available. This version can be used by calling `GenBashCompletionV2()` or A new V2 bash completion version is also available. This version can be used by calling `GenBashCompletionV2()` or
`GenBashCompletionFileV2()`. The V2 version does **not** support the legacy dynamic completion `GenBashCompletionFileV2()`. The V2 version does **not** support the legacy dynamic completion
(see [Bash Completions](bash_completions.md)) but instead works only with the Go dynamic completion (see [Bash Completions](bash.md)) but instead works only with the Go dynamic completion
solution described in this document. solution described in this document.
Unless your program already uses the legacy dynamic completion solution, it is recommended that you use the bash Unless your program already uses the legacy dynamic completion solution, it is recommended that you use the bash
completion V2 solution which provides the following extra features: completion V2 solution which provides the following extra features:
@ -482,7 +482,7 @@ search show status
### Zsh completions standardization ### Zsh completions standardization
Cobra 1.1 standardized its zsh completion support to align it with its other shell completions. Although the API was kept backward-compatible, some small changes in behavior were introduced. Cobra 1.1 standardized its zsh completion support to align it with its other shell completions. Although the API was kept backward-compatible, some small changes in behavior were introduced.
Please refer to [Zsh Completions](zsh_completions.md) for details. Please refer to [Zsh Completions](zsh.md) for details.
## fish completions ## fish completions

View file

@ -1,6 +1,6 @@
# Generating Bash Completions For Your cobra.Command # Generating Bash Completions For Your cobra.Command
Please refer to [Shell Completions](shell_completions.md) for details. Please refer to [Shell Completions](_index.md) for details.
## Bash legacy dynamic completions ## Bash legacy dynamic completions

View file

@ -0,0 +1,4 @@
## Generating Fish Completions For Your cobra.Command
Please refer to [Shell Completions](_index.md) for details.

View file

@ -0,0 +1,3 @@
# Generating PowerShell Completions For Your Own cobra.Command
Please refer to [Shell Completions](_index.md#powershell-completions) for details.

View file

@ -1,6 +1,6 @@
## Generating Zsh Completion For Your cobra.Command ## Generating Zsh Completion For Your cobra.Command
Please refer to [Shell Completions](shell_completions.md) for details. Please refer to [Shell Completions](_index.md) for details.
## Zsh completions standardization ## Zsh completions standardization

View file

@ -1,9 +1,9 @@
# Documentation generation # Documentation generation
- [Man page docs](./man_docs.md) - [Man page docs](man.md)
- [Markdown docs](./md_docs.md) - [Markdown docs](md.md)
- [Rest docs](./rest_docs.md) - [Rest docs](rest.md)
- [Yaml docs](./yaml_docs.md) - [Yaml docs](yaml.md)
## Options ## Options
### `DisableAutoGenTag` ### `DisableAutoGenTag`

View file

@ -29,8 +29,8 @@ func main() {
## Using the Cobra Generator ## Using the Cobra Generator
Cobra-CLI is its own program that will create your application and add any Cobra-CLI is its own program that will create your application and add any commands you want.
commands you want. It's the easiest way to incorporate Cobra into your application. It's the easiest way to incorporate Cobra into your application.
For complete details on using the Cobra generator, please refer to [The Cobra-CLI Generator README](https://github.com/spf13/cobra-cli/blob/main/README.md) For complete details on using the Cobra generator, please refer to [The Cobra-CLI Generator README](https://github.com/spf13/cobra-cli/blob/main/README.md)
@ -349,7 +349,16 @@ rootCmd.Flags().BoolVar(&ofYaml, "yaml", false, "Output in YAML")
rootCmd.MarkFlagsMutuallyExclusive("json", "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 - both local and persistent flags can be used
- **NOTE:** the group is only enforced on commands where every flag is defined - **NOTE:** the group is only enforced on commands where every flag is defined
- a flag may appear in multiple groups - a flag may appear in multiple groups
@ -587,9 +596,15 @@ Running an application with the '--version' flag will print the version to stdou
the version template. The template can be customized using the the version template. The template can be customized using the
`cmd.SetVersionTemplate(s string)` function. `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: <error contents>`.
The Prefix, `Error:` can be customized using the `cmd.SetErrPrefix(s string)` function.
## PreRun and PostRun Hooks ## 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` - `PersistentPreRun`
- `PreRun` - `PreRun`
@ -672,6 +687,10 @@ Inside subCmd PostRun with args: [arg1 arg2]
Inside subCmd PersistentPostRun 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 ## 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: 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:
@ -715,12 +734,17 @@ Run 'kubectl help' for usage.
## Generating documentation for your command ## Generating documentation for your command
Cobra can generate documentation based on subcommands, flags, etc. Read more about it in the [docs generation documentation](doc/README.md). Cobra can generate documentation based on subcommands, flags, etc.
Read more about it in the [docs generation documentation](docgen/_index.md).
## Generating shell completions ## Generating shell completions
Cobra can generate a shell-completion file for the following shells: bash, zsh, fish, PowerShell. If you add more information to your commands, these completions can be amazingly powerful and flexible. Read more about it in [Shell Completions](shell_completions.md). Cobra can generate a shell-completion file for the following shells: bash, zsh, fish, PowerShell.
If you add more information to your commands, these completions can be amazingly powerful and flexible.
Read more about it in [Shell Completions](completions/_index.md).
## Providing Active Help ## Providing Active Help
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). 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).