Skip to content

Replace raw stdlib parse errors in typed flag Set methods (#269)#469

Merged
tomasaschan merged 2 commits into
spf13:masterfrom
reuvenharrison:friendly-parse-errors
May 16, 2026
Merged

Replace raw stdlib parse errors in typed flag Set methods (#269)#469
tomasaschan merged 2 commits into
spf13:masterfrom
reuvenharrison:friendly-parse-errors

Conversation

@reuvenharrison
Copy link
Copy Markdown

Refs #269.

Problem

Each typed flag's Set method returns the strconv (or time.ParseDuration) error verbatim, so users see internal-looking text:

$ mytool --some-flag=x
Error: invalid argument "x" for "--some-flag" flag: strconv.ParseBool: parsing "x": invalid syntax

The leading invalid argument "x" for "--some-flag" flag: shape is fine — it tells the user what failed and where. The trailing strconv.ParseBool: parsing "x": invalid syntax is implementation detail leaking to end users.

Same shape applies to int* (strconv.ParseInt), uint* (strconv.ParseUint), float* (strconv.ParseFloat), and duration (time.ParseDuration).

Change

Each typed flag's Set method now returns a human-readable hint when the underlying parse fails:

Flag type Suffix
bool must be true or false
int, int8/16/32/64 must be an integer
uint, uint8/16/32/64 must be a non-negative integer
float32/64 must be a number
duration must be a duration like "30s" or "5m"

Result:

$ mytool --some-flag=x
Error: invalid argument "x" for "--some-flag" flag: must be true or false

The outer invalid argument %q for %q flag: %v shape from InvalidValueError.Error() is unchanged; only the cause changes.

Scope

  • 14 typed-flag files, one Set method each, identical pattern.
  • flag_test.go updated: the existing assertion in testParse (line 398, the --bool=abcdefg case) now asserts the new message.
  • New TestInvalidArgumentMessages table-driven test locks in the suffix per type so future drift is caught.
  • Slices, arrays, and map types are out of scope for this PR. They have a different shape (per-element parsing inside loops) and would be a natural follow-up if the approach lands.

Backwards compatibility

The error type returned from Parse is still *InvalidValueError. Code using errors.As(err, &ive) continues to work. The cause chain (errors.Unwrap to reach the *strconv.NumError) is no longer present, since the new errors are plain errors.New. Any caller that drilled into the wrapped strconv error would need to switch to inspecting InvalidValueError.GetFlag().Value.Type() instead. I expect such callers to be vanishingly rare; happy to add an errors.Is sentinel if maintainers want a stable handle.

Each typed flag's Set method previously returned its strconv (or
time.ParseDuration) error verbatim, which leaks implementation detail
into user-facing CLI errors:

    invalid argument "x" for "--bool" flag: strconv.ParseBool: parsing "x": invalid syntax

Replace with human-readable hints, keyed off the flag's underlying type:

    invalid argument "x" for "--bool" flag: must be true or false

Covered: bool, int/int8/16/32/64, uint/uint8/16/32/64, float32/64,
duration. Slices and map types fall through to their existing behavior.

Updates the affected line in TestParse, and adds TestInvalidArgumentMessages
to lock in the new messages across types.

Refs spf13#269.
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 10, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown
Collaborator

@tomasaschan tomasaschan left a comment

Choose a reason for hiding this comment

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

Thanks!

I'm on the fence on whether it's worth introducing stable errors for the different cases (at least a few strings passed to errors.New are repeated here), and then also whether to export them. The stdlib flag package seems to introduce them unexported, which from a user's perspective isn't that different from the repeated approach here (you can't use Is on them anyway), so I think we can merge this as is and then iterate if anyone asks for stable errors to reference.

CI surfaced two issues in the test changes:
- flag_test.go:669 column alignment off after the cases struct rename
  (gofmt -w produces a 3-line diff)
- flag_test.go:688 uses io.Discard which was introduced in Go 1.16;
  pflag's CI matrix still tests Go 1.12, so the Test (1.12) job failed
  with 'undefined: io.Discard'. ioutil.Discard works in all Go versions
  back to 1.0 and is still functional despite the post-1.16 deprecation.
@tomasaschan tomasaschan merged commit 00f25b9 into spf13:master May 16, 2026
8 checks passed
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.

3 participants