Retain entered flag spellings in parser failures
This commit is contained in:
@@ -251,7 +251,7 @@ value was prohibited. The complete offline root suite passed on Go 1.26.8.
|
||||
|
||||
## R-7: Retain entered flag spellings in parser-level usage failures
|
||||
|
||||
Status: open. Priority: P3. Category: incomplete diagnostic adaptation.
|
||||
Status: complete. Priority: P3. Category: incomplete diagnostic adaptation.
|
||||
|
||||
**Evidence:** Typed flag conversion uses occurrence provenance, but
|
||||
[Session.Parse](../../internal/cobra/parse.go#L89) returns normalized backend
|
||||
@@ -278,6 +278,15 @@ values and invalid help values, including an earlier invalid occurrence followed
|
||||
by a valid overwrite. Assert the identifying fragments and existing causes
|
||||
where supported; no new exact diagnostic format is required.
|
||||
|
||||
**Completed:** Missing-value failures now carry the entered long/short spelling
|
||||
through a neutral adapter record; earlier backend failures retain precedence.
|
||||
Built-in help conversion uses the failing occurrence's original spelling,
|
||||
including when a later valid value follows it. Adapter and public-boundary
|
||||
regressions failed before the fix and now cover alias values, clusters, help
|
||||
occurrences, entered usage paths, and input-initialization avoidance. Existing
|
||||
usage classification is unchanged. The complete offline root suite passed on
|
||||
Go 1.26.8.
|
||||
|
||||
## R-8: Reconcile the completed feature roadmap's historical descriptions
|
||||
|
||||
Status: open. Priority: P3. Category: documentation drift.
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package cobra
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||
)
|
||||
|
||||
func interpretFlagTokens(argv []string, flags []commandmeta.Flag, aliases []commandmeta.FlagAlias) ([]string, map[string][]string) {
|
||||
func interpretFlagTokens(argv []string, flags []commandmeta.Flag, aliases []commandmeta.FlagAlias) ([]string, map[string][]string, *commandmeta.Failure) {
|
||||
canonical := make(map[string]commandmeta.Flag, len(flags)*2+2)
|
||||
help := commandmeta.Flag{Name: "help", Short: "h", Boolean: true}
|
||||
canonical["--help"] = help
|
||||
@@ -61,6 +62,9 @@ func interpretFlagTokens(argv []string, flags []commandmeta.Flag, aliases []comm
|
||||
index += 2
|
||||
continue
|
||||
}
|
||||
if !flag.Boolean {
|
||||
return interpreted, spellings, &commandmeta.Failure{Message: fmt.Sprintf("flag needs an argument: %s", original)}
|
||||
}
|
||||
interpreted = append(interpreted, "--"+flag.Name)
|
||||
index++
|
||||
continue
|
||||
@@ -107,8 +111,7 @@ func interpretFlagTokens(argv []string, flags []commandmeta.Flag, aliases []comm
|
||||
consumedNext = true
|
||||
break
|
||||
}
|
||||
interpreted = append(interpreted, "--"+flag.Name)
|
||||
break
|
||||
return interpreted, spellings, &commandmeta.Failure{Message: fmt.Sprintf("flag needs an argument: %s", original)}
|
||||
}
|
||||
if !consumedNext {
|
||||
index++
|
||||
@@ -118,5 +121,5 @@ func interpretFlagTokens(argv []string, flags []commandmeta.Flag, aliases []comm
|
||||
interpreted = append(interpreted, token)
|
||||
index++
|
||||
}
|
||||
return interpreted, spellings
|
||||
return interpreted, spellings, nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package cobra
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||
@@ -158,3 +159,40 @@ func TestParseFlagAliasOccurrenceOrderAndSpellings(t *testing.T) {
|
||||
t.Fatalf("tag spellings = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFailuresRetainEnteredFlagSpellings(t *testing.T) {
|
||||
for _, spelling := range []string{"--color", "--colour", "-c", "-C"} {
|
||||
t.Run("missing/"+spelling, func(t *testing.T) {
|
||||
result := Parse(flagGrammarTree(), []string{"run", spelling}, flagGrammarOptions())
|
||||
if result.Failure == nil || !strings.Contains(result.Failure.Message, ": "+spelling) {
|
||||
t.Fatalf("missing value failure = %#v, want entered spelling %q", result.Failure, spelling)
|
||||
}
|
||||
})
|
||||
}
|
||||
result := Parse(flagGrammarTree(), []string{"run", "-wC"}, flagGrammarOptions())
|
||||
if result.Failure == nil || !strings.Contains(result.Failure.Message, ": -C") {
|
||||
t.Fatalf("cluster missing value failure = %#v", result.Failure)
|
||||
}
|
||||
for _, test := range []struct {
|
||||
args []string
|
||||
spelling string
|
||||
}{
|
||||
{[]string{"--hlep=bad"}, "--hlep"},
|
||||
{[]string{"-h=bad"}, "-h"},
|
||||
{[]string{"--help=bad"}, "--help"},
|
||||
{[]string{"--hlep=bad", "--help=true"}, "--hlep"},
|
||||
{[]string{"-h=bad", "--hlep=true"}, "-h"},
|
||||
{[]string{"--help=true", "--hlep=bad"}, "--hlep"},
|
||||
} {
|
||||
t.Run(strings.Join(test.args, " "), func(t *testing.T) {
|
||||
result := Parse(flagGrammarTree(), append([]string{"run"}, test.args...), flagGrammarOptions())
|
||||
if result.Failure == nil || !strings.Contains(result.Failure.Message, "for "+test.spelling) {
|
||||
t.Fatalf("help failure = %#v, want entered spelling %q", result.Failure, test.spelling)
|
||||
}
|
||||
})
|
||||
}
|
||||
result = Parse(flagGrammarTree(), []string{"run", "--unknown", "--colour"}, flagGrammarOptions())
|
||||
if result.Failure == nil || !strings.Contains(result.Failure.Message, "unknown") || strings.Contains(result.Failure.Message, "needs an argument") {
|
||||
t.Fatalf("earlier parser failure = %#v", result.Failure)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func (session *Session) Parse(argv []string) commandmeta.Result {
|
||||
}
|
||||
|
||||
remaining := append(commandpath.CloneTokens(unconsumed), tail...)
|
||||
interpreted, spellings := interpretFlagTokens(remaining, selectedNode.Flags, session.flagAliases)
|
||||
interpreted, spellings, interpretationFailure := interpretFlagTokens(remaining, selectedNode.Flags, session.flagAliases)
|
||||
for _, flag := range selectedNode.Flags {
|
||||
if value := selected.Flags().Lookup(flag.Name); value != nil {
|
||||
value.Value.(*rawValue).queueSpellings(spellings[flag.Name])
|
||||
@@ -89,6 +89,12 @@ func (session *Session) Parse(argv []string) commandmeta.Result {
|
||||
if err := selected.ParseFlags(interpreted); err != nil {
|
||||
return failure(result, err.Error())
|
||||
}
|
||||
// Parse the completed prefix first so an earlier backend failure keeps its
|
||||
// precedence over a missing value discovered while interpreting aliases.
|
||||
if interpretationFailure != nil {
|
||||
result.Failure = interpretationFailure
|
||||
return result
|
||||
}
|
||||
operands := selected.Flags().Args()
|
||||
|
||||
result.Operands = append([]string(nil), operands...)
|
||||
@@ -473,10 +479,14 @@ func (v *rawValue) spellingsCopy() []string { return append([]string(nil), v.spe
|
||||
|
||||
func (v *rawValue) lastBool() (bool, error) {
|
||||
var value bool
|
||||
for _, raw := range v.values {
|
||||
for index, raw := range v.values {
|
||||
parsed, err := strconv.ParseBool(raw)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("invalid boolean value %q for --help", raw)
|
||||
spelling := "--help"
|
||||
if index < len(v.spellings) {
|
||||
spelling = v.spellings[index]
|
||||
}
|
||||
return false, fmt.Errorf("invalid boolean value %q for %s", raw, spelling)
|
||||
}
|
||||
value = parsed
|
||||
}
|
||||
|
||||
@@ -11,6 +11,46 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParserFailuresRetainAliasUsageAndFlagSpellings(t *testing.T) {
|
||||
built := 0
|
||||
command := NewCommand(CommandSpec[struct{}]{
|
||||
Name: "inspect",
|
||||
NewInput: func() struct{} { built++; return struct{}{} },
|
||||
Flags: []Flag[struct{}]{ValueFlag(FlagMeta{Name: "format"}, (*string)(nil),
|
||||
func(value string) (string, error) { return value, nil },
|
||||
func(*struct{}, Supplied[string]) {})},
|
||||
Run: func(Invocation, struct{}) Outcome { t.Fatal("invalid input executed"); return Success() },
|
||||
})
|
||||
app, err := New(Identity{Name: "Demo"}, command, Alias("i", "inspect"),
|
||||
FlagAliasFor("inspect", "--fmt", "--format"),
|
||||
FlagAliasFor("inspect", "-F", "--format"), FlagAlias("--hlep", "--help"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, test := range []struct {
|
||||
args []string
|
||||
spelling string
|
||||
}{
|
||||
{[]string{"i", "--fmt"}, ": --fmt"},
|
||||
{[]string{"i", "-F"}, ": -F"},
|
||||
{[]string{"i", "--hlep=bad", "--help=true"}, "for --hlep"},
|
||||
{[]string{"i", "-h=bad"}, "for -h"},
|
||||
} {
|
||||
t.Run(strings.Join(test.args, " "), func(t *testing.T) {
|
||||
var diagnostics bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), test.args, Streams{Out: io.Discard, Err: &diagnostics})
|
||||
if outcome.Kind != OutcomeUsage || built != 0 {
|
||||
t.Fatalf("outcome = %#v, inputs built = %d", outcome, built)
|
||||
}
|
||||
for _, fragment := range []string{test.spelling, "Usage: Demo i", "Alias for: Demo inspect"} {
|
||||
if !strings.Contains(diagnostics.String(), fragment) {
|
||||
t.Errorf("diagnostics %q omit %q", diagnostics.String(), fragment)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandContextSurvivesIncompleteAndInvalidOperandCounts(t *testing.T) {
|
||||
for _, spelling := range []string{"run", "r"} {
|
||||
for _, test := range []struct {
|
||||
|
||||
Reference in New Issue
Block a user