Share flag consumption between parsing and completion
This commit is contained in:
@@ -53,44 +53,26 @@ func walkCompletionFlags(tokens []string, lookup commandmeta.FlagLookup) complet
|
|||||||
state.ended = true
|
state.ended = true
|
||||||
case commandmeta.OperandToken:
|
case commandmeta.OperandToken:
|
||||||
state.operands = append(state.operands, token)
|
state.operands = append(state.operands, token)
|
||||||
case commandmeta.LongFlagToken:
|
case commandmeta.LongFlagToken, commandmeta.ShortFlagToken:
|
||||||
spelling, _, attached := commandmeta.SplitLongFlag(token)
|
for offset := 1; ; {
|
||||||
flag, ok := lookup.Find(spelling)
|
fact := commandmeta.ConsumeFlag(lookup, token, offset)
|
||||||
if !ok {
|
if !fact.Found {
|
||||||
state.invalid = true
|
state.invalid = true
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
state.supplied[flag.Name] = true
|
state.supplied[fact.Flag.Name] = true
|
||||||
if !flag.Boolean && !attached {
|
if fact.NeedsNext {
|
||||||
if index+1 == len(tokens) {
|
if index+1 == len(tokens) {
|
||||||
|
flag := fact.Flag
|
||||||
state.pending = &flag
|
state.pending = &flag
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
case commandmeta.ShortFlagToken:
|
if fact.NextOffset == 0 {
|
||||||
for offset := 1; offset < len(token); offset++ {
|
|
||||||
flag, ok := lookup.Find("-" + token[offset:offset+1])
|
|
||||||
if !ok {
|
|
||||||
state.invalid = true
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
state.supplied[flag.Name] = true
|
|
||||||
suffix := token[offset+1:]
|
|
||||||
if flag.Boolean {
|
|
||||||
if strings.HasPrefix(suffix, "=") {
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
continue
|
offset = fact.NextOffset
|
||||||
}
|
|
||||||
if suffix == "" {
|
|
||||||
if index+1 == len(tokens) {
|
|
||||||
state.pending = &flag
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,10 +114,10 @@ func completionOptionTarget(current string, flags []commandmeta.Flag, lookup com
|
|||||||
return completionTarget{kind: completionTargetFlags, choices: completionFlagNames(flags, supplied, current)}
|
return completionTarget{kind: completionTargetFlags, choices: completionFlagNames(flags, supplied, current)}
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(current, "--") {
|
if strings.HasPrefix(current, "--") {
|
||||||
spelling, value, attached := commandmeta.SplitLongFlag(current)
|
fact := commandmeta.ConsumeFlag(lookup, current, 1)
|
||||||
if attached {
|
if fact.Attached {
|
||||||
if flag, ok := lookup.Find(spelling); ok {
|
if fact.Found {
|
||||||
return completionTarget{kind: completionTargetValue, hint: flag.Completion, prefix: spelling + "=", valuePrefix: value}
|
return completionTarget{kind: completionTargetValue, hint: fact.Flag.Completion, prefix: fact.Spelling + "=", valuePrefix: fact.Value}
|
||||||
}
|
}
|
||||||
return completionTarget{}
|
return completionTarget{}
|
||||||
}
|
}
|
||||||
@@ -145,25 +127,26 @@ func completionOptionTarget(current string, flags []commandmeta.Flag, lookup com
|
|||||||
for name, value := range supplied {
|
for name, value := range supplied {
|
||||||
used[name] = value
|
used[name] = value
|
||||||
}
|
}
|
||||||
for offset := 1; offset < len(current); offset++ {
|
for offset := 1; offset < len(current); {
|
||||||
flag, ok := lookup.Find("-" + current[offset:offset+1])
|
fact := commandmeta.ConsumeFlag(lookup, current, offset)
|
||||||
if !ok {
|
if !fact.Found {
|
||||||
return completionTarget{}
|
return completionTarget{}
|
||||||
}
|
}
|
||||||
used[flag.Name] = true
|
used[fact.Flag.Name] = true
|
||||||
suffix := current[offset+1:]
|
if fact.Flag.Boolean && fact.Attached {
|
||||||
if flag.Boolean {
|
return completionTarget{kind: completionTargetValue, hint: fact.Flag.Completion, prefix: current[:offset+2], valuePrefix: fact.Value}
|
||||||
if strings.HasPrefix(suffix, "=") {
|
|
||||||
return completionTarget{kind: completionTargetValue, hint: flag.Completion, prefix: current[:offset+2], valuePrefix: suffix[1:]}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
if !fact.Flag.Boolean {
|
||||||
prefix := current[:offset+1]
|
prefix := current[:offset+1]
|
||||||
if strings.HasPrefix(suffix, "=") {
|
if strings.HasPrefix(current[offset+1:], "=") {
|
||||||
prefix += "="
|
prefix += "="
|
||||||
suffix = suffix[1:]
|
|
||||||
}
|
}
|
||||||
return completionTarget{kind: completionTargetValue, hint: flag.Completion, prefix: prefix, valuePrefix: suffix}
|
return completionTarget{kind: completionTargetValue, hint: fact.Flag.Completion, prefix: prefix, valuePrefix: fact.Value}
|
||||||
|
}
|
||||||
|
if fact.NextOffset == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
offset = fact.NextOffset
|
||||||
}
|
}
|
||||||
var choices []commandmeta.CompletionChoice
|
var choices []commandmeta.CompletionChoice
|
||||||
for _, flag := range flags {
|
for _, flag := range flags {
|
||||||
|
|||||||
@@ -79,3 +79,40 @@ func TestCompletionChoicesDoNotRestrictExecution(t *testing.T) {
|
|||||||
t.Fatalf("outcome=%#v input=%#v", outcome, received)
|
t.Fatalf("outcome=%#v input=%#v", outcome, received)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCompletionConsumesFlagGrammarAndKeepsPendingValues(t *testing.T) {
|
||||||
|
flags := []commandmeta.Flag{{Name: "verbose", Short: "v", Boolean: true}, {Name: "choice", Short: "c", Completion: commandmeta.CompletionHint{Kind: commandmeta.CompletionChoices}}}
|
||||||
|
lookup := commandmeta.NewFlagLookup(flags, []commandmeta.FlagAlias{{Source: "-x", Target: "--choice"}})
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
tokens []string
|
||||||
|
pending, operand string
|
||||||
|
invalid bool
|
||||||
|
}{
|
||||||
|
{name: "cluster attached empty", tokens: []string{"-vc="}, operand: ""},
|
||||||
|
{name: "option-looking value", tokens: []string{"-vx", "--other"}, operand: ""},
|
||||||
|
{name: "pending", tokens: []string{"-vx"}, pending: "choice"},
|
||||||
|
{name: "unknown cluster", tokens: []string{"-vz"}, invalid: true},
|
||||||
|
{name: "terminator", tokens: []string{"--", "--choice"}, operand: "--choice"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
state := walkCompletionFlags(tc.tokens, lookup)
|
||||||
|
if state.invalid != tc.invalid {
|
||||||
|
t.Fatalf("state=%#v", state)
|
||||||
|
}
|
||||||
|
if tc.pending != "" && (state.pending == nil || state.pending.Name != tc.pending) {
|
||||||
|
t.Fatalf("pending=%#v", state.pending)
|
||||||
|
}
|
||||||
|
if tc.pending == "" && state.pending != nil {
|
||||||
|
t.Fatalf("unexpected pending=%#v", state.pending)
|
||||||
|
}
|
||||||
|
if tc.operand != "" && (len(state.operands) != 1 || state.operands[0] != tc.operand) {
|
||||||
|
t.Fatalf("operands=%v", state.operands)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
target := completionOptionTarget("-vc=", flags, lookup, nil)
|
||||||
|
if target.kind != completionTargetValue || target.prefix != "-vc=" || target.valuePrefix != "" {
|
||||||
|
t.Fatalf("partial target = %#v", target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Consolidated audit catalog
|
# Consolidated audit catalog
|
||||||
|
|
||||||
Status: **13 distinct open findings: 13 P3**, consolidated from 30
|
Status: **12 distinct open findings: 12 P3**, consolidated from 30
|
||||||
numbered entries in seven audit documents. Reviewed audit state:
|
numbered entries in seven audit documents. Reviewed audit state:
|
||||||
`db732e3a1de55c844f77870e1c6db683c2685d5d`.
|
`db732e3a1de55c844f77870e1c6db683c2685d5d`.
|
||||||
|
|
||||||
@@ -335,6 +335,11 @@ F14 under the decisions recorded there and in F13.
|
|||||||
|
|
||||||
## F16 — P3: Parsing and completion duplicate flag-consumption grammar
|
## F16 — P3: Parsing and completion duplicate flag-consumption grammar
|
||||||
|
|
||||||
|
**Complete.** A neutral consumption operation is shared by execution parsing
|
||||||
|
and completion. Focused grammar, provenance and completion tests and available
|
||||||
|
Bash interactive checks pass; missing-value and partial-input policies remain
|
||||||
|
with their callers.
|
||||||
|
|
||||||
Execution parsing and completion separately implement short clusters,
|
Execution parsing and completion separately implement short clusters,
|
||||||
attached/next values, boolean assignments, terminators, and unknown spellings.
|
attached/next values, boolean assignments, terminators, and unknown spellings.
|
||||||
Shared classification helpers cover only part of the grammar, leaving changes to
|
Shared classification helpers cover only part of the grammar, leaving changes to
|
||||||
@@ -350,7 +355,7 @@ CLI behavior; no public parser or replacement backend is selected.
|
|||||||
Retain caller-specific branches where combining them would require extensive
|
Retain caller-specific branches where combining them would require extensive
|
||||||
modes or special cases. Repair F06's completion fixture before validating this
|
modes or special cases. Repair F06's completion fixture before validating this
|
||||||
refactor. The [F16 analysis](remediation-analysis.md#f16--shared-flag-consumption)
|
refactor. The [F16 analysis](remediation-analysis.md#f16--shared-flag-consumption)
|
||||||
records the accepted boundary; implementation remains pending.
|
records the accepted boundary.
|
||||||
|
|
||||||
**Original:** [03-complexity M3](03-complexity.md#m3--p3-parsing-and-completion-maintain-parallel-flag-consumption-logic).
|
**Original:** [03-complexity M3](03-complexity.md#m3--p3-parsing-and-completion-maintain-parallel-flag-consumption-logic).
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Audit remediation implementation plan
|
# Audit remediation implementation plan
|
||||||
|
|
||||||
Status: in progress; Stages 1–13 complete.
|
Status: in progress; Stages 1–14 complete.
|
||||||
|
|
||||||
Baseline: `d492c7747175c01a53261206c2b4356e53453621`. Before this sprint,
|
Baseline: `d492c7747175c01a53261206c2b4356e53453621`. Before this sprint,
|
||||||
intervening changes recorded decisions and retired the completed older
|
intervening changes recorded decisions and retired the completed older
|
||||||
@@ -470,6 +470,12 @@ Keep F24 open until the binding wrappers are handled in stage 20.
|
|||||||
|
|
||||||
## Stage 14 — Share internal flag-consumption facts
|
## Stage 14 — Share internal flag-consumption facts
|
||||||
|
|
||||||
|
**Complete.** Parsing and completion share neutral consumption facts while
|
||||||
|
retaining their separate failure and partial-input policies. Focused parser,
|
||||||
|
alias, occurrence and completion tests and both interactive Bash checks passed
|
||||||
|
in the offline aligned Go 1.27.0 workspace. Zsh and Fish remain unavailable as
|
||||||
|
recorded in Stage 6.
|
||||||
|
|
||||||
Findings: F16. Sources: `internal/commandmeta/flags.go`,
|
Findings: F16. Sources: `internal/commandmeta/flags.go`,
|
||||||
`internal/cobra/flags.go`, and `completion_flags.go`; stage 6 is prerequisite.
|
`internal/cobra/flags.go`, and `completion_flags.go`; stage 6 is prerequisite.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package cobra
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
|
||||||
commandmeta "go.maximumdirect.net/commandkit/internal/commandmeta"
|
commandmeta "go.maximumdirect.net/commandkit/internal/commandmeta"
|
||||||
)
|
)
|
||||||
@@ -20,78 +19,43 @@ func interpretFlagTokens(argv []string, flags []commandmeta.Flag, aliases []comm
|
|||||||
interpreted = append(interpreted, argv[index:]...)
|
interpreted = append(interpreted, argv[index:]...)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if kind == commandmeta.LongFlagToken {
|
if kind == commandmeta.LongFlagToken || kind == commandmeta.ShortFlagToken {
|
||||||
original, value, attached := commandmeta.SplitLongFlag(token)
|
offset := 1
|
||||||
flag, ok := lookup.Find(original)
|
|
||||||
if !ok {
|
|
||||||
interpreted = append(interpreted, token)
|
|
||||||
return interpreted, spellings, occurrences, nil
|
|
||||||
}
|
|
||||||
spellings[flag.Name] = append(spellings[flag.Name], original)
|
|
||||||
if attached {
|
|
||||||
interpreted = append(interpreted, "--"+flag.Name+"="+value)
|
|
||||||
occurrences = append(occurrences, commandmeta.FlagOccurrence{Name: flag.Name, Spelling: original, RawValue: value})
|
|
||||||
index++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !flag.Boolean && index+1 < len(argv) {
|
|
||||||
value := argv[index+1]
|
|
||||||
interpreted = append(interpreted, "--"+flag.Name+"="+value)
|
|
||||||
occurrences = append(occurrences, commandmeta.FlagOccurrence{Name: flag.Name, Spelling: original, RawValue: value})
|
|
||||||
index += 2
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !flag.Boolean {
|
|
||||||
return interpreted, spellings, occurrences, &commandmeta.Failure{Message: fmt.Sprintf("flag needs an argument: %s", original)}
|
|
||||||
}
|
|
||||||
occurrences = append(occurrences, commandmeta.FlagOccurrence{Name: flag.Name, Spelling: original, RawValue: "true"})
|
|
||||||
interpreted = append(interpreted, "--"+flag.Name)
|
|
||||||
index++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if kind == commandmeta.ShortFlagToken {
|
|
||||||
consumedNext := false
|
consumedNext := false
|
||||||
for offset := 1; offset < len(token); {
|
for {
|
||||||
original := "-" + token[offset:offset+1]
|
fact := commandmeta.ConsumeFlag(lookup, token, offset)
|
||||||
flag, ok := lookup.Find(original)
|
if !fact.Found {
|
||||||
if !ok {
|
if kind == commandmeta.ShortFlagToken {
|
||||||
interpreted = append(interpreted, "-"+token[offset:])
|
interpreted = append(interpreted, "-"+token[offset:])
|
||||||
|
} else {
|
||||||
|
interpreted = append(interpreted, token)
|
||||||
|
}
|
||||||
return interpreted, spellings, occurrences, nil
|
return interpreted, spellings, occurrences, nil
|
||||||
}
|
}
|
||||||
|
flag, original := fact.Flag, fact.Spelling
|
||||||
spellings[flag.Name] = append(spellings[flag.Name], original)
|
spellings[flag.Name] = append(spellings[flag.Name], original)
|
||||||
suffix := token[offset+1:]
|
value := fact.Value
|
||||||
if flag.Boolean {
|
if fact.NeedsNext {
|
||||||
if strings.HasPrefix(suffix, "=") {
|
if index+1 >= len(argv) {
|
||||||
value := suffix[1:]
|
|
||||||
interpreted = append(interpreted, "--"+flag.Name+"="+value)
|
|
||||||
occurrences = append(occurrences, commandmeta.FlagOccurrence{Name: flag.Name, Spelling: original, RawValue: value})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
occurrences = append(occurrences, commandmeta.FlagOccurrence{Name: flag.Name, Spelling: original, RawValue: "true"})
|
|
||||||
interpreted = append(interpreted, "--"+flag.Name)
|
|
||||||
offset++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if suffix != "" {
|
|
||||||
value := suffix
|
|
||||||
if strings.HasPrefix(value, "=") {
|
|
||||||
value = value[1:]
|
|
||||||
}
|
|
||||||
interpreted = append(interpreted, "--"+flag.Name+"="+value)
|
|
||||||
occurrences = append(occurrences, commandmeta.FlagOccurrence{Name: flag.Name, Spelling: original, RawValue: value})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if index+1 < len(argv) {
|
|
||||||
value := argv[index+1]
|
|
||||||
interpreted = append(interpreted, "--"+flag.Name+"="+value)
|
|
||||||
occurrences = append(occurrences, commandmeta.FlagOccurrence{Name: flag.Name, Spelling: original, RawValue: value})
|
|
||||||
index += 2
|
|
||||||
consumedNext = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return interpreted, spellings, occurrences, &commandmeta.Failure{Message: fmt.Sprintf("flag needs an argument: %s", original)}
|
return interpreted, spellings, occurrences, &commandmeta.Failure{Message: fmt.Sprintf("flag needs an argument: %s", original)}
|
||||||
}
|
}
|
||||||
if !consumedNext {
|
value = argv[index+1]
|
||||||
|
consumedNext = true
|
||||||
|
}
|
||||||
|
if !fact.Attached && !fact.NeedsNext {
|
||||||
|
value = "true"
|
||||||
|
interpreted = append(interpreted, "--"+flag.Name)
|
||||||
|
} else {
|
||||||
|
interpreted = append(interpreted, "--"+flag.Name+"="+value)
|
||||||
|
}
|
||||||
|
occurrences = append(occurrences, commandmeta.FlagOccurrence{Name: flag.Name, Spelling: original, RawValue: value})
|
||||||
|
if fact.NextOffset == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
offset = fact.NextOffset
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
if consumedNext {
|
||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -65,3 +65,47 @@ func (lookup FlagLookup) Find(spelling string) (Flag, bool) {
|
|||||||
}
|
}
|
||||||
return flag, ok
|
return flag, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FlagConsumption describes one flag within an option token. NextOffset is zero
|
||||||
|
// when the token is exhausted; otherwise it identifies the next short spelling.
|
||||||
|
type FlagConsumption struct {
|
||||||
|
Flag Flag
|
||||||
|
Spelling string
|
||||||
|
Found bool
|
||||||
|
Value string
|
||||||
|
Attached bool
|
||||||
|
NeedsNext bool
|
||||||
|
NextOffset int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConsumeFlag reads a long option or a short option at offset in a cluster.
|
||||||
|
func ConsumeFlag(lookup FlagLookup, token string, offset int) FlagConsumption {
|
||||||
|
var result FlagConsumption
|
||||||
|
if ClassifyFlagToken(token) == LongFlagToken {
|
||||||
|
result.Spelling, result.Value, result.Attached = SplitLongFlag(token)
|
||||||
|
result.Flag, result.Found = lookup.Find(result.Spelling)
|
||||||
|
result.NeedsNext = result.Found && !result.Flag.Boolean && !result.Attached
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
result.Spelling = "-" + token[offset:offset+1]
|
||||||
|
result.Flag, result.Found = lookup.Find(result.Spelling)
|
||||||
|
if !result.Found {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
suffix := token[offset+1:]
|
||||||
|
if result.Flag.Boolean {
|
||||||
|
if strings.HasPrefix(suffix, "=") {
|
||||||
|
result.Attached, result.Value = true, suffix[1:]
|
||||||
|
} else if suffix != "" {
|
||||||
|
result.NextOffset = offset + 1
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
if suffix == "" {
|
||||||
|
result.NeedsNext = true
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
result.Attached = true
|
||||||
|
result.Value = strings.TrimPrefix(suffix, "=")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user