From 5308b4cd0fd522bf1cc2ff61a5ac738b9e8bcf26 Mon Sep 17 00:00:00 2001 From: Peter Smit Date: Fri, 6 Feb 2015 10:46:32 +0200 Subject: [PATCH 1/4] Allow commands to be hidden from help and autocomplete --- command.go | 2 ++ help.go | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/command.go b/command.go index ffd3ef8..6117a85 100644 --- a/command.go +++ b/command.go @@ -31,6 +31,8 @@ type Command struct { SkipFlagParsing bool // Boolean to hide built-in help command HideHelp bool + // Boolean to hide this command from help or completion + Hidden bool } // Invokes the command given the context, parses ctx.Args() to generate command-specific flags diff --git a/help.go b/help.go index bfb2788..0ea1b11 100644 --- a/help.go +++ b/help.go @@ -19,7 +19,7 @@ AUTHOR:{{if .Author}} {{.Email}}{{end}}{{end}} COMMANDS: - {{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}} + {{range .Commands}}{{if not .Hidden}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}{{end}} {{end}}{{if .Flags}} GLOBAL OPTIONS: {{range .Flags}}{{.}} @@ -53,7 +53,7 @@ USAGE: {{.Name}} command{{if .Flags}} [command options]{{end}} [arguments...] COMMANDS: - {{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}} + {{range .Commands}}{{if not .Hidden}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}{{end}} {{end}}{{if .Flags}} OPTIONS: {{range .Flags}}{{.}} @@ -103,6 +103,9 @@ func ShowAppHelp(c *Context) { // Prints the list of subcommands as the default app completion method func DefaultAppComplete(c *Context) { for _, command := range c.App.Commands { + if command.Hidden { + continue + } fmt.Fprintln(c.App.Writer, command.Name) if command.ShortName != "" { fmt.Fprintln(c.App.Writer, command.ShortName) From f397b1618ce783d09e35960ab83b3ad192649526 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Tue, 3 May 2016 05:51:26 -0400 Subject: [PATCH 2/4] Adding test for Command.Hidden handling in help text --- help_test.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/help_test.go b/help_test.go index ee5c25c..c6f2e57 100644 --- a/help_test.go +++ b/help_test.go @@ -2,6 +2,7 @@ package cli import ( "bytes" + "strings" "testing" ) @@ -110,3 +111,35 @@ func Test_Version_Custom_Flags(t *testing.T) { t.Errorf("unexpected output: %s", output.String()) } } + +func TestShowAppHelp_HiddenCommand(t *testing.T) { + app := &App{ + Commands: []Command{ + Command{ + Name: "frobbly", + Action: func(ctx *Context) error { + return nil + }, + }, + Command{ + Name: "secretfrob", + Hidden: true, + Action: func(ctx *Context) error { + return nil + }, + }, + }, + } + + output := &bytes.Buffer{} + app.Writer = output + app.Run([]string{"app", "--help"}) + + if strings.Contains(output.String(), "secretfrob") { + t.Fatalf("expected output to exclude \"secretfrob\"; got: %q", output.String()) + } + + if !strings.Contains(output.String(), "frobbly") { + t.Fatalf("expected output to include \"frobbly\"; got: %q", output.String()) + } +} From cc481d6b0ea0e659faecc03f48bd0352b5502b4b Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Tue, 3 May 2016 06:54:05 -0400 Subject: [PATCH 3/4] Adjust command hiding to use similar convention as hidden flags plus breaking out "setup" portion of `App.Run` into its own method, cleaning up some bits of the help templates, and allowing for runtime opt-in of displaying template errors to stderr. --- app.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++--- app_test.go | 4 ++-- category.go | 11 +++++++++++ errors_test.go | 4 ++-- help.go | 26 ++++++++++++++----------- help_test.go | 4 ++-- 6 files changed, 80 insertions(+), 20 deletions(-) diff --git a/app.go b/app.go index 89c741b..7ad070b 100644 --- a/app.go +++ b/app.go @@ -84,6 +84,8 @@ type App struct { Writer io.Writer // Other custom info Metadata map[string]interface{} + + didSetup bool } // Tries to find out when this binary was compiled. @@ -111,8 +113,16 @@ func NewApp() *App { } } -// Entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination -func (a *App) Run(arguments []string) (err error) { +// Setup runs initialization code to ensure all data structures are ready for +// `Run` or inspection prior to `Run`. It is internally called by `Run`, but +// will return early if setup has already happened. +func (a *App) Setup() { + if a.didSetup { + return + } + + a.didSetup = true + if a.Author != "" || a.Email != "" { a.Authors = append(a.Authors, Author{Name: a.Author, Email: a.Email}) } @@ -148,6 +158,11 @@ func (a *App) Run(arguments []string) (err error) { if !a.HideVersion { a.appendFlag(VersionFlag) } +} + +// Entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination +func (a *App) Run(arguments []string) (err error) { + a.Setup() // parse flags set := flagSet(a.Name, a.Flags) @@ -357,11 +372,41 @@ func (a *App) Command(name string) *Command { return nil } -// Returnes the array containing all the categories with the commands they contain +// Categories returns a slice containing all the categories with the commands they contain func (a *App) Categories() CommandCategories { return a.categories } +// VisibleCategories returns a slice of categories and commands that are +// Hidden=false +func (a *App) VisibleCategories() []*CommandCategory { + ret := []*CommandCategory{} + for _, category := range a.categories { + if visible := func() *CommandCategory { + for _, command := range category.Commands { + if !command.Hidden { + return category + } + } + return nil + }(); visible != nil { + ret = append(ret, visible) + } + } + return ret +} + +// VisibleCommands returns a slice of the Commands with Hidden=false +func (a *App) VisibleCommands() []Command { + ret := []Command{} + for _, command := range a.Commands { + if !command.Hidden { + ret = append(ret, command) + } + } + return ret +} + // VisibleFlags returns a slice of the Flags with Hidden=false func (a *App) VisibleFlags() []Flag { return visibleFlags(a.Flags) diff --git a/app_test.go b/app_test.go index bf2887e..b08abb2 100644 --- a/app_test.go +++ b/app_test.go @@ -1124,8 +1124,8 @@ func TestApp_Run_Categories(t *testing.T) { output := buf.String() t.Logf("output: %q\n", buf.Bytes()) - if !strings.Contains(output, "1:\n command1") { - t.Errorf("want buffer to include category %q, did not: \n%q", "1:\n command1", output) + if !strings.Contains(output, "1:\n command1") { + t.Errorf("want buffer to include category %q, did not: \n%q", "1:\n command1", output) } } diff --git a/category.go b/category.go index 7dbf218..5c3d4b5 100644 --- a/category.go +++ b/category.go @@ -28,3 +28,14 @@ func (c CommandCategories) AddCommand(category string, command Command) CommandC } return append(c, &CommandCategory{Name: category, Commands: []Command{command}}) } + +// VisibleCommands returns a slice of the Commands with Hidden=false +func (c *CommandCategory) VisibleCommands() []Command { + ret := []Command{} + for _, command := range c.Commands { + if !command.Hidden { + ret = append(ret, command) + } + } + return ret +} diff --git a/errors_test.go b/errors_test.go index 6863105..8f5f284 100644 --- a/errors_test.go +++ b/errors_test.go @@ -34,7 +34,7 @@ func TestHandleExitCoder_ExitCoder(t *testing.T) { defer func() { OsExiter = os.Exit }() - HandleExitCoder(NewExitError("galactic perimiter breach", 9)) + HandleExitCoder(NewExitError("galactic perimeter breach", 9)) expect(t, exitCode, 9) expect(t, called, true) @@ -51,7 +51,7 @@ func TestHandleExitCoder_MultiErrorWithExitCoder(t *testing.T) { defer func() { OsExiter = os.Exit }() - exitErr := NewExitError("galactic perimiter breach", 9) + exitErr := NewExitError("galactic perimeter breach", 9) err := NewMultiError(errors.New("wowsa"), errors.New("egad"), exitErr) HandleExitCoder(err) diff --git a/help.go b/help.go index 3f47efd..666791f 100644 --- a/help.go +++ b/help.go @@ -3,6 +3,7 @@ package cli import ( "fmt" "io" + "os" "strings" "text/tabwriter" "text/template" @@ -21,15 +22,15 @@ VERSION: {{.Version}} {{end}}{{end}}{{if len .Authors}} AUTHOR(S): - {{range .Authors}}{{ . }}{{end}} - {{end}}{{if .Commands}} -COMMANDS:{{range .Categories}}{{if .Name}} - {{.Name}}{{ ":" }}{{end}}{{range .Commands}} - {{if not .Hidden}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}{{end}}{{end}} + {{range .Authors}}{{.}}{{end}} + {{end}}{{if .VisibleCommands}} +COMMANDS:{{range .VisibleCategories}}{{if .Name}} + {{.Name}}:{{end}}{{range .VisibleCommands}} + {{.Name}}{{with .ShortName}}, {{.}}{{end}}{{"\t"}}{{.Usage}}{{end}} {{end}}{{end}}{{if .VisibleFlags}} GLOBAL OPTIONS: {{range .VisibleFlags}}{{.}} - {{end}}{{end}}{{if .Copyright }} + {{end}}{{end}}{{if .Copyright}} COPYRIGHT: {{.Copyright}} {{end}} @@ -52,7 +53,7 @@ DESCRIPTION: OPTIONS: {{range .VisibleFlags}}{{.}} - {{end}}{{ end }} + {{end}}{{end}} ` // The text template for the subcommand help topic. @@ -64,9 +65,9 @@ var SubcommandHelpTemplate = `NAME: USAGE: {{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} -COMMANDS:{{range .Categories}}{{if .Name}} - {{.Name}}{{ ":" }}{{end}}{{range .Commands}} - {{if not .Hidden}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}{{end}}{{end}} +COMMANDS:{{range .VisibleCategories}}{{if .Name}} + {{.Name}}:{{end}}{{range .VisibleCommands}} + {{.Name}}{{with .ShortName}}, {{.}}{{end}}{{"\t"}}{{.Usage}}{{end}} {{end}}{{if .VisibleFlags}} OPTIONS: {{range .VisibleFlags}}{{.}} @@ -191,7 +192,10 @@ func printHelp(out io.Writer, templ string, data interface{}) { err := t.Execute(w, data) if err != nil { // If the writer is closed, t.Execute will fail, and there's nothing - // we can do to recover. We could send this to os.Stderr if we need. + // we can do to recover. + if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" { + fmt.Fprintf(os.Stderr, "CLI TEMPLATE ERROR: %#v\n", err) + } return } w.Flush() diff --git a/help_test.go b/help_test.go index c6f2e57..db0cb21 100644 --- a/help_test.go +++ b/help_test.go @@ -136,10 +136,10 @@ func TestShowAppHelp_HiddenCommand(t *testing.T) { app.Run([]string{"app", "--help"}) if strings.Contains(output.String(), "secretfrob") { - t.Fatalf("expected output to exclude \"secretfrob\"; got: %q", output.String()) + t.Errorf("expected output to exclude \"secretfrob\"; got: %q", output.String()) } if !strings.Contains(output.String(), "frobbly") { - t.Fatalf("expected output to include \"frobbly\"; got: %q", output.String()) + t.Errorf("expected output to include \"frobbly\"; got: %q", output.String()) } } From dfa9a87bee6396f67a8d0b09e5ae47b19b985494 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 8 May 2016 23:54:12 -0400 Subject: [PATCH 4/4] Add tests for App.VisibleCategories & App.VisibleCommands --- app_test.go | 145 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/app_test.go b/app_test.go index b08abb2..4f054a4 100644 --- a/app_test.go +++ b/app_test.go @@ -304,6 +304,48 @@ func TestApp_CommandWithNoFlagBeforeTerminator(t *testing.T) { expect(t, args[2], "notAFlagAtAll") } +func TestApp_VisibleCommands(t *testing.T) { + app := NewApp() + app.Commands = []Command{ + Command{ + Name: "frob", + HelpName: "foo frob", + Action: func(_ *Context) error { return nil }, + }, + Command{ + Name: "frib", + HelpName: "foo frib", + Hidden: true, + Action: func(_ *Context) error { return nil }, + }, + } + + app.Setup() + expected := []Command{ + app.Commands[0], + app.Commands[2], // help + } + actual := app.VisibleCommands() + expect(t, len(expected), len(actual)) + for i, actualCommand := range actual { + expectedCommand := expected[i] + + if expectedCommand.Action != nil { + // comparing func addresses is OK! + expect(t, fmt.Sprintf("%p", expectedCommand.Action), fmt.Sprintf("%p", actualCommand.Action)) + } + + // nil out funcs, as they cannot be compared + // (https://github.com/golang/go/issues/8554) + expectedCommand.Action = nil + actualCommand.Action = nil + + if !reflect.DeepEqual(expectedCommand, actualCommand) { + t.Errorf("expected\n%#v\n!=\n%#v", expectedCommand, actualCommand) + } + } +} + func TestApp_Float64Flag(t *testing.T) { var meters float64 @@ -1129,6 +1171,109 @@ func TestApp_Run_Categories(t *testing.T) { } } +func TestApp_VisibleCategories(t *testing.T) { + app := NewApp() + app.Name = "visible-categories" + app.Commands = []Command{ + Command{ + Name: "command1", + Category: "1", + HelpName: "foo command1", + Hidden: true, + }, + Command{ + Name: "command2", + Category: "2", + HelpName: "foo command2", + }, + Command{ + Name: "command3", + Category: "3", + HelpName: "foo command3", + }, + } + + expected := []*CommandCategory{ + &CommandCategory{ + Name: "2", + Commands: []Command{ + app.Commands[1], + }, + }, + &CommandCategory{ + Name: "3", + Commands: []Command{ + app.Commands[2], + }, + }, + } + + app.Setup() + expect(t, expected, app.VisibleCategories()) + + app = NewApp() + app.Name = "visible-categories" + app.Commands = []Command{ + Command{ + Name: "command1", + Category: "1", + HelpName: "foo command1", + Hidden: true, + }, + Command{ + Name: "command2", + Category: "2", + HelpName: "foo command2", + Hidden: true, + }, + Command{ + Name: "command3", + Category: "3", + HelpName: "foo command3", + }, + } + + expected = []*CommandCategory{ + &CommandCategory{ + Name: "3", + Commands: []Command{ + app.Commands[2], + }, + }, + } + + app.Setup() + expect(t, expected, app.VisibleCategories()) + + app = NewApp() + app.Name = "visible-categories" + app.Commands = []Command{ + Command{ + Name: "command1", + Category: "1", + HelpName: "foo command1", + Hidden: true, + }, + Command{ + Name: "command2", + Category: "2", + HelpName: "foo command2", + Hidden: true, + }, + Command{ + Name: "command3", + Category: "3", + HelpName: "foo command3", + Hidden: true, + }, + } + + expected = []*CommandCategory{} + + app.Setup() + expect(t, expected, app.VisibleCategories()) +} + func TestApp_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) { app := NewApp() app.Action = func(c *Context) error { return nil }