-
-
Notifications
You must be signed in to change notification settings - Fork 51
feat(go): turn bound text into the type a field wants #978
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
67edfbe
feat(go): turn bound text into the type a field wants
jdx ac31a60
fix(go): refuse a padded value, as the Rust sibling does
jdx 6b84c44
fix(go): escape the rejected value before showing it back
jdx 0e1d820
fix(go): escape the rejected value in the error value too
jdx 47b7c53
fix(go): take the values Rust takes, and refuse the ones it refuses
jdx 668ad12
fix(go): keep the value where Go complains and Rust does not
jdx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| package argv | ||
|
|
||
| import ( | ||
| "errors" | ||
| "math" | ||
| "strconv" | ||
| "strings" | ||
| "time" | ||
| ) | ||
|
|
||
| // Turning bound text into the type a field wants. | ||
| // | ||
| // Binding collects text, deliberately: the grammar decides which token becomes | ||
| // which flag or argument, not what it means, so `"8"` stays a string until | ||
| // something that knows the target type asks. This is where that asking happens. | ||
| // | ||
| // The functions are separate rather than one generic `Convert`, because the set | ||
| // of types a CLI wants is small and closed, and each one has its own idea of what | ||
| // it accepts — `1h30m` is a duration and not a number, `yes` is neither. A | ||
| // generated struct calls the one matching its field. | ||
| // | ||
| // Every failure carries the text that would not convert and the type it was being | ||
| // converted to. A message that says only "invalid value" makes the user guess | ||
| // which of their words was wrong. | ||
| // | ||
| // Nothing is trimmed on the way in. `" 8 "` is refused, as `" 8 ".parse::<i64>()` | ||
| // is on the Rust side — checked rather than assumed. The parser goes out of its | ||
| // way to hand over the bytes the operating system gave it, and a converter | ||
| // quietly tidying them would mean a quoted argument means one thing in Go and | ||
| // another in Rust from the same spec. | ||
|
|
||
| // Int converts a bound value, naming the entry in any failure. | ||
| func Int(name, value string) (int64, *Error) { | ||
| n, err := strconv.ParseInt(value, 10, 64) | ||
| if err != nil { | ||
| return 0, invalid(name, value, "a whole number") | ||
| } | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| return n, nil | ||
| } | ||
|
|
||
| // Uint is [Int] for a value that may not be negative. | ||
| // | ||
| // A leading `+` is a sign, not a digit, and Rust takes it: `"+8".parse::<u64>()` | ||
| // is 8, while `strconv.ParseUint` refuses the string outright. Refusing `+8` here | ||
| // would have the same spec accept a value in Rust and reject it in Go — and the | ||
| // message would have said it was not a non-negative whole number, about a value | ||
| // that is plainly both. | ||
| func Uint(name, value string) (uint64, *Error) { | ||
| digits := value | ||
| if rest, found := strings.CutPrefix(digits, "+"); found { | ||
| digits = rest | ||
| } | ||
| n, err := strconv.ParseUint(digits, 10, 64) | ||
| if err != nil { | ||
| return 0, invalid(name, value, "a whole number, not negative") | ||
| } | ||
| return n, nil | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| // Float converts a bound value to a float. | ||
| // | ||
| // The two standard libraries disagree at three edges, and each one is settled the | ||
| // way Rust settles it — a spec that means one thing compiled through the derive | ||
| // and another through this is the failure mode the whole port is written against. | ||
| // | ||
| // - Go takes digit separators (`1_5`) and hexadecimal floats (`0x1.8p0`); | ||
| // `f64::from_str` takes neither, so they are refused before parsing. Neither | ||
| // `_` nor `x` appears in any float Rust accepts, which is why this is a check | ||
| // on two characters rather than a grammar of its own. | ||
| // - A number too large to hold is `inf` in Rust and a range error in Go, which | ||
| // hands back the same ±Inf beside it. The value is kept and the error is not. | ||
| // - A signed NaN — `+nan`, `-NaN` — parses in Rust and not in Go. The sign | ||
| // means nothing on either side. | ||
| // | ||
| // Everything else agrees: `inf`, `infinity`, a bare `.5` or `5.`, and an | ||
| // underflow to zero. | ||
| func Float(name, value string) (float64, *Error) { | ||
| if strings.ContainsAny(value, "_xX") { | ||
| return 0, invalid(name, value, "a number") | ||
| } | ||
| n, err := strconv.ParseFloat(value, 64) | ||
| switch { | ||
| case err == nil: | ||
| return n, nil | ||
| case errors.Is(err, strconv.ErrRange): | ||
| // ±Inf for an overflow, zero for an underflow — the value Go returns is | ||
| // the one Rust would have. | ||
| return n, nil | ||
| case signedNaN(value): | ||
| return math.NaN(), nil | ||
| } | ||
| return 0, invalid(name, value, "a number") | ||
| } | ||
|
|
||
| // signedNaN reports the one spelling Rust's f64 takes that Go's does not. | ||
| func signedNaN(value string) bool { | ||
| return len(value) > 1 && (value[0] == '+' || value[0] == '-') && | ||
| strings.EqualFold(value[1:], "nan") | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| // Bool converts a bound value to a bool. | ||
| // | ||
| // The spellings are Go's own, which are also the ones `strconv.ParseBool` takes: | ||
| // `1`, `t`, `T`, `true`, `TRUE`, `True` and their false counterparts. Note this | ||
| // is *wider* than [EnvTruth], which an environment variable setting a value-less | ||
| // flag goes through — that one is an allow-list matching usage-lib, and the two | ||
| // answer different questions: this converts a value somebody typed, that one | ||
| // decides whether a variable counts as setting a flag at all. | ||
| func Bool(name, value string) (bool, *Error) { | ||
| b, err := strconv.ParseBool(value) | ||
| if err != nil { | ||
| return false, invalid(name, value, "true or false") | ||
| } | ||
| return b, nil | ||
| } | ||
|
|
||
| // Duration converts a bound value to a duration, in Go's notation: `1h30m`, | ||
| // `250ms`, `2s`. | ||
| func Duration(name, value string) (time.Duration, *Error) { | ||
| d, err := time.ParseDuration(value) | ||
| if err != nil { | ||
| return 0, invalid(name, value, "a duration such as 30s or 1h30m") | ||
| } | ||
| return d, nil | ||
| } | ||
|
|
||
| // Each maps a conversion over the values a variadic or repeatable entry | ||
| // collected, stopping at the first that will not convert. | ||
| // | ||
| // Written out because the alternative is every caller writing the same loop, and | ||
| // getting the early return wrong in a way that reports the last failure instead | ||
| // of the first. | ||
| func Each[T any](name string, values []string, convert func(string, string) (T, *Error)) ([]T, *Error) { | ||
| out := make([]T, 0, len(values)) | ||
| for _, v := range values { | ||
| converted, err := convert(name, v) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| out = append(out, converted) | ||
| } | ||
| return out, nil | ||
| } | ||
|
|
||
| func invalid(name, value, want string) *Error { | ||
| return &Error{Code: CodeInvalidValue, Name: name, Value: value, Want: want} | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.