From ad20753c5d0b8d89f5b5a25c1a44e0feb1948e40 Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Sat, 6 Jun 2026 08:45:19 -0400 Subject: [PATCH 1/2] feat: add Command.Walk() and Command.Path() convenience methods --- command.go | 27 ++++++++++++++-- command_test.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/command.go b/command.go index d1fc008e78..e02495fe6a 100644 --- a/command.go +++ b/command.go @@ -169,13 +169,34 @@ type Command struct { // For commands with parents this ensures that the parent commands // are part of the command path. func (cmd *Command) FullName() string { - namePath := []string{} + return strings.Join(cmd.Path(), " ") +} +// Path returns the path of command names from the root to cmd, inclusive. +// Each element is a Command.Name. FullName() is equivalent to +// strings.Join(cmd.Path(), " "). +func (cmd *Command) Path() []string { if cmd.parent != nil { - namePath = append(namePath, cmd.parent.FullName()) + return append(cmd.parent.Path(), cmd.Name) } + return []string{cmd.Name} +} - return strings.Join(append(namePath, cmd.Name), " ") +// Walk visits cmd and every descendant in pre-order. Returning a non-nil +// error from fn short-circuits the walk and is returned to the caller. +func (cmd *Command) Walk(fn func(*Command) error) error { + if fn == nil { + return nil + } + if err := fn(cmd); err != nil { + return err + } + for _, sub := range cmd.Commands { + if err := sub.Walk(fn); err != nil { + return err + } + } + return nil } func (cmd *Command) Command(name string) *Command { diff --git a/command_test.go b/command_test.go index 24ce5e4739..b5eb300ed7 100644 --- a/command_test.go +++ b/command_test.go @@ -5827,3 +5827,86 @@ func TestCommand_NoDefaultCmdArgMatchingFlag(t *testing.T) { require.NoError(t, err) require.Equal(t, &expectedArgs, actualArgs) } + +func TestCommand_Path(t *testing.T) { + subCmd := &Command{Name: "bar"} + subSubCmd := &Command{Name: "baz"} + subCmd.Commands = []*Command{subSubCmd} + + cmd := &Command{ + Name: "foo", + Commands: []*Command{subCmd}, + } + + require.NoError(t, cmd.Run(buildTestContext(t), []string{"foo", "bar", "baz"})) + + assert.Equal(t, []string{"foo"}, cmd.Path()) + assert.Equal(t, []string{"foo", "bar"}, subCmd.Path()) + assert.Equal(t, []string{"foo", "bar", "baz"}, subSubCmd.Path()) +} + +func TestCommand_Walk(t *testing.T) { + subCmd := &Command{Name: "bar"} + subSubCmd := &Command{Name: "baz"} + subCmd.Commands = []*Command{subSubCmd} + + cmd := &Command{ + Name: "foo", + Commands: []*Command{subCmd}, + } + + var visited []string + err := cmd.Walk(func(c *Command) error { + visited = append(visited, c.Name) + return nil + }) + require.NoError(t, err) + assert.Equal(t, []string{"foo", "bar", "baz"}, visited) +} + +func TestCommand_Walk_ShortCircuit(t *testing.T) { + subCmd := &Command{Name: "bar"} + subSubCmd := &Command{Name: "baz"} + subCmd.Commands = []*Command{subSubCmd} + + cmd := &Command{ + Name: "foo", + Commands: []*Command{subCmd}, + } + + errWalk := fmt.Errorf("stop") + var visited []string + err := cmd.Walk(func(c *Command) error { + visited = append(visited, c.Name) + if c.Name == "bar" { + return errWalk + } + return nil + }) + assert.ErrorIs(t, err, errWalk) + assert.Equal(t, []string{"foo", "bar"}, visited) +} + +func TestCommand_Walk_Hidden(t *testing.T) { + subCmd := &Command{Name: "bar", HideHelp: true} + subSubCmd := &Command{Name: "baz"} + subCmd.Commands = []*Command{subSubCmd} + + cmd := &Command{ + Name: "foo", + Commands: []*Command{subCmd}, + } + + var visited []string + err := cmd.Walk(func(c *Command) error { + visited = append(visited, c.Name) + return nil + }) + require.NoError(t, err) + assert.Equal(t, []string{"foo", "bar", "baz"}, visited) +} + +func TestCommand_Walk_NilFn(t *testing.T) { + cmd := &Command{Name: "foo"} + assert.Nil(t, cmd.Walk(nil)) +} From 20a63313e59c656908e3c30e58498b7c34afae88 Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Sat, 6 Jun 2026 08:53:58 -0400 Subject: [PATCH 2/2] Add docs --- godoc-current.txt | 9 +++++++++ testdata/godoc-v3.x.txt | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/godoc-current.txt b/godoc-current.txt index 681df19d63..9914b7cec5 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -689,6 +689,11 @@ func (cmd *Command) Names() []string func (cmd *Command) NumFlags() int NumFlags returns the number of flags set +func (cmd *Command) Path() []string + Path returns the path of command names from the root to cmd, + inclusive. Each element is a Command.Name. FullName() is equivalent to + strings.Join(cmd.Path(), " "). + func (cmd *Command) Root() *Command Root returns the Command at the root of the graph @@ -801,6 +806,10 @@ func (cmd *Command) VisiblePersistentFlags() []Flag VisiblePersistentFlags returns a slice of LocalFlag with Persistent=true and Hidden=false. +func (cmd *Command) Walk(fn func(*Command) error) error + Walk visits cmd and every descendant in pre-order. Returning a non-nil error + from fn short-circuits the walk and is returned to the caller. + type CommandCategories interface { // AddCommand adds a command to a category, creating a new category if necessary. AddCommand(category string, command *Command) diff --git a/testdata/godoc-v3.x.txt b/testdata/godoc-v3.x.txt index 681df19d63..9914b7cec5 100644 --- a/testdata/godoc-v3.x.txt +++ b/testdata/godoc-v3.x.txt @@ -689,6 +689,11 @@ func (cmd *Command) Names() []string func (cmd *Command) NumFlags() int NumFlags returns the number of flags set +func (cmd *Command) Path() []string + Path returns the path of command names from the root to cmd, + inclusive. Each element is a Command.Name. FullName() is equivalent to + strings.Join(cmd.Path(), " "). + func (cmd *Command) Root() *Command Root returns the Command at the root of the graph @@ -801,6 +806,10 @@ func (cmd *Command) VisiblePersistentFlags() []Flag VisiblePersistentFlags returns a slice of LocalFlag with Persistent=true and Hidden=false. +func (cmd *Command) Walk(fn func(*Command) error) error + Walk visits cmd and every descendant in pre-order. Returning a non-nil error + from fn short-circuits the walk and is returned to the caller. + type CommandCategories interface { // AddCommand adds a command to a category, creating a new category if necessary. AddCommand(category string, command *Command)