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
|
## 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
|
**Evidence:** Typed flag conversion uses occurrence provenance, but
|
||||||
[Session.Parse](../../internal/cobra/parse.go#L89) returns normalized backend
|
[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
|
by a valid overwrite. Assert the identifying fragments and existing causes
|
||||||
where supported; no new exact diagnostic format is required.
|
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
|
## R-8: Reconcile the completed feature roadmap's historical descriptions
|
||||||
|
|
||||||
Status: open. Priority: P3. Category: documentation drift.
|
Status: open. Priority: P3. Category: documentation drift.
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package cobra
|
package cobra
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
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)
|
canonical := make(map[string]commandmeta.Flag, len(flags)*2+2)
|
||||||
help := commandmeta.Flag{Name: "help", Short: "h", Boolean: true}
|
help := commandmeta.Flag{Name: "help", Short: "h", Boolean: true}
|
||||||
canonical["--help"] = help
|
canonical["--help"] = help
|
||||||
@@ -61,6 +62,9 @@ func interpretFlagTokens(argv []string, flags []commandmeta.Flag, aliases []comm
|
|||||||
index += 2
|
index += 2
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !flag.Boolean {
|
||||||
|
return interpreted, spellings, &commandmeta.Failure{Message: fmt.Sprintf("flag needs an argument: %s", original)}
|
||||||
|
}
|
||||||
interpreted = append(interpreted, "--"+flag.Name)
|
interpreted = append(interpreted, "--"+flag.Name)
|
||||||
index++
|
index++
|
||||||
continue
|
continue
|
||||||
@@ -107,8 +111,7 @@ func interpretFlagTokens(argv []string, flags []commandmeta.Flag, aliases []comm
|
|||||||
consumedNext = true
|
consumedNext = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
interpreted = append(interpreted, "--"+flag.Name)
|
return interpreted, spellings, &commandmeta.Failure{Message: fmt.Sprintf("flag needs an argument: %s", original)}
|
||||||
break
|
|
||||||
}
|
}
|
||||||
if !consumedNext {
|
if !consumedNext {
|
||||||
index++
|
index++
|
||||||
@@ -118,5 +121,5 @@ func interpretFlagTokens(argv []string, flags []commandmeta.Flag, aliases []comm
|
|||||||
interpreted = append(interpreted, token)
|
interpreted = append(interpreted, token)
|
||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
return interpreted, spellings
|
return interpreted, spellings, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package cobra
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
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)
|
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...)
|
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 {
|
for _, flag := range selectedNode.Flags {
|
||||||
if value := selected.Flags().Lookup(flag.Name); value != nil {
|
if value := selected.Flags().Lookup(flag.Name); value != nil {
|
||||||
value.Value.(*rawValue).queueSpellings(spellings[flag.Name])
|
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 {
|
if err := selected.ParseFlags(interpreted); err != nil {
|
||||||
return failure(result, err.Error())
|
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()
|
operands := selected.Flags().Args()
|
||||||
|
|
||||||
result.Operands = append([]string(nil), operands...)
|
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) {
|
func (v *rawValue) lastBool() (bool, error) {
|
||||||
var value bool
|
var value bool
|
||||||
for _, raw := range v.values {
|
for index, raw := range v.values {
|
||||||
parsed, err := strconv.ParseBool(raw)
|
parsed, err := strconv.ParseBool(raw)
|
||||||
if err != nil {
|
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
|
value = parsed
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,46 @@ import (
|
|||||||
"testing"
|
"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) {
|
func TestCommandContextSurvivesIncompleteAndInvalidOperandCounts(t *testing.T) {
|
||||||
for _, spelling := range []string{"run", "r"} {
|
for _, spelling := range []string{"run", "r"} {
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user