Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 33 additions & 13 deletions command.go
Comment thread
dearchap marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -167,19 +167,6 @@ type Command struct {
isCompletionCommand bool
}

// FullName returns the full name of the command.
// For commands with parents this ensures that the parent commands
// are part of the command path.
func (cmd *Command) FullName() string {
namePath := []string{}

if cmd.parent != nil {
namePath = append(namePath, cmd.parent.FullName())
}

return strings.Join(append(namePath, cmd.Name), " ")
}

func (cmd *Command) Command(name string) *Command {
for _, subCmd := range cmd.Commands {
if subCmd.HasName(name) {
Expand Down Expand Up @@ -577,6 +564,39 @@ func (cmd *Command) Lineage() []*Command {
return lineage
}

// FullName returns the full name of the command.
// Includes parent commands separated by space.
func (cmd *Command) FullName() 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. Path traverses upward via parent pointers
// similar to Lineage. FullName() is equivalent to strings.Join(cmd.Path(), " ").
func (cmd *Command) Path() []string {
if cmd.parent != nil {
return append(cmd.parent.Path(), cmd.Name)
}
return []string{cmd.Name}
}

// Walk visits cmd and every descendant. If fn returns a non-nil error, the
// walk terminates and the error 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
}

// Count returns the num of occurrences of this flag
func (cmd *Command) Count(name string) int {
if cf, ok := cmd.lookupFlag(name).(Countable); ok {
Expand Down
12 changes: 7 additions & 5 deletions command_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,13 @@ func (cmd *Command) setupDefaults(osArgs []string) {
func (cmd *Command) setupCommandGraph() {
tracef("setting up command graph (cmd=%[1]q)", cmd.Name)

for _, subCmd := range cmd.Commands {
subCmd.parent = cmd
subCmd.setupSubcommand()
subCmd.setupCommandGraph()
}
_ = cmd.Walk(func(sub *Command) error {
for _, subCmd := range sub.Commands {
subCmd.parent = sub

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange that parent is injected only here and not when subCmd is added into sub.Commands array.

subCmd.setupSubcommand()
}
return nil
})
}

func (cmd *Command) setupSubcommand() {
Expand Down
83 changes: 83 additions & 0 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5895,3 +5895,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))
}
148 changes: 148 additions & 0 deletions docs/v3/path-and-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
tags:
- v3
search:
boost: 2
---

# Path and Walk

`Command.Path()` returns the list of command names from the root to the
current command. `Command.Walk()` visits a command and every subcommand
recursively, calling a function on each.

## Path

The `Path()` method returns `[]string` where each element is a `Command.Name`
starting from the root. `FullName()` is equivalent to
`strings.Join(cmd.Path(), " ")`.

<!-- {
"output": "top mid bottom"
} -->
```go
package main

import (
"context"
"fmt"
"os"

"github.com/urfave/cli/v3"
)

func main() {
subSubCmd := &cli.Command{Name: "bottom", Action: func(context.Context, *cli.Command) error { return nil }}
subCmd := &cli.Command{Name: "mid", Subcommands: []*cli.Command{subSubCmd}, Action: func(context.Context, *cli.Command) error { return nil }}
cmd := &cli.Command{
Name: "top",
Subcommands: []*cli.Command{subCmd},
Action: func(ctx context.Context, c *cli.Command) error {
fmt.Println(strings.Join(c.Path(), " "))
return nil
},
}

cmd.Run(context.Background(), []string{"top", "mid", "bottom"})
}
```

```sh-session
$ go run .
top mid bottom
```

## Walk

`Walk()` traverses the command tree depth-first, visiting the command itself
first, then each subcommand recursively.

<!-- {
"output": "top\nmid\nbottom"
} -->
```go
package main

import (
"context"
"fmt"
"os"

"github.com/urfave/cli/v3"
)

func main() {
subSubCmd := &cli.Command{Name: "bottom", Action: func(context.Context, *cli.Command) error { return nil }}
subCmd := &cli.Command{Name: "mid", Subcommands: []*cli.Command{subSubCmd}, Action: func(context.Context, *cli.Command) error { return nil }}
cmd := &cli.Command{
Name: "top",
Subcommands: []*cli.Command{subCmd},
Action: func(ctx context.Context, c *cli.Command) error { return nil },
}

cmd.Walk(func(c *cli.Command) error {
fmt.Println(c.Name)
return nil
})
}
```

```sh-session
$ go run .
top
mid
bottom
```

### Short-circuiting

Return a non-nil error from the walk function to stop traversal early.

<!-- {
"output": "top\nmid"
} -->
```go
package main

import (
"errors"
"fmt"
"os"

"github.com/urfave/cli/v3"
)

func main() {
subSubCmd := &cli.Command{Name: "bottom", Action: func(context.Context, *cli.Command) error { return nil }}
subCmd := &cli.Command{Name: "mid", Subcommands: []*cli.Command{subSubCmd}, Action: func(context.Context, *cli.Command) error { return nil }}
cmd := &cli.Command{
Name: "top",
Subcommands: []*cli.Command{subCmd},
Action: func(ctx context.Context, c *cli.Command) error { return nil },
}

err := cmd.Walk(func(c *cli.Command) error {
fmt.Println(c.Name)
if c.Name == "mid" {
return errors.New("stop")
}
return nil
})
fmt.Println(err)
}
```

```sh-session
$ go run .
top
mid
stop
```

## Relation to Lineage

[`Lineage()`](https://pkg.go.dev/github.com/urfave/cli/v3#Command.Lineage)
returns the command and all its ancestors as `[]*Command` (child first).
`Path()` is similar but returns only the command names as `[]string` (root
first). Use `Lineage()` when you need access to the ancestor `*Command`
values; use `Path()` when you only need the names.
14 changes: 12 additions & 2 deletions godoc-current.txt
Original file line number Diff line number Diff line change
Expand Up @@ -608,8 +608,8 @@ func (cmd *Command) FloatSlice(name string) []float64
found

func (cmd *Command) FullName() string
FullName returns the full name of the command. For commands with parents
this ensures that the parent commands are part of the command path.
FullName returns the full name of the command. Includes parent commands
separated by space.

func (cmd *Command) Generic(name string) Value
Generic looks up the value of a local GenericFlag, returns nil if not found
Expand Down Expand Up @@ -691,6 +691,12 @@ 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. Path traverses upward via parent pointers
similar to Lineage. FullName() is equivalent to strings.Join(cmd.Path(),
" ").

func (cmd *Command) Root() *Command
Root returns the Command at the root of the graph

Expand Down Expand Up @@ -803,6 +809,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. If fn returns a non-nil error,
the walk terminates and the error 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)
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ nav:
- v3 Manual:
- Getting Started: v3/getting-started.md
- Migrating From Older Releases: v3/migrating-from-older-releases.md
- Path and Walk: v3/path-and-walk.md
- Binary Size: v3/binary-size.md
- Examples:
- Greet: v3/examples/greet.md
Expand Down
14 changes: 12 additions & 2 deletions testdata/godoc-v3.x.txt
Original file line number Diff line number Diff line change
Expand Up @@ -608,8 +608,8 @@ func (cmd *Command) FloatSlice(name string) []float64
found

func (cmd *Command) FullName() string
FullName returns the full name of the command. For commands with parents
this ensures that the parent commands are part of the command path.
FullName returns the full name of the command. Includes parent commands
separated by space.

func (cmd *Command) Generic(name string) Value
Generic looks up the value of a local GenericFlag, returns nil if not found
Expand Down Expand Up @@ -691,6 +691,12 @@ 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. Path traverses upward via parent pointers
similar to Lineage. FullName() is equivalent to strings.Join(cmd.Path(),
" ").

func (cmd *Command) Root() *Command
Root returns the Command at the root of the graph

Expand Down Expand Up @@ -803,6 +809,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. If fn returns a non-nil error,
the walk terminates and the error 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)
Expand Down
Loading