Skip to content
Open
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
23 changes: 23 additions & 0 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -582,8 +582,31 @@ func (cmd *Command) Path() []string {
return []string{cmd.Name}
}

// HasHiddenParent returns true if any ancestor of this command has
// Hidden set to true.
func (cmd *Command) HasHiddenParent() bool {
if cmd.parent == nil {
return false
}
if cmd.parent.Hidden {
return true
}
return cmd.parent.HasHiddenParent()
}

// Walk visits cmd and every descendant. If fn returns a non-nil error, the
// walk terminates and the error is returned to the caller.
//
// To skip subcommands of hidden commands in the walk, check both Hidden and
// HasHiddenParent in the callback:
//
// cmd.Walk(func(c *cli.Command) error {
// if c.Hidden || c.HasHiddenParent() {
// return nil
// }
// // process visible commands only
// return nil
// })
func (cmd *Command) Walk(fn func(*Command) error) error {
if fn == nil {
return nil
Expand Down
53 changes: 53 additions & 0 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6450,3 +6450,56 @@ func TestCommand_Walk_NilFn(t *testing.T) {
cmd := &Command{Name: "foo"}
assert.Nil(t, cmd.Walk(nil))
}

func TestCommand_HasHiddenParent(t *testing.T) {
t.Run("root command", func(t *testing.T) {
cmd := &Command{Name: "root"}
assert.False(t, cmd.HasHiddenParent())
})

t.Run("direct hidden parent", func(t *testing.T) {
parent := &Command{Name: "parent", Hidden: true}
child := &Command{Name: "child"}
child.parent = parent
assert.True(t, child.HasHiddenParent())
})

t.Run("non-hidden parent", func(t *testing.T) {
parent := &Command{Name: "parent"}
child := &Command{Name: "child"}
child.parent = parent
assert.False(t, child.HasHiddenParent())
})

t.Run("grandparent is hidden", func(t *testing.T) {
grandparent := &Command{Name: "grandparent", Hidden: true}
parent := &Command{Name: "parent"}
child := &Command{Name: "child"}
parent.parent = grandparent
child.parent = parent
assert.True(t, child.HasHiddenParent())
})

t.Run("subcommands of hidden command are skipped in walk", func(t *testing.T) {
hiddenCmd := &Command{Name: "completion", Hidden: true}
subCmd := &Command{Name: "bash"}
subCmd.parent = hiddenCmd
hiddenCmd.Commands = []*Command{subCmd}

root := &Command{
Name: "root",
Commands: []*Command{hiddenCmd},
}

var visited []string
err := root.Walk(func(c *Command) error {
if c.Hidden || c.HasHiddenParent() {
return nil
}
visited = append(visited, c.Name)
return nil
})
require.NoError(t, err)
assert.Equal(t, []string{"root"}, visited)
})
}
23 changes: 23 additions & 0 deletions examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/mail"
"os"
"strings"
"time"

// Alias the package import to make the examples runnable on pkg.go.dev.
Expand Down Expand Up @@ -608,3 +609,25 @@ func ExampleCommand_Suggest_command() {
//
// Did you mean "--smiling"?
}

func ExampleCommand_Walk() {
cmd := &cli.Command{
Name: "app",
Commands: []*cli.Command{
{Name: "serve"},
{Name: "migrate"},
},
Action: func(_ context.Context, _ *cli.Command) error {
return nil
},
}

var names []string
_ = cmd.Walk(func(c *cli.Command) error {
names = append(names, c.Name)
return nil
})
fmt.Println(strings.Join(names, ", "))
// Output:
// app, serve, migrate
}
15 changes: 15 additions & 0 deletions godoc-current.txt
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,10 @@ func (cmd *Command) FullName() string
func (cmd *Command) Generic(name string) Value
Generic looks up the value of a local GenericFlag, returns nil if not found

func (cmd *Command) HasHiddenParent() bool
HasHiddenParent returns true if any ancestor of this command has Hidden set
to true.

func (cmd *Command) HasName(name string) bool
HasName returns true if Command.Name matches given name

Expand Down Expand Up @@ -817,6 +821,17 @@ 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.

To skip subcommands of hidden commands in the walk, check both Hidden and
HasHiddenParent in the callback:

cmd.Walk(func(c *cli.Command) error {
if c.Hidden || c.HasHiddenParent() {
return nil
}
// process visible commands only
return nil
})

type CommandCategories interface {
// AddCommand adds a command to a category, creating a new category if necessary.
AddCommand(category string, command *Command)
Expand Down
15 changes: 15 additions & 0 deletions testdata/godoc-v3.x.txt
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,10 @@ func (cmd *Command) FullName() string
func (cmd *Command) Generic(name string) Value
Generic looks up the value of a local GenericFlag, returns nil if not found

func (cmd *Command) HasHiddenParent() bool
HasHiddenParent returns true if any ancestor of this command has Hidden set
to true.

func (cmd *Command) HasName(name string) bool
HasName returns true if Command.Name matches given name

Expand Down Expand Up @@ -817,6 +821,17 @@ 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.

To skip subcommands of hidden commands in the walk, check both Hidden and
HasHiddenParent in the callback:

cmd.Walk(func(c *cli.Command) error {
if c.Hidden || c.HasHiddenParent() {
return nil
}
// process visible commands only
return nil
})

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.

If c.Hidden is set, then the function returns nil, the walk terminates, and c.HasHiddenParent is never reached, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yeah

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.

@dearchap so what's the point in having c.HasHiddenParent condition here? It could be that immediate parent is not hidden, but the grandfather is. Maybe c.AnyHiddenParent method would be better? If the logic is if any([c.Hidden for c in [c] + c.Parents]).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

See the implementation of HasHiddenParent. If any command in the chain has Hidden set it will return true

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.

@dearchap I feel like the better API would be to add a generic way to check some property in current and all parent commands. Otherwise there has to be a plenty of method for each property.

And also, how to check that only the parent command has hidden status? If I want to hide intermediate command, but make all its children visible? This HasHiddenParent method seems too specific as for me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@abitrolly do you have a design in mind ?


type CommandCategories interface {
// AddCommand adds a command to a category, creating a new category if necessary.
AddCommand(category string, command *Command)
Expand Down