Interpret command flag aliases in the parser
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Application-owned aliases implementation plan
|
# Application-owned aliases implementation plan
|
||||||
|
|
||||||
Status: in progress; Stages 1–7 completed.
|
Status: in progress; Stages 1–8 completed.
|
||||||
|
|
||||||
## Authority, baseline, and scope
|
## Authority, baseline, and scope
|
||||||
|
|
||||||
@@ -465,6 +465,13 @@ canonical/alias clusters; `-cvq` consuming `vq`; `-qv=false`; explicit empty
|
|||||||
values for long, short, and aliased spellings; missing values; unknown cluster
|
values for long, short, and aliased spellings; missing values; unknown cluster
|
||||||
members; `--message --hlep` as a value; `--message=--hlep`; post-`--` operands.
|
members; `--message --hlep` as a value; `--message=--hlep`; post-`--` operands.
|
||||||
|
|
||||||
|
**Completed:** The routed flag context now reaches the adapter through neutral
|
||||||
|
flag-alias records. One adapter pass interprets canonical and alternate long
|
||||||
|
and short spellings, preserves option values and terminator operands, and feeds
|
||||||
|
only canonical flags to pflag. Raw values retain per-occurrence source
|
||||||
|
spellings. Canonical short empty values now follow the same long-form boundary.
|
||||||
|
Verified with root and both integration module suites using the local workspace.
|
||||||
|
|
||||||
## Stage 9: Wire execution and invocation identity
|
## Stage 9: Wire execution and invocation identity
|
||||||
|
|
||||||
**Work**
|
**Work**
|
||||||
|
|||||||
26
execute.go
26
execute.go
@@ -80,7 +80,6 @@ func newExecution(app *App, ctx context.Context, streams Streams) *execution {
|
|||||||
}
|
}
|
||||||
if app != nil {
|
if app != nil {
|
||||||
current.snapshot = app.routeSnapshot()
|
current.snapshot = app.routeSnapshot()
|
||||||
current.session = internalcobra.Prepare(current.snapshot.metadata, current.options)
|
|
||||||
}
|
}
|
||||||
return current
|
return current
|
||||||
}
|
}
|
||||||
@@ -89,9 +88,31 @@ func (current *execution) parse(args []string) commandmeta.Result {
|
|||||||
if current.app == nil {
|
if current.app == nil {
|
||||||
return commandmeta.Result{Failure: &commandmeta.Failure{Message: "cannot parse commands on a nil app"}}
|
return commandmeta.Result{Failure: &commandmeta.Failure{Message: "cannot parse commands on a nil app"}}
|
||||||
}
|
}
|
||||||
|
if current.session == nil {
|
||||||
|
current.prepareSession()
|
||||||
|
}
|
||||||
return current.session.Parse(args)
|
return current.session.Parse(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (current *execution) prepareSession() {
|
||||||
|
if current.app == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if current.snapshot == nil {
|
||||||
|
current.snapshot = current.app.routeSnapshot()
|
||||||
|
}
|
||||||
|
options := current.options
|
||||||
|
if current.snapshot != nil {
|
||||||
|
switch current.route.kind {
|
||||||
|
case routeCanonical:
|
||||||
|
options.FlagAliases = current.snapshot.flagAliases[current.route.canonicalPath.Key()].neutralRecords()
|
||||||
|
case routeDiscovery:
|
||||||
|
options.FlagAliases = current.snapshot.discoveryFlags[current.route.discoveryPath.Key()].neutralRecords()
|
||||||
|
}
|
||||||
|
current.session = internalcobra.Prepare(current.snapshot.metadata, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (current *execution) presentationSession() *internalcobra.Session {
|
func (current *execution) presentationSession() *internalcobra.Session {
|
||||||
if current.app == nil {
|
if current.app == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -99,7 +120,8 @@ func (current *execution) presentationSession() *internalcobra.Session {
|
|||||||
snapshot := current.app.routeSnapshot()
|
snapshot := current.app.routeSnapshot()
|
||||||
if current.session == nil || current.snapshot != snapshot {
|
if current.session == nil || current.snapshot != snapshot {
|
||||||
current.snapshot = snapshot
|
current.snapshot = snapshot
|
||||||
current.session = internalcobra.Prepare(snapshot.metadata, current.options)
|
current.session = nil
|
||||||
|
current.prepareSession()
|
||||||
}
|
}
|
||||||
return current.session
|
return current.session
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||||
"gitea.maximumdirect.net/eric/CommandKit/internal/commandpath"
|
"gitea.maximumdirect.net/eric/CommandKit/internal/commandpath"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,6 +29,20 @@ type flagAliasContext struct {
|
|||||||
discovery bool
|
discovery bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (aliases resolvedFlagAliases) neutralRecords() []commandmeta.FlagAlias {
|
||||||
|
keys := make([]string, 0, len(aliases.bySpelling))
|
||||||
|
for key := range aliases.bySpelling {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
records := make([]commandmeta.FlagAlias, 0, len(keys))
|
||||||
|
for _, key := range keys {
|
||||||
|
alias := aliases.bySpelling[key]
|
||||||
|
records = append(records, commandmeta.FlagAlias{Source: alias.source.token(), Target: alias.target.token()})
|
||||||
|
}
|
||||||
|
return records
|
||||||
|
}
|
||||||
|
|
||||||
func (spelling capturedFlagSpelling) token() string {
|
func (spelling capturedFlagSpelling) token() string {
|
||||||
if spelling.short {
|
if spelling.short {
|
||||||
return "-" + spelling.name
|
return "-" + spelling.name
|
||||||
|
|||||||
122
internal/cobra/flags.go
Normal file
122
internal/cobra/flags.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package cobra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||||
|
)
|
||||||
|
|
||||||
|
func interpretFlagTokens(argv []string, flags []commandmeta.Flag, aliases []commandmeta.FlagAlias) ([]string, map[string][]string) {
|
||||||
|
canonical := make(map[string]commandmeta.Flag, len(flags)*2+2)
|
||||||
|
help := commandmeta.Flag{Name: "help", Short: "h", Boolean: true}
|
||||||
|
canonical["--help"] = help
|
||||||
|
canonical["-h"] = help
|
||||||
|
for _, flag := range flags {
|
||||||
|
canonical["--"+flag.Name] = flag
|
||||||
|
if flag.Short != "" {
|
||||||
|
canonical["-"+flag.Short] = flag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byAlias := make(map[string]commandmeta.Flag, len(aliases))
|
||||||
|
for _, alias := range aliases {
|
||||||
|
if target, ok := canonical[alias.Target]; ok {
|
||||||
|
byAlias[alias.Source] = target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spellings := make(map[string][]string)
|
||||||
|
interpreted := make([]string, 0, len(argv))
|
||||||
|
for index := 0; index < len(argv); {
|
||||||
|
token := argv[index]
|
||||||
|
if token == "--" {
|
||||||
|
interpreted = append(interpreted, argv[index:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(token, "--") && len(token) > 2 {
|
||||||
|
body := token[2:]
|
||||||
|
equal := strings.IndexByte(body, '=')
|
||||||
|
inputName := body
|
||||||
|
if equal >= 0 {
|
||||||
|
inputName = body[:equal]
|
||||||
|
}
|
||||||
|
original := "--" + inputName
|
||||||
|
flag, ok := canonical[original]
|
||||||
|
if !ok {
|
||||||
|
flag, ok = byAlias[original]
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
interpreted = append(interpreted, token)
|
||||||
|
index++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
spellings[flag.Name] = append(spellings[flag.Name], original)
|
||||||
|
if equal >= 0 {
|
||||||
|
interpreted = append(interpreted, "--"+flag.Name+"="+body[equal+1:])
|
||||||
|
index++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !flag.Boolean && index+1 < len(argv) && argv[index+1] != "--" {
|
||||||
|
interpreted = append(interpreted, "--"+flag.Name+"="+argv[index+1])
|
||||||
|
index += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
interpreted = append(interpreted, "--"+flag.Name)
|
||||||
|
index++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(token) > 1 && token[0] == '-' {
|
||||||
|
if token == "-" {
|
||||||
|
interpreted = append(interpreted, token)
|
||||||
|
index++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
consumedNext := false
|
||||||
|
for offset := 1; offset < len(token); {
|
||||||
|
original := "-" + token[offset:offset+1]
|
||||||
|
flag, ok := canonical[original]
|
||||||
|
if !ok {
|
||||||
|
flag, ok = byAlias[original]
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
interpreted = append(interpreted, "-"+token[offset:])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
spellings[flag.Name] = append(spellings[flag.Name], original)
|
||||||
|
suffix := token[offset+1:]
|
||||||
|
if flag.Boolean {
|
||||||
|
if strings.HasPrefix(suffix, "=") {
|
||||||
|
interpreted = append(interpreted, "--"+flag.Name+"="+suffix[1:])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
interpreted = append(interpreted, "--"+flag.Name)
|
||||||
|
offset++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if suffix != "" {
|
||||||
|
value := suffix
|
||||||
|
if strings.HasPrefix(value, "=") {
|
||||||
|
value = value[1:]
|
||||||
|
}
|
||||||
|
interpreted = append(interpreted, "--"+flag.Name+"="+value)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if index+1 < len(argv) && argv[index+1] != "--" {
|
||||||
|
interpreted = append(interpreted, "--"+flag.Name+"="+argv[index+1])
|
||||||
|
index += 2
|
||||||
|
consumedNext = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
interpreted = append(interpreted, "--"+flag.Name)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !consumedNext {
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
interpreted = append(interpreted, token)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
return interpreted, spellings
|
||||||
|
}
|
||||||
137
internal/cobra/flags_test.go
Normal file
137
internal/cobra/flags_test.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package cobra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||||
|
)
|
||||||
|
|
||||||
|
func flagGrammarTree() *commandmeta.Command {
|
||||||
|
return &commandmeta.Command{Name: "app", Children: []*commandmeta.Command{{
|
||||||
|
Name: "run", Runnable: true,
|
||||||
|
Flags: []commandmeta.Flag{
|
||||||
|
{Name: "color", Short: "c"},
|
||||||
|
{Name: "verbose", Short: "v", Boolean: true},
|
||||||
|
{Name: "quiet", Short: "q", Boolean: true},
|
||||||
|
{Name: "message", Short: "m"},
|
||||||
|
{Name: "tag", Short: "t", Repeated: true},
|
||||||
|
},
|
||||||
|
}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func flagGrammarOptions() ParseOptions {
|
||||||
|
return ParseOptions{FlagAliases: []commandmeta.FlagAlias{
|
||||||
|
{Source: "--colour", Target: "--color"},
|
||||||
|
{Source: "-C", Target: "--color"},
|
||||||
|
{Source: "-w", Target: "--verbose"},
|
||||||
|
{Source: "-z", Target: "--quiet"},
|
||||||
|
{Source: "--taggy", Target: "-t"},
|
||||||
|
{Source: "--hlep", Target: "--help"},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseCanonicalAndAliasShortGrammar(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
values [][]string
|
||||||
|
spellings [][]string
|
||||||
|
}{
|
||||||
|
{"boolean cluster", []string{"run", "-vq"}, [][]string{nil, {"true"}, {"true"}, nil, nil}, [][]string{nil, {"-v"}, {"-q"}, nil, nil}},
|
||||||
|
{"aliased boolean cluster", []string{"run", "-wz"}, [][]string{nil, {"true"}, {"true"}, nil, nil}, [][]string{nil, {"-w"}, {"-z"}, nil, nil}},
|
||||||
|
{"value short separated", []string{"run", "-c", "blue"}, [][]string{{"blue"}, nil, nil, nil, nil}, [][]string{{"-c"}, nil, nil, nil, nil}},
|
||||||
|
{"value short equals", []string{"run", "-c=blue"}, [][]string{{"blue"}, nil, nil, nil, nil}, [][]string{{"-c"}, nil, nil, nil, nil}},
|
||||||
|
{"value short attached", []string{"run", "-cblue"}, [][]string{{"blue"}, nil, nil, nil, nil}, [][]string{{"-c"}, nil, nil, nil, nil}},
|
||||||
|
{"long alias", []string{"run", "--colour=blue"}, [][]string{{"blue"}, nil, nil, nil, nil}, [][]string{{"--colour"}, nil, nil, nil, nil}},
|
||||||
|
{"short alias", []string{"run", "-Cblue"}, [][]string{{"blue"}, nil, nil, nil, nil}, [][]string{{"-C"}, nil, nil, nil, nil}},
|
||||||
|
{"cluster value consumes suffix", []string{"run", "-vqcblue"}, [][]string{{"blue"}, {"true"}, {"true"}, nil, nil}, [][]string{{"-c"}, {"-v"}, {"-q"}, nil, nil}},
|
||||||
|
{"cluster value consumes next", []string{"run", "-vqc", "blue"}, [][]string{{"blue"}, {"true"}, {"true"}, nil, nil}, [][]string{{"-c"}, {"-v"}, {"-q"}, nil, nil}},
|
||||||
|
{"mixed canonical and alias cluster", []string{"run", "-wqcblue"}, [][]string{{"blue"}, {"true"}, {"true"}, nil, nil}, [][]string{{"-c"}, {"-w"}, {"-q"}, nil, nil}},
|
||||||
|
{"value takes remaining suffix", []string{"run", "-cvq"}, [][]string{{"vq"}, nil, nil, nil, nil}, [][]string{{"-c"}, nil, nil, nil, nil}},
|
||||||
|
{"boolean explicit false", []string{"run", "-v=false"}, [][]string{nil, {"false"}, nil, nil, nil}, [][]string{nil, {"-v"}, nil, nil, nil}},
|
||||||
|
{"boolean explicit false in cluster", []string{"run", "-qv=false"}, [][]string{nil, {"false"}, {"true"}, nil, nil}, [][]string{nil, {"-v"}, {"-q"}, nil, nil}},
|
||||||
|
{"empty canonical short", []string{"run", "-c="}, [][]string{{""}, nil, nil, nil, nil}, [][]string{{"-c"}, nil, nil, nil, nil}},
|
||||||
|
{"empty canonical long", []string{"run", "--color="}, [][]string{{""}, nil, nil, nil, nil}, [][]string{{"--color"}, nil, nil, nil, nil}},
|
||||||
|
{"empty separated short", []string{"run", "-c", ""}, [][]string{{""}, nil, nil, nil, nil}, [][]string{{"-c"}, nil, nil, nil, nil}},
|
||||||
|
{"empty aliased long", []string{"run", "--colour="}, [][]string{{""}, nil, nil, nil, nil}, [][]string{{"--colour"}, nil, nil, nil, nil}},
|
||||||
|
{"empty aliased short", []string{"run", "-C="}, [][]string{{""}, nil, nil, nil, nil}, [][]string{{"-C"}, nil, nil, nil, nil}},
|
||||||
|
{"empty boolean short", []string{"run", "-v="}, [][]string{nil, {""}, nil, nil, nil}, [][]string{nil, {"-v"}, nil, nil, nil}},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
result := Parse(flagGrammarTree(), test.args, flagGrammarOptions())
|
||||||
|
if result.Failure != nil {
|
||||||
|
t.Fatalf("parse failed: %s", result.Failure.Message)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(result.FlagValues, test.values) {
|
||||||
|
t.Errorf("values = %#v, want %#v", result.FlagValues, test.values)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(result.FlagSpellings, test.spellings) {
|
||||||
|
t.Errorf("spellings = %#v, want %#v", result.FlagSpellings, test.spellings)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseFlagAliasesPreserveValuesAndTerminators(t *testing.T) {
|
||||||
|
result := Parse(flagGrammarTree(), []string{"run", "--message", "--hlep", "--message=--hlep", "--taggy", "one", "--", "--taggy"}, flagGrammarOptions())
|
||||||
|
if result.Failure != nil {
|
||||||
|
t.Fatalf("parse failed: %s", result.Failure.Message)
|
||||||
|
}
|
||||||
|
if got, want := result.FlagValues[3], []string{"--hlep", "--hlep"}; !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("message values = %#v, want %#v", got, want)
|
||||||
|
}
|
||||||
|
if got, want := result.FlagSpellings[3], []string{"--message", "--message"}; !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("message spellings = %#v, want %#v", got, want)
|
||||||
|
}
|
||||||
|
if got, want := result.FlagValues[4], []string{"one"}; !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("tag values = %#v, want %#v", got, want)
|
||||||
|
}
|
||||||
|
if got, want := result.Operands, []string{"--taggy"}; !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("operands = %#v, want %#v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseHelpAliasAndUnknownShortMembers(t *testing.T) {
|
||||||
|
result := Parse(flagGrammarTree(), []string{"run", "--hlep"}, flagGrammarOptions())
|
||||||
|
if result.Failure != nil || !result.HelpRequested {
|
||||||
|
t.Fatalf("help alias result = %#v", result)
|
||||||
|
}
|
||||||
|
result = Parse(flagGrammarTree(), []string{"run", "-vx"}, flagGrammarOptions())
|
||||||
|
if result.Failure == nil || result.Failure.Message == "" {
|
||||||
|
t.Fatalf("unknown short cluster member result = %#v", result)
|
||||||
|
}
|
||||||
|
result = Parse(flagGrammarTree(), []string{"run", "--color"}, flagGrammarOptions())
|
||||||
|
if result.Failure == nil || result.Failure.Message == "" {
|
||||||
|
t.Fatalf("missing value result = %#v", result)
|
||||||
|
}
|
||||||
|
result = Parse(flagGrammarTree(), []string{"run", "--colour"}, flagGrammarOptions())
|
||||||
|
if result.Failure == nil || result.Failure.Message == "" {
|
||||||
|
t.Fatalf("missing aliased value result = %#v", result)
|
||||||
|
}
|
||||||
|
result = Parse(flagGrammarTree(), []string{"run", "-c"}, flagGrammarOptions())
|
||||||
|
if result.Failure == nil || result.Failure.Message == "" {
|
||||||
|
t.Fatalf("missing short value result = %#v", result)
|
||||||
|
}
|
||||||
|
result = Parse(flagGrammarTree(), []string{"run", "-C"}, flagGrammarOptions())
|
||||||
|
if result.Failure == nil || result.Failure.Message == "" {
|
||||||
|
t.Fatalf("missing aliased short value result = %#v", result)
|
||||||
|
}
|
||||||
|
result = Parse(flagGrammarTree(), []string{"run", "-vfalse"}, flagGrammarOptions())
|
||||||
|
if result.Failure == nil || result.Failure.Message == "" {
|
||||||
|
t.Fatalf("boolean suffix without equals result = %#v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseFlagAliasOccurrenceOrderAndSpellings(t *testing.T) {
|
||||||
|
result := Parse(flagGrammarTree(), []string{"run", "--taggy=first", "-t", "second", "--taggy", "third"}, flagGrammarOptions())
|
||||||
|
if result.Failure != nil {
|
||||||
|
t.Fatalf("parse failed: %s", result.Failure.Message)
|
||||||
|
}
|
||||||
|
if got, want := result.FlagValues[4], []string{"first", "second", "third"}; !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("tag values = %#v, want %#v", got, want)
|
||||||
|
}
|
||||||
|
if got, want := result.FlagSpellings[4], []string{"--taggy", "-t", "--taggy"}; !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("tag spellings = %#v, want %#v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ type Session struct {
|
|||||||
metadata *commandmeta.Command
|
metadata *commandmeta.Command
|
||||||
root *spfcobra.Command
|
root *spfcobra.Command
|
||||||
declarations map[*spfcobra.Command]*commandmeta.Command
|
declarations map[*spfcobra.Command]*commandmeta.Command
|
||||||
|
flagAliases []commandmeta.FlagAlias
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare constructs a fresh backend tree for one invocation.
|
// Prepare constructs a fresh backend tree for one invocation.
|
||||||
@@ -31,6 +32,7 @@ func Prepare(root *commandmeta.Command, options commandmeta.ParseOptions) *Sessi
|
|||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
session.metadata = root
|
session.metadata = root
|
||||||
|
session.flagAliases = append([]commandmeta.FlagAlias(nil), options.FlagAliases...)
|
||||||
options = normalizeOptions(options)
|
options = normalizeOptions(options)
|
||||||
session.root, session.declarations = buildTree(root, options)
|
session.root, session.declarations = buildTree(root, options)
|
||||||
return session
|
return session
|
||||||
@@ -75,16 +77,28 @@ func (session *Session) Parse(argv []string) commandmeta.Result {
|
|||||||
}
|
}
|
||||||
|
|
||||||
remaining := append(commandpath.CloneTokens(unconsumed), tail...)
|
remaining := append(commandpath.CloneTokens(unconsumed), tail...)
|
||||||
if err := selected.ParseFlags(remaining); err != nil {
|
interpreted, spellings := 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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if helpFlag := selected.Flags().Lookup("help"); helpFlag != nil {
|
||||||
|
helpFlag.Value.(*rawValue).queueSpellings(spellings["help"])
|
||||||
|
}
|
||||||
|
if err := selected.ParseFlags(interpreted); err != nil {
|
||||||
return failure(result, err.Error())
|
return failure(result, err.Error())
|
||||||
}
|
}
|
||||||
operands := selected.Flags().Args()
|
operands := selected.Flags().Args()
|
||||||
|
|
||||||
result.Operands = append([]string(nil), operands...)
|
result.Operands = append([]string(nil), operands...)
|
||||||
result.FlagValues = make([][]string, len(selectedNode.Flags))
|
result.FlagValues = make([][]string, len(selectedNode.Flags))
|
||||||
|
result.FlagSpellings = make([][]string, len(selectedNode.Flags))
|
||||||
for index, flag := range selectedNode.Flags {
|
for index, flag := range selectedNode.Flags {
|
||||||
if value := selected.Flags().Lookup(flag.Name); value != nil {
|
if value := selected.Flags().Lookup(flag.Name); value != nil {
|
||||||
result.FlagValues[index] = value.Value.(*rawValue).valuesCopy()
|
raw := value.Value.(*rawValue)
|
||||||
|
result.FlagValues[index] = raw.valuesCopy()
|
||||||
|
result.FlagSpellings[index] = raw.spellingsCopy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if helpFlag := selected.Flags().Lookup("help"); helpFlag != nil {
|
if helpFlag := selected.Flags().Lookup("help"); helpFlag != nil {
|
||||||
@@ -337,15 +351,25 @@ func failure(result commandmeta.Result, message string) commandmeta.Result {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type rawValue struct {
|
type rawValue struct {
|
||||||
values []string
|
values []string
|
||||||
boolean bool
|
spellings []string
|
||||||
|
pendingSpellings []string
|
||||||
|
boolean bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *rawValue) Set(value string) error {
|
func (v *rawValue) Set(value string) error {
|
||||||
v.values = append(v.values, value)
|
v.values = append(v.values, value)
|
||||||
|
if len(v.pendingSpellings) > 0 {
|
||||||
|
v.spellings = append(v.spellings, v.pendingSpellings[0])
|
||||||
|
v.pendingSpellings = v.pendingSpellings[1:]
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *rawValue) queueSpellings(spellings []string) {
|
||||||
|
v.pendingSpellings = append(v.pendingSpellings, spellings...)
|
||||||
|
}
|
||||||
|
|
||||||
func (v *rawValue) String() string {
|
func (v *rawValue) String() string {
|
||||||
if len(v.values) > 0 {
|
if len(v.values) > 0 {
|
||||||
return v.values[len(v.values)-1]
|
return v.values[len(v.values)-1]
|
||||||
@@ -364,6 +388,8 @@ func (v *rawValue) IsBoolFlag() bool { return v.boolean }
|
|||||||
|
|
||||||
func (v *rawValue) valuesCopy() []string { return append([]string(nil), v.values...) }
|
func (v *rawValue) valuesCopy() []string { return append([]string(nil), v.values...) }
|
||||||
|
|
||||||
|
func (v *rawValue) spellingsCopy() []string { return append([]string(nil), v.spellings...) }
|
||||||
|
|
||||||
func (v *rawValue) lastBool() (bool, error) {
|
func (v *rawValue) lastBool() (bool, error) {
|
||||||
var value bool
|
var value bool
|
||||||
for _, raw := range v.values {
|
for _, raw := range v.values {
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ type Flag struct {
|
|||||||
Required bool
|
Required bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FlagAlias maps one option spelling to a canonical flag spelling.
|
||||||
|
type FlagAlias struct {
|
||||||
|
Source string
|
||||||
|
Target string
|
||||||
|
}
|
||||||
|
|
||||||
// Failure is a neutral parsing or presentation failure.
|
// Failure is a neutral parsing or presentation failure.
|
||||||
type Failure struct {
|
type Failure struct {
|
||||||
Message string
|
Message string
|
||||||
@@ -53,14 +59,16 @@ type Result struct {
|
|||||||
Path []string
|
Path []string
|
||||||
HelpRequested bool
|
HelpRequested bool
|
||||||
FlagValues [][]string
|
FlagValues [][]string
|
||||||
|
FlagSpellings [][]string
|
||||||
Operands []string
|
Operands []string
|
||||||
Failure *Failure
|
Failure *Failure
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseOptions makes parser context and streams explicit.
|
// ParseOptions makes parser context and streams explicit.
|
||||||
type ParseOptions struct {
|
type ParseOptions struct {
|
||||||
Context context.Context
|
Context context.Context
|
||||||
In io.Reader
|
In io.Reader
|
||||||
Out io.Writer
|
Out io.Writer
|
||||||
Err io.Writer
|
Err io.Writer
|
||||||
|
FlagAliases []FlagAlias
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -189,6 +190,46 @@ func TestExecuteDispatchesEnteredAliasToCanonicalRunner(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExecuteAppliesCanonicalCommandFlagAliasesAfterRouting(t *testing.T) {
|
||||||
|
type input struct{ color string }
|
||||||
|
var got string
|
||||||
|
command := NewCommand(CommandSpec[input]{
|
||||||
|
Name: "inspect",
|
||||||
|
Flags: []Flag[input]{ValueFlag(FlagMeta{Name: "color", Short: "c"}, (*string)(nil),
|
||||||
|
func(value string) (string, error) { return value, nil },
|
||||||
|
func(target *input, supplied Supplied[string]) { target.color = supplied.Value },
|
||||||
|
)},
|
||||||
|
Run: func(_ Invocation, value input) Outcome {
|
||||||
|
got = value.color
|
||||||
|
return Success()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
app, err := New(Identity{Name: "Example"}, command,
|
||||||
|
Alias("shortcut", "inspect"),
|
||||||
|
FlagAliasFor("inspect", "--colour", "-c"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outcome := app.Execute(context.Background(), []string{"shortcut", "--colour=blue"}, Streams{Out: io.Discard, Err: io.Discard})
|
||||||
|
if outcome.Kind != OutcomeSuccess || got != "blue" {
|
||||||
|
t.Fatalf("alias flag execution = (%v, color %q), want success and blue", outcome.Kind, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecuteAppliesGlobalHelpAliasAtApplicationRoot(t *testing.T) {
|
||||||
|
app, err := New(Identity{Name: "Example"}, FlagAlias("--hlep", "--help"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var output bytes.Buffer
|
||||||
|
outcome := app.Execute(context.Background(), []string{"--hlep"}, Streams{Out: &output, Err: io.Discard})
|
||||||
|
if outcome.Kind != OutcomeSuccess || !strings.Contains(output.String(), "Usage:") {
|
||||||
|
t.Fatalf("root help alias outcome = %v, output %q", outcome.Kind, output.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAliasOnlyPrefixProducesDiscoveryRoute(t *testing.T) {
|
func TestAliasOnlyPrefixProducesDiscoveryRoute(t *testing.T) {
|
||||||
app, err := New(Identity{Name: "Example"},
|
app, err := New(Identity{Name: "Example"},
|
||||||
Group("promptkit", "PromptKit", Group("list", "List", simpleRunnable("collections"))),
|
Group("promptkit", "PromptKit", Group("list", "List", simpleRunnable("collections"))),
|
||||||
|
|||||||
Reference in New Issue
Block a user