Compare commits

...

6 commits

Author SHA1 Message Date
Pieter Noordhuis a21c1ab28a
Merge 8e2c9596e1 into 0c72800b8d 2023-09-11 13:51:15 +05:30
Souma 0c72800b8d
Customizable error message prefix (#2023) 2023-09-08 13:29:06 -04:00
Nuno Adrego c5dacb3ea4
ci: test golang 1.21 (#2024) 2023-09-07 20:30:51 -04:00
Unai Martinez-Corral 285460dca6
command: temporarily disable G602 due to securego/gosec#1005 (#2022) 2023-08-29 10:04:59 -04:00
dependabot[bot] 4955da7c11
build(deps): bump golangci/golangci-lint-action from 3.6.0 to 3.7.0 (#2021) 2023-08-28 18:53:34 +00:00
Pieter Noordhuis 8e2c9596e1
Always propagate root context to child command
The context passed to the root command should propagate to its children
not only on the first execution but also subsequent calls.
Calling the same command multiple times is common when testing cobra applications.
2022-12-11 08:36:49 +01:00
4 changed files with 160 additions and 11 deletions

View file

@ -43,13 +43,13 @@ jobs:
- uses: actions/setup-go@v3
with:
go-version: '^1.20'
go-version: '^1.21'
check-latest: true
cache: true
- 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
@ -67,6 +67,7 @@ jobs:
- 18
- 19
- 20
- 21
name: '${{ matrix.platform }} | 1.${{ matrix.go }}.x'
runs-on: ${{ matrix.platform }}-latest
steps:

View file

@ -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 {
@ -752,7 +772,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
@ -1009,7 +1031,7 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
// Regardless of what command execute is called on, run on Root only
if c.HasParent() {
return c.Root().ExecuteC()
return c.Root().ExecuteContextC(c.ctx)
}
// windows hook
@ -1048,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
@ -1059,11 +1081,8 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
cmd.commandCalledAs.name = cmd.Name()
}
// We have to pass global context to children command
// if context is present on the parent command.
if cmd.ctx == nil {
cmd.ctx = c.ctx
}
// Pass context of root command to child command.
cmd.ctx = c.ctx
err = cmd.execute(flags)
if err != nil {
@ -1077,7 +1096,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,

View file

@ -232,6 +232,96 @@ func TestExecuteContextC(t *testing.T) {
}
}
// This tests that the context passed to the root command propagates to its children
// not only on the first execution but also subsequent calls.
// Calling the same command multiple times is common when testing cobra applications.
func TestExecuteContextMultiple(t *testing.T) {
var key string
// Define unique contexts so we can tell them apart below.
ctxs := []context.Context{
context.WithValue(context.Background(), &key, "1"),
context.WithValue(context.Background(), &key, "2"),
}
// Shared reference to the context in the current iteration.
var currentCtx context.Context
ctxRun := func(cmd *Command, args []string) {
if cmd.Context() != currentCtx {
t.Errorf("Command %q must have context with value %s", cmd.Use, currentCtx.Value(&key))
}
}
rootCmd := &Command{Use: "root", Run: ctxRun, PreRun: ctxRun}
childCmd := &Command{Use: "child", Run: ctxRun, PreRun: ctxRun}
granchildCmd := &Command{Use: "grandchild", Run: ctxRun, PreRun: ctxRun}
childCmd.AddCommand(granchildCmd)
rootCmd.AddCommand(childCmd)
for i := 0; i < 2; i++ {
currentCtx = ctxs[i]
if _, err := executeCommandWithContext(currentCtx, rootCmd, ""); err != nil {
t.Errorf("Root command must not fail: %+v", err)
}
if _, err := executeCommandWithContext(currentCtx, rootCmd, "child"); err != nil {
t.Errorf("Subcommand must not fail: %+v", err)
}
if _, err := executeCommandWithContext(currentCtx, rootCmd, "child", "grandchild"); err != nil {
t.Errorf("Command child must not fail: %+v", err)
}
}
}
// This tests that the context passed to a subcommand propagates to the root.
// If the entry point happens to be different from the root command, the
// context should still propagate throughout the execution.
func TestExecuteContextOnSubcommand(t *testing.T) {
var key string
// Define unique contexts so we can tell them apart below.
ctxs := []context.Context{
context.WithValue(context.Background(), &key, "1"),
context.WithValue(context.Background(), &key, "2"),
context.WithValue(context.Background(), &key, "3"),
}
// Shared reference to the context in the current iteration.
var currentCtx context.Context
ctxRun := func(cmd *Command, args []string) {
if cmd.Context() != currentCtx {
t.Errorf("Command %q must have context with value %s", cmd.Use, currentCtx.Value(&key))
}
}
rootCmd := &Command{Use: "root", Run: ctxRun, PreRun: ctxRun}
childCmd := &Command{Use: "child", Run: ctxRun, PreRun: ctxRun}
granchildCmd := &Command{Use: "grandchild", Run: ctxRun, PreRun: ctxRun}
childCmd.AddCommand(granchildCmd)
rootCmd.AddCommand(childCmd)
currentCtx = ctxs[0]
if _, err := executeCommandWithContext(currentCtx, rootCmd, ""); err != nil {
t.Errorf("Root command must not fail: %+v", err)
}
currentCtx = ctxs[1]
if _, err := executeCommandWithContext(currentCtx, childCmd, "child"); err != nil {
t.Errorf("Subcommand must not fail: %+v", err)
}
currentCtx = ctxs[2]
if _, err := executeCommandWithContext(currentCtx, granchildCmd, "child", "grandchild"); err != nil {
t.Errorf("Command child must not fail: %+v", err)
}
}
func TestExecute_NoContext(t *testing.T) {
run := func(cmd *Command, args []string) {
if cmd.Context() != context.Background() {
@ -1099,6 +1189,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})

View file

@ -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: <error contents>`.
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: