Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,25 @@ rules drift, so both are run over mise's real spec and compared — the same che
Two implementations checked against one oracle beats two checked against each
other.

## Errors

`argv.Render` turns a failure into what a CLI should print to stderr:

```
error: unknown flag `--wat`

Usage: ex run [-f --force]

For more information, try `--help`.
```

The one part of this module with **no reference to match**. usage-lib prints a
one-line message inside miette's frame and usage-argv renders through miette too;
neither travels, because miette is a Rust library and a Go CLI drawing the same
ASCII art would be imitating a diagnostic format rather than sharing one. So this
is judged on whether the message says what went wrong, where, and what to try —
and tested by asserting those rather than by comparing bytes.

## Conformance

The [corpus](../corpus) is the definition of correct, and it is plain JSON so that
Expand Down Expand Up @@ -191,9 +210,6 @@ claim is measured at real scale rather than against a fixture with four flags:
- **Typed values.** Binding collects text. Something still has to turn `"8"` into
an `int` and `"1m"` into a `time.Duration`, and report the ones that will not
convert.
- **Errors worth reading.** `Error()` returns `unknown flag: --wat`, which names
the problem and helps nobody fix it. usage-argv renders these through miette
with the offending token underlined.
- **Completions.** The Rust side serves these from the parser's own scope rules so
that what is offered and what is accepted cannot disagree; the hooks for it
(`Collecting`, `PendingArg`, `FlagsInScope`, `CommandStart`) are already here.
16 changes: 14 additions & 2 deletions go/argv/argv.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,11 @@ type Error struct {
// Name is the flag or argument the post-binding rules rejected, as the spec
// spells it.
Name string
// Spelling is how the entry is typed, where the rule that raised this knew —
// see [Meta.Spelling]. Empty means only the name is known.
Spelling string
// OtherSpelling is the same for [Error.Other].
OtherSpelling string
// Choices carries the declared list for CodeInvalidChoice, rather than the
// offending value: the value is the caller's to render, and it has it.
Choices []string
Expand All @@ -331,14 +336,21 @@ type Error struct {
Other string
}

// Error is the message Go's own error interface asks for.
//
// The tokens go through `safe` here as they do in [Render]: this string reaches a
// terminal too, by way of whatever logs or prints it, and a rejected argument
// carrying an escape sequence can recolour that output or forge a line in it.
// Where the message quotes the spec — a flag's name, an argument's — there is
// nothing to escape, because the author wrote it and the parse tables hold it.
func (e *Error) Error() string {
switch e.Code {
case CodeUnknownFlag:
return "unknown flag: " + e.Token
return "unknown flag: " + safe(e.Token)
case CodeMissingFlagValue:
return "missing value for flag: " + e.Flag.Name
case CodeUnexpectedArg:
return "unexpected argument: " + e.Token
return "unexpected argument: " + safe(e.Token)
case CodeArgRequiresDoubleDash:
return "argument requires a -- separator: " + e.Arg.Name
case CodeTooDeep:
Expand Down
24 changes: 20 additions & 4 deletions go/argv/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,10 @@ func (p *Parser) longFlag(token string) bool {
if hasAttached {
value = attached
} else {
v, ok := p.takeDetachedValue(flag)
// `token`, not `--`+name: with no attached value the token is the
// spelling, and slicing it costs nothing on a path that must not
// allocate.
v, ok := p.takeDetachedValue(flag, token, 0)
if !ok {
return false
}
Expand Down Expand Up @@ -365,7 +368,7 @@ func (p *Parser) shortFlag() bool {
var value string
switch {
case rest == "":
v, ok := p.takeDetachedValue(flag)
v, ok := p.takeDetachedValue(flag, "", b)
if !ok {
return false
}
Expand All @@ -386,13 +389,26 @@ func (p *Parser) shortFlag() bool {
// It refuses a flag-like token: `--jobs --force` is far more likely a forgotten
// value than a deliberate one, and the attached form is available for the
// deliberate case. The negative-number exception means `--offset -1` still works.
func (p *Parser) takeDetachedValue(flag *Flag) (string, bool) {
func (p *Parser) takeDetachedValue(flag *Flag, long string, short byte) (string, bool) {
if p.pos < len(p.argv) && !isFlagLike(p.argv[p.pos]) {
v := p.argv[p.pos]
p.pos++
return v, true
}
p.fail(Error{Code: CodeMissingFlagValue, Flag: flag})
// The form the user actually wrote, carried so the advice can use it. A flag
// answers to several spellings and the first is not always the one in front of
// them: with an inherited `--jobs --workers` whose `--jobs` a nearer command
// has taken, `--workers` is what bound, and telling them to write `--jobs=…`
// sends them to a different flag.
//
// The long form arrives as a slice of the token, and the short one is built
// here rather than by the caller — this branch is the failure, and the caller
// is the hot path that must not allocate.
typed := long
if typed == "" && short != 0 {
typed = "-" + string(short)
}
p.fail(Error{Code: CodeMissingFlagValue, Flag: flag, Token: typed})
return "", false
}

Expand Down
19 changes: 15 additions & 4 deletions go/argv/post.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ type Meta struct {
Key uint64
// Name is what the spec calls it, for the error.
Name string
// Spelling is how a user types it — `--file`, `-f` — for the errors raised
// here, which judge an *entry* and so never see a [Flag].
//
// Carried rather than derived from the name: a name is a long form wherever
// there is one, but `--a` and `-a` are both one character and guessing
// between them can name a different flag entirely. Empty for an argument,
// which is typed as its value rather than as a form.
Spelling string
// Flag distinguishes a missing flag from a missing argument, which the
// grammar reports as different classes.
Flag bool
Expand Down Expand Up @@ -175,15 +183,16 @@ func Check(m *Meta, values []string, occurrences int) *Error {
if m.Flag {
code = CodeMissingRequiredFlag
}
return &Error{Code: code, Name: m.Name}
return &Error{Code: code, Name: m.Name, Spelling: m.Spelling}
Comment thread
cursor[bot] marked this conversation as resolved.
}

if len(m.Choices) > 0 {
// Every value, not just the first: a variadic can be given a good value
// and a bad one, and so can a repeatable flag across occurrences.
for _, v := range values {
if !contains(m.Choices, v) {
return &Error{Code: CodeInvalidChoice, Name: m.Name, Choices: m.Choices}
return &Error{Code: CodeInvalidChoice, Name: m.Name,
Spelling: m.Spelling, Choices: m.Choices}
}
}
}
Expand All @@ -192,11 +201,13 @@ func Check(m *Meta, values []string, occurrences int) *Error {
// its minimum; it simply is not there, and reporting `var_too_few` for it
// would make every bounded variadic effectively required.
if m.VarMin > 0 && len(values) > 0 && uint32(len(values)) < m.VarMin {
return &Error{Code: CodeVarTooFew, Name: m.Name, Bound: m.VarMin, Got: len(values)}
return &Error{Code: CodeVarTooFew, Name: m.Name, Spelling: m.Spelling,
Bound: m.VarMin, Got: len(values)}
}

if m.VarMax > 0 && occurrences > int(m.VarMax) {
return &Error{Code: CodeVarTooMany, Name: m.Name, Bound: m.VarMax, Got: occurrences}
return &Error{Code: CodeVarTooMany, Name: m.Name, Spelling: m.Spelling,
Bound: m.VarMax, Got: occurrences}
}

return nil
Expand Down
21 changes: 6 additions & 15 deletions go/argv/relationships.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,12 @@ func CheckRelationships(meta Metadata, entries []uint64, sourceOf func(uint64) S
if given(other) {
// Both names, because either alone reads as a puzzle: which flag
// is unwelcome depends entirely on what else was given.
return &Error{
Code: CodeConflictingFlags,
Name: m.Name,
Other: nameOf(meta, other),
o := meta.Lookup(other)
e := &Error{Code: CodeConflictingFlags, Name: m.Name, Spelling: m.Spelling}
if o != nil {
e.Other, e.OtherSpelling = o.Name, o.Spelling
}
return e
}
}
continue
Expand Down Expand Up @@ -146,7 +147,7 @@ func missingRequired(m *Meta) *Error {
if m.Flag {
code = CodeMissingRequiredFlag
}
return &Error{Code: code, Name: m.Name}
return &Error{Code: code, Name: m.Name, Spelling: m.Spelling}
}

func anySet(keys []uint64, isSet func(uint64) bool) bool {
Expand All @@ -157,13 +158,3 @@ func anySet(keys []uint64, isSet func(uint64) bool) bool {
}
return false
}

// nameOf renders the other side of a relationship, falling back to nothing rather
// than to a number: an error naming `key 7` is worse than one naming only the
// flag the reader already knows about.
func nameOf(meta Metadata, key uint64) string {
if m := meta.Lookup(key); m != nil {
return m.Name
}
return ""
}
Loading