Skip to content

Conversation

eth-p
Copy link

@eth-p eth-p commented Apr 15, 2025

This pull request converts a bunch of fmt.Errorf errors into structs implementing the error interface.

Similar to #2178, the goal of this pull request is to make it easier for callers of a Cobra command to determine why a command failed without having to resort to regex matching or parsing the error message. This pull request goes a bit further, covering all types of CLI validation-related errors (with the exception of pflag-created ones) and adding methods to get the specific details of the errors.

Giving callers this info enables them to do new things such as:

  • Printing localized error messages.
  • Changing how the errors are displayed (e.g. pretty printing, colors).

The error structs added are:

  • InvalidArgCountError
  • InvalidArgValueError
  • UnknownSubcommandError
  • RequiredFlagError
  • FlagGroupError

Tests and documentation have been updated as well.

@CLAassistant
Copy link

CLAassistant commented Apr 15, 2025

CLA assistant check
All committers have signed the CLA.

@eth-p eth-p changed the title Use structs for CLI-validation errors returned by Cobra. feat: Use structs for CLI-validation errors returned by Cobra. Apr 15, 2025
errors.go Outdated
Comment on lines 33 to 48
if e.atMost == -1 && e.atLeast >= 0 { // MinimumNArgs
return fmt.Sprintf("requires at least %d arg(s), only received %d", e.atLeast, len(e.args))
}

if e.atLeast == -1 && e.atMost >= 0 { // MaximumNArgs
return fmt.Sprintf("accepts at most %d arg(s), received %d", e.atMost, len(e.args))
}

if e.atLeast == e.atMost && e.atLeast != -1 { // ExactArgs
return fmt.Sprintf("accepts %d arg(s), received %d", e.atLeast, len(e.args))
}

// RangeArgs
return fmt.Sprintf("accepts between %d and %d arg(s), received %d", e.atLeast, e.atMost, len(e.args))
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume -1 is the default value when they are not set.

It's not that obvious.

Maybe code could be clearer by inverting the logic

Suggested change
if e.atMost == -1 && e.atLeast >= 0 { // MinimumNArgs
return fmt.Sprintf("requires at least %d arg(s), only received %d", e.atLeast, len(e.args))
}
if e.atLeast == -1 && e.atMost >= 0 { // MaximumNArgs
return fmt.Sprintf("accepts at most %d arg(s), received %d", e.atMost, len(e.args))
}
if e.atLeast == e.atMost && e.atLeast != -1 { // ExactArgs
return fmt.Sprintf("accepts %d arg(s), received %d", e.atLeast, len(e.args))
}
// RangeArgs
return fmt.Sprintf("accepts between %d and %d arg(s), received %d", e.atLeast, e.atMost, len(e.args))
}
if e.atLeast == e.atMost && e.atLeast > 0 { // ExactArgs
return fmt.Sprintf("accepts %d arg(s), received %d", e.atLeast, len(e.args))
}
if e.atMost >= 0 && e.atLeast >= 0 { // RangeArgs
return fmt.Sprintf("accepts between %d and %d arg(s), received %d", e.atLeast, e.atMost, len(e.args))
}
if e.atLeast >= 0 { // MinimumNArgs
return fmt.Sprintf("requires at least %d arg(s), only received %d", e.atLeast, len(e.args))
}
// MaximumNArgs
return fmt.Sprintf("accepts at most %d arg(s), received %d", e.atMost, len(e.args))
}

This is pseudo code written on a phone, but I hope you get the idea

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's much better! Great suggestion, thanks!

errors.go Outdated
Comment on lines 117 to 120
func (e *UnknownSubcommandError) GetSuggestions() string {
return e.suggestions
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here an for other errors, I would have preferred if this method returned a []string

But it would require to rewrite findSuggestions, or at least to split by \n here, but it would be ugly and dirty

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's totally fair. I added another commit to split up findSuggestions into two separate functions, and I changed the error structs to hold suggestions []string and return the suggestions slice instead of the merged string.

The `findSuggestions` function keeps the logic for finding the
suggestions, and a new `helpTextForSuggestions` function joins
them together into a string.

This will allow error structs to return the suggestions as a slice,
rather than a string.
@eth-p eth-p force-pushed the error-structs branch from a980b4a to e9e4665 Compare May 6, 2025 03:03
@eth-p
Copy link
Author

eth-p commented May 6, 2025

Awesome, thanks for the review @ccoVeille!

I rebased the commits against main and applied your suggestions. In hindsight it might have been better to keep them as fixup commits for review purposes, but here's a quick TLDR on what changed since your previous review:

  • New commit to split up findSuggestions: 8dd7f63

  • Applied your suggestion to InvalidArgCountError.Error(). I did make a slight change, though:

    - if e.atLeast == e.atMost && e.atLeast > 0 { // ExactArgs
    + if e.atLeast == e.atMost && e.atLeast >= 0 { // ExactArgs

    My thoughts were that somebody might do ExactArgs(0), in which case we would want to display the message "accepts 0 arg(s), received N" instead of falling back to the next case and displaying "accepts between 0 and 0 arg(s), received N".

  • errors.go:

      type InvalidArgValueError struct {
      	cmd         *Command
      	arg         string
    - 	suggestions string
    + 	suggestions []string
      }
    
    - func (e *InvalidArgValueError) GetSuggestions() string {
    + func (e *InvalidArgValueError) GetSuggestions() []string {
      	return e.suggestions
      }
    
      func (e *InvalidArgValueError) Error() string {
    - 	return fmt.Sprintf("invalid argument %q for %q%s", e.arg, e.cmd.CommandPath(), e.suggestions)
    + 	return fmt.Sprintf("invalid argument %q for %q%s", e.arg, e.cmd.CommandPath(), helpTextForSuggestions(e.suggestions))
      }
    
      type UnknownSubcommandError struct {
      	cmd         *Command
      	subcmd      string
    - 	suggestions string
    + 	suggestions []string
      }
    
    - func (e *UnknownSubcommandError) GetSuggestions() string {
    + func (e *UnknownSubcommandError) GetSuggestions() []string {
      	return e.suggestions
      }
    
      func (e *UnknownSubcommandError) Error() string {
    - 	return fmt.Sprintf("unknown command %q for %q%s", e.subcmd, e.cmd.CommandPath(), e.suggestions)
    + 	return fmt.Sprintf("unknown command %q for %q%s", e.subcmd, e.cmd.CommandPath(), helpTextForSuggestions(e.suggestions))
      }
    
      func (e *FlagGroupError) Error() string {
      	switch e.flagGroupType {
      	case FlagsAreRequiredTogether:
      		return fmt.Sprintf("if any flags in the group [%v] are set they must all be set; missing %v", e.flagList, e.problemFlags)
      	case FlagsAreOneRequired:
      		return fmt.Sprintf("at least one of the flags in the group [%v] is required", e.flagList)
      	case FlagsAreMutuallyExclusive:
      		return fmt.Sprintf("if any flags in the group [%v] are set none of the others can be; %v were all set", e.flagList, e.problemFlags)
      	}
    
    + 	// If the error struct is empty (i.e. wasn't created by Cobra), e.flagGroupType will be an empty string.
    + 	// We don't have a specific message to print, so instead just print the struct contents.
    + 	return fmt.Sprintf("%#v", e)
    - 	panic("invalid flagGroupType")
      }

    I chose to replace the panic in FlagGroupError.Error() as a precaution. I don't think it's likely that someone would create an empty FlagGroupError and call Error() on it, but just in case, I felt like the better approach was to return something instead of causing a panic later down the line when something eventually tried to get the error string.

  • errors_test.go:

      func TestInvalidArgValueError_GetSuggestions(t *testing.T) {
    - 	expected := "a"
    + 	expected := []string{"a", "b"}
      	err := &InvalidArgValueError{suggestions: expected}
    
      	got := err.GetSuggestions()
    + 	expectedString := fmt.Sprintf("%#v", expected)
    + 	gotString := fmt.Sprintf("%#v", got)
    - 	if expected != gotString {
    + 	if expectedString != got {
      		t.Fatalf("expected %v, got %v", expected, got)
      	}
      }
    
      func TestUnknownSubcommandError_GetSuggestions(t *testing.T) {
    - 	expected := "a"
    + 	expected := []string{"a", "b"}
      	err := &UnknownSubcommandError{suggestions: expected}
    
      	got := err.GetSuggestions()
    + 	expectedString := fmt.Sprintf("%#v", expected)
    + 	gotString := fmt.Sprintf("%#v", got)
    - 	if expected != gotString {
    + 	if expectedString != got {
      		t.Fatalf("expected %v, got %v", expected, got)
      	}
      }

Copy link
Contributor

@ccoVeille ccoVeille left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider these suggestions, and give me feedbacks about them.

I tried to make things easier, and reduce the number of exported struct/constants

errors.go Outdated
return fmt.Sprintf("if any flags in the group [%v] are set none of the others can be; %v were all set", e.flagList, e.problemFlags)
}

panic("invalid flagGroupType")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't you talk about returning an error here?

But maybe, you could take a look at this comment first

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't you talk about returning an error here?

I forgot to push that change after rebase, sorry 😅

errors.go Outdated
Comment on lines 147 to 212
// FlagGroupError is the error returned when mutually-required or
// mutually-exclusive flags are not properly specified.
type FlagGroupError struct {
cmd *Command
flagList string
flagGroupType FlagGroupType
problemFlags []string
}

// GetCommand returns the Command that the error occurred in.
func (e *FlagGroupError) GetCommand() *Command {
return e.cmd
}

// GetFlagList returns the flags in the group.
func (e *FlagGroupError) GetFlags() []string {
return strings.Split(e.flagList, " ")
}

// GetFlagGroupType returns the type of flag group causing the error.
//
// Valid types are:
// - FlagsAreMutuallyExclusive for mutually-exclusive flags.
// - FlagsAreRequiredTogether for mutually-required flags.
// - FlagsAreOneRequired for flags where at least one must be present.
func (e *FlagGroupError) GetFlagGroupType() FlagGroupType {
return e.flagGroupType
}

// GetProblemFlags returns the flags causing the error.
//
// For flag groups where:
// - FlagsAreMutuallyExclusive, these are all the flags set.
// - FlagsAreRequiredTogether, these are the missing flags.
// - FlagsAreOneRequired, this is empty.
func (e *FlagGroupError) GetProblemFlags() []string {
return e.problemFlags
}

// FlagGroupType identifies which failed validation caused a FlagGroupError.
type FlagGroupType string

const (
FlagsAreMutuallyExclusive FlagGroupType = "if any is set, none of the others can be"
FlagsAreRequiredTogether FlagGroupType = "if any is set, they must all be set"
FlagsAreOneRequired FlagGroupType = "at least one of the flags is required"
)

// Error implements error.
func (e *FlagGroupError) Error() string {
switch e.flagGroupType {
case FlagsAreRequiredTogether:
return fmt.Sprintf("if any flags in the group [%v] are set they must all be set; missing %v", e.flagList, e.problemFlags)
case FlagsAreOneRequired:
return fmt.Sprintf("at least one of the flags in the group [%v] is required", e.flagList)
case FlagsAreMutuallyExclusive:
return fmt.Sprintf("if any flags in the group [%v] are set none of the others can be; %v were all set", e.flagList, e.problemFlags)
}

panic("invalid flagGroupType")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is another implementation logic that could help by implementing Unwrap (please read go std errors package for further information)

It avoids to add more exported types,and help with using errors.Is(err,

Suggested change
// FlagGroupError is the error returned when mutually-required or
// mutually-exclusive flags are not properly specified.
type FlagGroupError struct {
cmd *Command
flagList string
flagGroupType FlagGroupType
problemFlags []string
}
// GetCommand returns the Command that the error occurred in.
func (e *FlagGroupError) GetCommand() *Command {
return e.cmd
}
// GetFlagList returns the flags in the group.
func (e *FlagGroupError) GetFlags() []string {
return strings.Split(e.flagList, " ")
}
// GetFlagGroupType returns the type of flag group causing the error.
//
// Valid types are:
// - FlagsAreMutuallyExclusive for mutually-exclusive flags.
// - FlagsAreRequiredTogether for mutually-required flags.
// - FlagsAreOneRequired for flags where at least one must be present.
func (e *FlagGroupError) GetFlagGroupType() FlagGroupType {
return e.flagGroupType
}
// GetProblemFlags returns the flags causing the error.
//
// For flag groups where:
// - FlagsAreMutuallyExclusive, these are all the flags set.
// - FlagsAreRequiredTogether, these are the missing flags.
// - FlagsAreOneRequired, this is empty.
func (e *FlagGroupError) GetProblemFlags() []string {
return e.problemFlags
}
// FlagGroupType identifies which failed validation caused a FlagGroupError.
type FlagGroupType string
const (
FlagsAreMutuallyExclusive FlagGroupType = "if any is set, none of the others can be"
FlagsAreRequiredTogether FlagGroupType = "if any is set, they must all be set"
FlagsAreOneRequired FlagGroupType = "at least one of the flags is required"
)
// Error implements error.
func (e *FlagGroupError) Error() string {
switch e.flagGroupType {
case FlagsAreRequiredTogether:
return fmt.Sprintf("if any flags in the group [%v] are set they must all be set; missing %v", e.flagList, e.problemFlags)
case FlagsAreOneRequired:
return fmt.Sprintf("at least one of the flags in the group [%v] is required", e.flagList)
case FlagsAreMutuallyExclusive:
return fmt.Sprintf("if any flags in the group [%v] are set none of the others can be; %v were all set", e.flagList, e.problemFlags)
}
panic("invalid flagGroupType")
}
// FlagGroupError is the error returned when mutually-required or
// mutually-exclusive flags are not properly specified.
type FlagGroupError struct {
cmd *Command
flagList string
err error
problemFlags []string
}
// GetCommand returns the Command that the error occurred in.
func (e *FlagGroupError) GetCommand() *Command {
return e.cmd
}
// GetFlagList returns the flags in the group.
func (e *FlagGroupError) GetFlags() []string {
return strings.Split(e.flagList, " ")
}
// Unwrap returns the type of flag group causing the error.
//
// Valid types are:
// - ErrFlagsAreMutuallyExclusive for mutually-exclusive flags.
// - ErrFlagsAreRequiredTogether for mutually-required flags.
// - ErrFlagsAreOneRequired for flags where at least one must be present.
func (e *FlagGroupError) Unwrap() error {
return e.err
}
// GetProblemFlags returns the flags causing the error.
//
// For flag groups where:
// - FlagsAreMutuallyExclusive, these are all the flags set.
// - FlagsAreRequiredTogether, these are the missing flags.
// - FlagsAreOneRequired, this is empty.
func (e *FlagGroupError) GetProblemFlags() []string {
return e.problemFlags
}
var (
ErrFlagsAreMutuallyExclusive = errors.New("if any is set, none of the others can be")
ErrFlagsAreRequiredTogether = errors.New("if any is set, they must all be set")
ErrFlagsAreOneRequired = errors.New("at least one of the flags is required")
)
// Error implements error.
func (e *FlagGroupError) Error() string {
switch {
case errors.Is(err, ErrFlagsAreRequiredTogether):
return fmt.Sprintf("if any flags in the group [%v] are set they must all be set; missing %v", e.flagList, e.problemFlags)
case errors.Is(e.err, ErrFlagsAreOneRequired):
return fmt.Sprintf("at least one of the flags in the group [%v] is required", e.flagList)
case errors.Is(e.err, ErrFlagsAreMutuallyExclusive):
return fmt.Sprintf("if any flags in the group [%v] are set none of the others can be; %v were all set", e.flagList, e.problemFlags)
}
return e.err
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, yeah. Using errors and errors.Is would be more idiomatic than comparing const strings.

flag_groups.go Outdated
Comment on lines 164 to 167
return &FlagGroupError{
flagList: flagList,
flagGroupType: FlagsAreRequiredTogether,
problemFlags: unset,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and everywhere, I don't see a logic to use a pointer

Suggested change
return &FlagGroupError{
flagList: flagList,
flagGroupType: FlagsAreRequiredTogether,
problemFlags: unset,
return FlagGroupError{
flagList: flagList,
flagGroupType: FlagsAreRequiredTogether,
problemFlags: unset,

It leads to complicated syntax later, and double pointers so you have to do this

  var invalidArgCountErr *cobra.InvalidArgCountError 
   if errors.As(err, &invalidArgCountErr) {

When this could be enough

  var invalidArgCountErr cobra.InvalidArgCountError 
   if errors.As(err, &invalidArgCountErr) {

Copy link
Author

@eth-p eth-p May 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I totally agree that it makes the syntax complicated. I'm perfectly happy to change it, but I had a reason for originally choosing to use a pointer receiver for the Error() string method: consistency with how the Go standard library implements error structs*.

A couple examples:

A newer recent reason we might want to keep the pointer receivers is for consistency with pflag. When I made the PR to add error structs to that project, it ended up being merged with the pointer-receiver error structs.

*I haven't been able to find any official documentation explaining why the standard library chooses to use pointer receivers for errors, but this Reddit comment explains a compile-time type safety benefit of using them over value receivers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know a lot of code in Go standard library are not respecting the Go idiomatic way. So sometimes finding examples in Go code doesn't help much.

Droping @alexandear here.

What do you think about this?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eth-p you're completely right about os.PathError and others. But I'm sticking to @ccoVeille's approach of returning a non-pointer FlagGroupError as a simpler option.

Other suggestions:

  • Maybe we should make the FlagGroupError struct unexported (i.e., flagGroupError)? In the future, we can refactor to return a pointer if needed without breaking compatibility.
  • Should we return error instead of *FlagGroupError?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough. I just pushed the code with the following changes made:

  • @ccoVeille's suggestion to make all error structs use value receivers instead of pointer receivers.
  • @alexandear's suggestion to have validateOneRequiredFlagGroups, validateRequiredFlagGroups, and validateExclusiveFlagGroups return error instead of *FlagGroupError. I had to add a new parameter cmd *Command so those functions could still create a FlagGroupError containing the relevant Command.

Maybe we should make the FlagGroupError struct unexported (i.e., flagGroupError)?

Unfortunately, that would go against the goal for this PR. Adding the error structs is meant to provide a way for callers of Cobra commands to be able to determine the error type and its details without having to parse the error message.

Comment on lines 850 to 851
var invalidArgCountErr *cobra.InvalidArgCountError
if errors.As(err, &invalidArgCountErr) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please read this

#2266 (comment)

Comment on lines 211 to 212
var flagGroupErr *FlagGroupError
if !errors.As(err, &flagGroupErr) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please read this

#2266 (comment)

flag_groups.go Outdated
}
return nil
}

func validateExclusiveFlagGroups(data map[string]map[string]bool) error {
func validateExclusiveFlagGroups(data map[string]map[string]bool) *FlagGroupError {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func validateExclusiveFlagGroups(data map[string]map[string]bool) *FlagGroupError {
func validateExclusiveFlagGroups(data map[string]map[string]bool) FlagGroupError {

Please read this

#2266 (comment)

flag_groups.go Outdated
Comment on lines 214 to 218
return &FlagGroupError{
flagList: flagList,
flagGroupType: FlagsAreMutuallyExclusive,
problemFlags: set,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return &FlagGroupError{
flagList: flagList,
flagGroupType: FlagsAreMutuallyExclusive,
problemFlags: set,
}
return FlagGroupError{
flagList: flagList,
flagGroupType: FlagsAreMutuallyExclusive,
problemFlags: set,
}

Please read this

#2266 (comment)

@eth-p eth-p force-pushed the error-structs branch 2 times, most recently from 5098ed8 to 355d742 Compare May 6, 2025 08:22
@eth-p
Copy link
Author

eth-p commented May 6, 2025

I implemented your suggestion to use errors.New and errors.Is instead of const strings, and I left a comment explaining why I originally chose to use pointer receivers for the error structs. If you still want me to convert the Error() string functions to value receivers, I can make another set of changes after.

I wasn't able to create fixup commits without causing merge conflicts when squashing them, so here's another diff to summarize this set of changes:

 type FlagGroupError struct {
+	err           error
 	cmd           *Command
 	flagList      string
-	flagGroupType FlagGroupType
 	problemFlags  []string
 }
 
-// FlagGroupType identifies which failed validation caused a FlagGroupError.
-type FlagGroupType string
-
-const (
-	FlagsAreMutuallyExclusive FlagGroupType = "if any is set, none of the others can be"
-	FlagsAreRequiredTogether  FlagGroupType = "if any is set, they must all be set"
-	FlagsAreOneRequired       FlagGroupType = "at least one of the flags is required"
-)
-

+var (
+	// ErrFlagsAreMutuallyExclusive indicates that more than one flag marked by MarkFlagsMutuallyExclusive was provided.
+	ErrFlagsAreMutuallyExclusive = errors.New("if any is set, none of the others can be")
+
+	// ErrFlagsAreRequiredTogether indicates that only one of the flags marked by MarkFlagsRequiredTogether were provided.
+	ErrFlagsAreRequiredTogether = errors.New("if any is set, they must all be set")
+
+	// ErrFlagsAreOneRequired indicates that none of the flags marked by MarkFlagsOneRequired flags were provided.
+	ErrFlagsAreOneRequired = errors.New("at least one of the flags is required")
+)
+

-// GetFlagGroupType returns the type of flag group causing the error.
-// Valid types are:
-//   - FlagsAreMutuallyExclusive for mutually-exclusive flags.
-//   - FlagsAreRequiredTogether for mutually-required flags.
-//   - FlagsAreOneRequired for flags where at least one must be present.
-func (e *FlagGroupError) GetFlagGroupType() FlagGroupType {
-	return e.flagGroupType
-}

+// Unwrap implements errors.Unwrap
+//
+// This returns one of:
+//   - ErrFlagsAreMutuallyExclusive
+//   - ErrFlagsAreRequiredTogether
+//   - ErrFlagsAreOneRequired
+func (e *FlagGroupError) Unwrap() error {
+	return e.err
+}
 
 // Error implements error.
 func (e *FlagGroupError) Error() string {
-	switch e.flagGroupType {
-	case FlagsAreRequiredTogether:
+	switch {
+	case errors.Is(e.err, ErrFlagsAreRequiredTogether):
 		return fmt.Sprintf("if any flags in the group [%v] are set they must all be set; missing %v", e.flagList, e.problemFlags)
-	case FlagsAreOneRequired:
+	case errors.Is(e.err, ErrFlagsAreOneRequired):
 		return fmt.Sprintf("at least one of the flags in the group [%v] is required", e.flagList)
-	case FlagsAreMutuallyExclusive:
+	case errors.Is(e.err, ErrFlagsAreMutuallyExclusive):
 		return fmt.Sprintf("if any flags in the group [%v] are set none of the others can be; %v were all set", e.flagList, e.problemFlags)
 	}
 
-	// If the error struct is empty (i.e. wasn't created by Cobra), e.flagGroupType will be an empty string.
-	// We don't have a specific message to print, so instead just print the struct contents.
+	// If the error struct is empty (i.e. wasn't created by Cobra), e.err will be nil.
+	// We don't have a message to print, so instead just print the struct contents.
 	return fmt.Sprintf("%#v", e)
 }

I left the fmt.Sprintf("%#v") as is, since err can be nil in the same way flagGroupType could be empty, which would've made return e.err.Error() panic.

@eth-p eth-p force-pushed the error-structs branch from 355d742 to ceb04af Compare May 7, 2025 14:51
@eth-p eth-p force-pushed the error-structs branch from ceb04af to 4f45711 Compare May 7, 2025 14:56
Copy link
Contributor

@ccoVeille ccoVeille left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it.

I feel like it still need some minors adjustment, but I would say, let's wait for @marckhouzam feedbacks. He is the maintainer of cobra. He owns the logic of features.

I don't think there is a need to go further before he comes back to you

Comment on lines +54 to +64
func expectErrorAs(err error, target error, t *testing.T) {
if err == nil {
t.Fatalf("Expected error, got nil")
}

targetType := reflect.TypeOf(target)
targetPtr := reflect.New(targetType).Interface() // *SomeError
if !errors.As(err, targetPtr) {
t.Fatalf("Expected error to be %T, got %T", target, err)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not using simply using this?

Suggested change
func expectErrorAs(err error, target error, t *testing.T) {
if err == nil {
t.Fatalf("Expected error, got nil")
}
targetType := reflect.TypeOf(target)
targetPtr := reflect.New(targetType).Interface() // *SomeError
if !errors.As(err, targetPtr) {
t.Fatalf("Expected error to be %T, got %T", target, err)
}
}
func expectErrorAs(err error, target error, t *testing.T) {
if err == nil {
t.Fatalf("Expected error, got nil")
}
if !errors.As(err, &target) {
t.Fatalf("Expected error to be %T, got %T", target, err)
}
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, actually, yeah. Good point. I'll fix it in the next set of changes.

Comment on lines +37 to +43
func getCommandName(c *Command) string {
if c == nil {
return "<nil>"
} else {
return c.Name()
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func getCommandName(c *Command) string {
if c == nil {
return "<nil>"
} else {
return c.Name()
}
}
func getCommandName(c *Command) string {
if c == nil {
return "<nil>"
}
return c.Name()
}


err := rootCmd.Execute()

var invalidArgCountErr cobra.InvalidArgCountError
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var invalidArgCountErr cobra.InvalidArgCountError
var errInvalidArgCount cobra.InvalidArgCountError

@ccoVeille
Copy link
Contributor

@marckhouzam could you review this PR ?

cc @caarlos0

@caarlos0
Copy link
Collaborator

This would be really useful in some projects I maintain, especially in Fang.

@spf13 @marckhouzam how can I help getting this merged in? :)

@spf13
Copy link
Owner

spf13 commented Jul 8, 2025

@caarlos0 would you be interested in joining the project as a maintainer? Help us evaluate and merge in PRs like this?

@caarlos0
Copy link
Collaborator

caarlos0 commented Jul 8, 2025

@spf13 not sure how much time I can put in, but happy to help as much as I can yes! <3

@spf13
Copy link
Owner

spf13 commented Jul 8, 2025

Invited. Welcome to the team.

@shadiramadan
Copy link

Haha @caarlos0 I found this thread through your comment in Fang!

This is great! I was wondering how I could override the usage errors without simply hiding them!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants