Interpret command flag aliases in the parser
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Application-owned aliases implementation plan
|
||||
|
||||
Status: in progress; Stages 1–7 completed.
|
||||
Status: in progress; Stages 1–8 completed.
|
||||
|
||||
## 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
|
||||
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
|
||||
|
||||
**Work**
|
||||
|
||||
26
execute.go
26
execute.go
@@ -80,7 +80,6 @@ func newExecution(app *App, ctx context.Context, streams Streams) *execution {
|
||||
}
|
||||
if app != nil {
|
||||
current.snapshot = app.routeSnapshot()
|
||||
current.session = internalcobra.Prepare(current.snapshot.metadata, current.options)
|
||||
}
|
||||
return current
|
||||
}
|
||||
@@ -89,9 +88,31 @@ func (current *execution) parse(args []string) commandmeta.Result {
|
||||
if current.app == nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
if current.app == nil {
|
||||
return nil
|
||||
@@ -99,7 +120,8 @@ func (current *execution) presentationSession() *internalcobra.Session {
|
||||
snapshot := current.app.routeSnapshot()
|
||||
if current.session == nil || current.snapshot != snapshot {
|
||||
current.snapshot = snapshot
|
||||
current.session = internalcobra.Prepare(snapshot.metadata, current.options)
|
||||
current.session = nil
|
||||
current.prepareSession()
|
||||
}
|
||||
return current.session
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
commandmeta "gitea.maximumdirect.net/eric/CommandKit/internal/commandmeta"
|
||||
"gitea.maximumdirect.net/eric/CommandKit/internal/commandpath"
|
||||
)
|
||||
|
||||
@@ -28,6 +29,20 @@ type flagAliasContext struct {
|
||||
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 {
|
||||
if spelling.short {
|
||||
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
|
||||
root *spfcobra.Command
|
||||
declarations map[*spfcobra.Command]*commandmeta.Command
|
||||
flagAliases []commandmeta.FlagAlias
|
||||
}
|
||||
|
||||
// Prepare constructs a fresh backend tree for one invocation.
|
||||
@@ -31,6 +32,7 @@ func Prepare(root *commandmeta.Command, options commandmeta.ParseOptions) *Sessi
|
||||
return session
|
||||
}
|
||||
session.metadata = root
|
||||
session.flagAliases = append([]commandmeta.FlagAlias(nil), options.FlagAliases...)
|
||||
options = normalizeOptions(options)
|
||||
session.root, session.declarations = buildTree(root, options)
|
||||
return session
|
||||
@@ -75,16 +77,28 @@ func (session *Session) Parse(argv []string) commandmeta.Result {
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
operands := selected.Flags().Args()
|
||||
|
||||
result.Operands = append([]string(nil), operands...)
|
||||
result.FlagValues = make([][]string, len(selectedNode.Flags))
|
||||
result.FlagSpellings = make([][]string, len(selectedNode.Flags))
|
||||
for index, flag := range selectedNode.Flags {
|
||||
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 {
|
||||
@@ -337,15 +351,25 @@ func failure(result commandmeta.Result, message string) commandmeta.Result {
|
||||
}
|
||||
|
||||
type rawValue struct {
|
||||
values []string
|
||||
boolean bool
|
||||
values []string
|
||||
spellings []string
|
||||
pendingSpellings []string
|
||||
boolean bool
|
||||
}
|
||||
|
||||
func (v *rawValue) Set(value string) error {
|
||||
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
|
||||
}
|
||||
|
||||
func (v *rawValue) queueSpellings(spellings []string) {
|
||||
v.pendingSpellings = append(v.pendingSpellings, spellings...)
|
||||
}
|
||||
|
||||
func (v *rawValue) String() string {
|
||||
if len(v.values) > 0 {
|
||||
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) spellingsCopy() []string { return append([]string(nil), v.spellings...) }
|
||||
|
||||
func (v *rawValue) lastBool() (bool, error) {
|
||||
var value bool
|
||||
for _, raw := range v.values {
|
||||
|
||||
@@ -41,6 +41,12 @@ type Flag struct {
|
||||
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.
|
||||
type Failure struct {
|
||||
Message string
|
||||
@@ -53,14 +59,16 @@ type Result struct {
|
||||
Path []string
|
||||
HelpRequested bool
|
||||
FlagValues [][]string
|
||||
FlagSpellings [][]string
|
||||
Operands []string
|
||||
Failure *Failure
|
||||
}
|
||||
|
||||
// ParseOptions makes parser context and streams explicit.
|
||||
type ParseOptions struct {
|
||||
Context context.Context
|
||||
In io.Reader
|
||||
Out io.Writer
|
||||
Err io.Writer
|
||||
Context context.Context
|
||||
In io.Reader
|
||||
Out io.Writer
|
||||
Err io.Writer
|
||||
FlagAliases []FlagAlias
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"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) {
|
||||
app, err := New(Identity{Name: "Example"},
|
||||
Group("promptkit", "PromptKit", Group("list", "List", simpleRunnable("collections"))),
|
||||
|
||||
Reference in New Issue
Block a user