Skip to content

Commit

Permalink
Introduce CommandHidden to hide commands from help and autocomplete
Browse files Browse the repository at this point in the history
  • Loading branch information
mitchellh committed Sep 8, 2017
1 parent 7a1a617 commit 08bd20f
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 1 deletion.
30 changes: 29 additions & 1 deletion cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ type CLI struct {
// For example, if the key is "foo bar", then to access it our CLI
// must be accessed with "./cli foo bar". See the docs for CLI for
// notes on how this changes some other behavior of the CLI as well.
//
// The factory should be as cheap as possible, ideally only allocating
// a struct. The factory may be called multiple times in the course
// of a command execution and certain events such as help require the
// instantiation of all commands. Expensive initialization should be
// deferred to function calls within the interface implementation.
Commands map[string]CommandFactory

// Name defines the name of the CLI.
Expand Down Expand Up @@ -433,6 +439,11 @@ func (c *CLI) initAutocompleteSub(prefix string) complete.Command {
impl = nil
}

// If the command is hidden, don't record it at all
if c, ok := impl.(CommandHidden); ok && c.Hidden() {
return false
}

// Check if it implements ComandAutocomplete. If so, setup the autocomplete
if c, ok := impl.(CommandAutocomplete); ok {
subCmd.Args = c.AutocompleteArgs()
Expand Down Expand Up @@ -566,7 +577,24 @@ func (c *CLI) helpCommands(prefix string) map[string]CommandFactory {
panic("not found: " + k)
}

result[k] = raw.(CommandFactory)
f := raw.(CommandFactory)

// Instantiate it so see if it is a hidden command. This forces
// us to instantiate every command on help, but help should be
// sufficiently rare in addition to the factory functions generally
// being pure allocations.
cmd, err := f()
if err != nil {
// If we have an error we can't really return it from this function.
// This is exceptionally rare since command factories usually just
// allocate a struct, so we'll ignore it.
continue
}
if ch, ok := cmd.(CommandHidden); ok && ch.Hidden() {
continue
}

result[k] = f
}

return result
Expand Down
94 changes: 94 additions & 0 deletions cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,90 @@ func TestCLIRun_printCommandHelpTemplate(t *testing.T) {
}
}

func TestCLIRun_helpHiddenRoot(t *testing.T) {
helpCalled := false
buf := new(bytes.Buffer)
cli := &CLI{
Args: []string{"--help"},
Commands: map[string]CommandFactory{
"foo": func() (Command, error) {
return &MockCommand{}, nil
},
"bar": func() (Command, error) {
return &MockCommandHidden{HiddenValue: true}, nil
},
},
HelpFunc: func(m map[string]CommandFactory) string {
helpCalled = true

if _, ok := m["foo"]; !ok {
t.Fatal("should have foo")
}
if _, ok := m["bar"]; ok {
t.Fatal("should not have bar")
}

return ""
},
HelpWriter: buf,
}

code, err := cli.Run()
if err != nil {
t.Fatalf("Error: %s", err)
}

if code != 0 {
t.Fatalf("Code: %d", code)
}

if !helpCalled {
t.Fatal("help not called")
}
}

func TestCLIRun_helpHiddenNested(t *testing.T) {
command := &MockCommand{
HelpText: "donuts",
}

buf := new(bytes.Buffer)
cli := &CLI{
Args: []string{"foo", "--help"},
Commands: map[string]CommandFactory{
"foo": func() (Command, error) {
return command, nil
},
"foo bar": func() (Command, error) {
return &MockCommand{SynopsisText: "hi!"}, nil
},
"foo zip": func() (Command, error) {
return &MockCommandHidden{HiddenValue: true}, nil
},
"foo longer": func() (Command, error) {
return &MockCommand{SynopsisText: "hi!"}, nil
},
"foo longer longest": func() (Command, error) {
return &MockCommandHidden{HiddenValue: true}, nil
},
},
HelpWriter: buf,
}

exitCode, err := cli.Run()
if err != nil {
t.Fatalf("err: %s", err)
}

if exitCode != 0 {
t.Fatalf("bad exit code: %d", exitCode)
}

if buf.String() != testCommandHelpSubcommandsHiddenOutput {
t.Fatalf("bad: '%#v'\n\n'%#v'", buf.String(), testCommandHelpSubcommandsOutput)
}
}

func TestCLIRun_autocompleteBoth(t *testing.T) {
command := new(MockCommand)
cli := &CLI{
Expand Down Expand Up @@ -906,6 +990,7 @@ func TestCLIAutocomplete_root(t *testing.T) {
{nil, "n", []string{"nodes", "noodles"}},
{nil, "noo", []string{"noodles"}},
{nil, "su", []string{"sub"}},
{nil, "h", nil},

// Make sure global flags work on subcommands
{[]string{"sub"}, "-v", nil},
Expand All @@ -918,11 +1003,13 @@ func TestCLIAutocomplete_root(t *testing.T) {
for _, tc := range cases {
t.Run(tc.Last, func(t *testing.T) {
command := new(MockCommand)
commandHidden := &MockCommandHidden{HiddenValue: true}
cli := &CLI{
Commands: map[string]CommandFactory{
"foo": func() (Command, error) { return command, nil },
"nodes": func() (Command, error) { return command, nil },
"noodles": func() (Command, error) { return command, nil },
"hidden": func() (Command, error) { return commandHidden, nil },
"sub one": func() (Command, error) { return command, nil },
"sub two": func() (Command, error) { return command, nil },
"sub sub2 one": func() (Command, error) { return command, nil },
Expand Down Expand Up @@ -1192,6 +1279,13 @@ Subcommands:
zip hi!
`

const testCommandHelpSubcommandsHiddenOutput = `donuts
Subcommands:
bar hi!
longer hi!
`

const testCommandHelpSubcommandsTwoLevelOutput = `donuts
Subcommands:
Expand Down
8 changes: 8 additions & 0 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ type CommandHelpTemplate interface {
HelpTemplate() string
}

// CommandHidden is an extension of Command that allows a command to
// declare whether or not it is "hidden." A hidden command is not
// present in the help callback or command autocompletion.
type CommandHidden interface {
// Hidden should return true if hidden, and false if not hidden.
Hidden() bool
}

// CommandFactory is a type of function that is a factory for commands.
// We need a factory because we may need to setup some state on the
// struct that implements the command itself.
Expand Down
11 changes: 11 additions & 0 deletions command_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,14 @@ type MockCommandHelpTemplate struct {
func (c *MockCommandHelpTemplate) HelpTemplate() string {
return c.HelpTemplateText
}

// MockCommandHidden is an implementation of CommandHidden.
type MockCommandHidden struct {
MockCommand

HiddenValue bool
}

func (c *MockCommandHidden) Hidden() bool {
return c.HiddenValue
}

0 comments on commit 08bd20f

Please sign in to comment.