Skip to content

Commit 5f20f10

Browse files
committed
fix: check duplicated sub command name and alias
1 parent b686f4f commit 5f20f10

File tree

4 files changed

+131
-2
lines changed

4 files changed

+131
-2
lines changed

app.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,6 @@ func (a *App) Run(arguments []string) (err error) {
314314
// propagate timeouts and cancellation requests
315315
func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
316316
a.Setup()
317-
318317
// handle the completion flag separately from the flagset since
319318
// completion could be attempted after a flag, but before its value was put
320319
// on the command line. this causes the flagset to interpret the completion
@@ -328,7 +327,9 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) {
328327

329328
a.rootCommand = a.newRootCommand()
330329
cCtx.Command = a.rootCommand
331-
330+
if err := checkDuplicatedCmds(a.rootCommand); err != nil {
331+
return err
332+
}
332333
return a.rootCommand.Run(cCtx, arguments...)
333334
}
334335

app_test.go

+90
Original file line numberDiff line numberDiff line change
@@ -3087,3 +3087,93 @@ func TestFlagAction(t *testing.T) {
30873087
})
30883088
}
30893089
}
3090+
3091+
func TestDuplicateSubcommand(t *testing.T) {
3092+
var testdata = []struct {
3093+
app *App
3094+
expectNoError bool
3095+
}{
3096+
{&App{
3097+
Name: "p1",
3098+
}, true},
3099+
{&App{
3100+
Name: "p2",
3101+
Commands: []*Command{},
3102+
}, true},
3103+
{&App{
3104+
Name: "p3",
3105+
Commands: []*Command{{Name: "sub1"}},
3106+
}, true},
3107+
{&App{
3108+
Name: "p4",
3109+
Commands: []*Command{{Name: "sub1"}, {Name: "sub1"}},
3110+
}, false},
3111+
{&App{
3112+
Name: "p5",
3113+
Commands: []*Command{{Name: "sub1"}, {Name: "sub2", Aliases: []string{"sub1"}}},
3114+
}, false},
3115+
{&App{
3116+
Name: "p6",
3117+
Commands: []*Command{{Name: "sub1"}, {Name: "sub2"}},
3118+
}, true},
3119+
}
3120+
for _, tt := range testdata {
3121+
err := tt.app.Run([]string{})
3122+
if tt.expectNoError {
3123+
expect(t, err, nil)
3124+
} else {
3125+
expectNotEqual(t, err, nil)
3126+
}
3127+
3128+
err = checkDuplicatedCmds(tt.app.rootCommand)
3129+
if tt.expectNoError {
3130+
expect(t, err, nil)
3131+
} else {
3132+
expectNotEqual(t, err, nil)
3133+
}
3134+
}
3135+
3136+
var testNested = []struct {
3137+
app *App
3138+
subcommandTocheck string
3139+
expectNoError bool
3140+
}{
3141+
{
3142+
&App{
3143+
Name: "p7",
3144+
Commands: []*Command{
3145+
{Name: "sub1",
3146+
Subcommands: []*Command{
3147+
{Name: "sub1_a"},
3148+
{Name: "sub1_b"},
3149+
},
3150+
},
3151+
{Name: "sub2"}},
3152+
},
3153+
"sub1",
3154+
true},
3155+
{&App{
3156+
Name: "p8",
3157+
Commands: []*Command{
3158+
{Name: "sub1",
3159+
Subcommands: []*Command{
3160+
{Name: "sub1_a"},
3161+
{Name: "sub1_a"},
3162+
},
3163+
},
3164+
{Name: "sub2"}},
3165+
},
3166+
"sub1",
3167+
false},
3168+
}
3169+
3170+
for index, tt := range testNested {
3171+
err := tt.app.Run([]string{tt.app.Name, tt.subcommandTocheck})
3172+
if tt.expectNoError {
3173+
expect(t, err, nil)
3174+
} else {
3175+
fmt.Println(index)
3176+
expectNotEqual(t, err, nil)
3177+
}
3178+
}
3179+
}

command.go

+30
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cli
22

33
import (
4+
"errors"
45
"flag"
56
"fmt"
67
"reflect"
@@ -149,6 +150,9 @@ func (c *Command) Run(cCtx *Context, arguments ...string) (err error) {
149150

150151
if !c.isRoot {
151152
c.setup(cCtx)
153+
if err := checkDuplicatedCmds(c); err != nil {
154+
return err
155+
}
152156
}
153157

154158
a := args(arguments)
@@ -404,3 +408,29 @@ func hasCommand(commands []*Command, command *Command) bool {
404408

405409
return false
406410
}
411+
412+
func checkDuplicatedCmds(parent *Command) error {
413+
if parent == nil {
414+
return nil
415+
}
416+
if len(parent.Subcommands) == 0 {
417+
return nil
418+
}
419+
420+
var (
421+
seen = make(map[string]struct{})
422+
)
423+
for _, c := range parent.Subcommands {
424+
if c == nil {
425+
continue
426+
}
427+
for _, name := range c.Names() {
428+
if _, exists := seen[name]; exists {
429+
errMsg := fmt.Sprintf("parent command [%s] has duplicated subcommand name or alias: %s", parent.Name, name)
430+
return errors.New(errMsg)
431+
}
432+
seen[name] = struct{}{}
433+
}
434+
}
435+
return nil
436+
}

helpers_test.go

+8
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,11 @@ func expect(t *testing.T, a interface{}, b interface{}) {
1717
t.Errorf("Expected %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
1818
}
1919
}
20+
21+
func expectNotEqual(t *testing.T, a interface{}, b interface{}) {
22+
t.Helper()
23+
24+
if reflect.DeepEqual(a, b) {
25+
t.Errorf("Expected not equal, but got: %v (type %v), %v (type %v) ", b, reflect.TypeOf(b), a, reflect.TypeOf(a))
26+
}
27+
}

0 commit comments

Comments
 (0)