Skip to content

Commit bccb3d6

Browse files
committed
refactor: build subcommand paths at once during full validation
1 parent e36f261 commit bccb3d6

2 files changed

Lines changed: 64 additions & 13 deletions

File tree

builder.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,11 @@ func (c *CommandInfo) prepareAndValidate() {
9696
}
9797
}
9898
}
99-
10099
for i := range c.Subcmds {
100+
c.Subcmds[i].Path = make([]string, len(c.Path)+1)
101+
copy(c.Subcmds[i].Path, c.Path)
102+
c.Subcmds[i].Path[len(c.Subcmds[i].Path)-1] = c.Subcmds[i].Name
103+
101104
c.Subcmds[i].prepareAndValidate()
102105
}
103106
}
@@ -167,15 +170,13 @@ func (c CommandInfo) Arg(a InputInfo) CommandInfo {
167170
return c
168171
}
169172

173+
// Subcmd adds the given [CommandInfo] sc as a subcommand under c. This function will
174+
// panic if c already has at least one positional argument because commands cannot contain
175+
// both positional arguments and subcommands simultaneously.
170176
func (c CommandInfo) Subcmd(sc CommandInfo) CommandInfo {
171177
if len(c.Args) > 0 {
172178
panic(errMixingPosArgsAndSubcmds)
173179
}
174-
175-
sc.Path = make([]string, len(c.Path))
176-
copy(sc.Path, c.Path)
177-
sc.Path = append(sc.Path, sc.Name)
178-
179180
c.Subcmds = append(c.Subcmds, sc)
180181
return c
181182
}

cli_test.go

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,7 @@ func TestParsing(t *testing.T) {
482482
t.Fatalf("expected no error, got %[1]T: %[1]v", gotErr)
483483
}
484484
if !errors.Is(gotErr, tio.expErr) {
485-
t.Fatalf("tt:%s: errors don't match:\nexpected: (%[2]T) %+#[2]v\n got: (%[3]T) %+#[3]v",
485+
t.Fatalf("%s: errors don't match:\nexpected: (%[2]T) %+#[2]v\n got: (%[3]T) %+#[3]v",
486486
tio.Case, tio.expErr, gotErr)
487487
}
488488
return
@@ -502,29 +502,29 @@ func cmpParsed(t *testing.T, tioInfo string, exp, got *Command) {
502502
gotNumInputs := len(got.Inputs)
503503
expNumInputs := len(exp.Inputs)
504504
if gotNumInputs != expNumInputs {
505-
t.Fatalf("tt:%s: expected %d parsed options, got %d", tioInfo, expNumInputs, gotNumInputs)
505+
t.Fatalf("%s: expected %d parsed options, got %d", tioInfo, expNumInputs, gotNumInputs)
506506
}
507507
for i, gotOpt := range got.Inputs {
508508
expOpt := exp.Inputs[i]
509509
if !reflect.DeepEqual(gotOpt, expOpt) {
510-
t.Errorf("tt:%s: parsed options[%d]:\nexpected %+#v\n got %+#v", tioInfo, i, expOpt, gotOpt)
510+
t.Errorf("%s: parsed options[%d]:\nexpected %+#v\n got %+#v", tioInfo, i, expOpt, gotOpt)
511511
}
512512
}
513513
}
514514
// surplus args
515515
{
516516
if !slices.Equal(got.Surplus, exp.Surplus) {
517-
t.Errorf("tt:%s: surplus args:\nexpected %+#v\n got %+#v",
517+
t.Errorf("%s: surplus args:\nexpected %+#v\n got %+#v",
518518
tioInfo, exp.Surplus, got.Surplus)
519519
}
520520
}
521521
// subcommand
522522
{
523523
switch {
524524
case got.Subcmd == nil && exp.Subcmd != nil:
525-
t.Errorf("tt:%s:\nexpected subcommand %+v\ngot nil", tioInfo, exp.Subcmd)
525+
t.Errorf("%s:\nexpected subcommand %+v\ngot nil", tioInfo, exp.Subcmd)
526526
case got.Subcmd != nil && exp.Subcmd == nil:
527-
t.Errorf("tt:%s:\ndid not expect a subcommand\ngot %+v", tioInfo, got.Subcmd)
527+
t.Errorf("%s:\ndid not expect a subcommand\ngot %+v", tioInfo, got.Subcmd)
528528
case got.Subcmd != nil && exp.Subcmd != nil:
529529
cmpParsed(t, tioInfo, exp.Subcmd, got.Subcmd)
530530
}
@@ -829,7 +829,57 @@ func TestArgLookups(t *testing.T) {
829829
}
830830
}
831831

832+
func TestHelpSubcommands(t *testing.T) {
833+
for _, tt := range []struct {
834+
Case string
835+
cmd CommandInfo
836+
cliArgs []string
837+
expHelpMsg string
838+
}{
839+
{
840+
Case: ttCase(),
841+
cmd: NewCmd("example").
842+
Help("nested two levels").
843+
Subcmd(NewCmd("a").
844+
Subcmd(NewCmd("b").
845+
Help("subcommand b"))),
846+
cliArgs: []string{"a", "b", "-h"},
847+
expHelpMsg: `example a b - subcommand b
848+
849+
usage:
850+
b [options]
851+
852+
options:
853+
-h, --help Show this help message and exit.
854+
`,
855+
}, {
856+
Case: ttCase(),
857+
cmd: NewCmd("example").
858+
Help("nested three levels").
859+
Subcmd(NewCmd("a").
860+
Subcmd(NewCmd("b").
861+
Subcmd(NewCmd("c").
862+
Help("subcommand c")))),
863+
cliArgs: []string{"a", "b", "c", "-h"},
864+
expHelpMsg: `example a b c - subcommand c
865+
866+
usage:
867+
c [options]
868+
869+
options:
870+
-h, --help Show this help message and exit.
871+
`,
872+
},
873+
} {
874+
_, err := tt.cmd.Parse(tt.cliArgs...)
875+
gotHelpMsg := err.Error()
876+
if gotHelpMsg != tt.expHelpMsg {
877+
t.Errorf("%s: expected:\n%s\ngot:\n%s", tt.Case, tt.expHelpMsg, gotHelpMsg)
878+
}
879+
}
880+
}
881+
832882
func ttCase() string {
833883
_, _, line, _ := runtime.Caller(1)
834-
return fmt.Sprintf("%d", line)
884+
return fmt.Sprintf("tt:%d", line)
835885
}

0 commit comments

Comments
 (0)