From 5308b4cd0fd522bf1cc2ff61a5ac738b9e8bcf26 Mon Sep 17 00:00:00 2001 From: Peter Smit Date: Fri, 6 Feb 2015 10:46:32 +0200 Subject: [PATCH 1/7] 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/7] 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/7] 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 2a256d4c5397fb0e91ab71cc73787698d13023e0 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Thu, 5 May 2016 10:26:53 -0400 Subject: [PATCH 4/7] Provide a variable for writing output with a default of os.Stderr --- app.go | 6 +++--- errors.go | 6 +++++- flag.go | 2 +- help.go | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app.go b/app.go index 89c741b..28bf78b 100644 --- a/app.go +++ b/app.go @@ -228,11 +228,11 @@ func (a *App) Run(arguments []string) (err error) { // DEPRECATED: Another entry point to the cli app, takes care of passing arguments and error handling func (a *App) RunAndExitOnError() { - fmt.Fprintf(os.Stderr, + fmt.Fprintf(OutWriter, "DEPRECATED cli.App.RunAndExitOnError. %s See %s\n", contactSysadmin, runAndExitOnErrorDeprecationURL) if err := a.Run(os.Args); err != nil { - fmt.Fprintln(os.Stderr, err) + fmt.Fprintln(OutWriter, err) OsExiter(1) } } @@ -422,7 +422,7 @@ func HandleAction(action interface{}, context *Context) (err error) { vals := reflect.ValueOf(action).Call([]reflect.Value{reflect.ValueOf(context)}) if len(vals) == 0 { - fmt.Fprintf(os.Stderr, + fmt.Fprintf(OutWriter, "DEPRECATED Action signature. Must be `cli.ActionFunc`. %s See %s\n", contactSysadmin, appActionDeprecationURL) return nil diff --git a/errors.go b/errors.go index 5f1e83b..c03e676 100644 --- a/errors.go +++ b/errors.go @@ -8,6 +8,10 @@ import ( var OsExiter = os.Exit +// OutWriter is used to write output to the user. This can be anything +// implementing the io.Writer interface and defaults to os.Stderr. +var OutWriter = os.Stderr + type MultiError struct { Errors []error } @@ -69,7 +73,7 @@ func HandleExitCoder(err error) { if exitErr, ok := err.(ExitCoder); ok { if err.Error() != "" { - fmt.Fprintln(os.Stderr, err) + fmt.Fprintln(OutWriter, err) } OsExiter(exitErr.ExitCode()) return diff --git a/flag.go b/flag.go index 3b6a2e1..7778a2d 100644 --- a/flag.go +++ b/flag.go @@ -220,7 +220,7 @@ func (f IntSliceFlag) Apply(set *flag.FlagSet) { s = strings.TrimSpace(s) err := newVal.Set(s) if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) + fmt.Fprintf(OutWriter, err.Error()) } } f.Value = newVal diff --git a/help.go b/help.go index 45e8603..79f2e85 100644 --- a/help.go +++ b/help.go @@ -188,7 +188,7 @@ 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. We could send this to OutWriter if we need. return } w.Flush() From 6f0b442222239d0c6c6a999742e6d56b58494d7e Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Fri, 6 May 2016 12:14:26 -0400 Subject: [PATCH 5/7] Update to ErrWriter and make available on app --- app.go | 18 +++++++++++++++--- errors.go | 7 ++++--- flag.go | 2 +- help.go | 2 +- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/app.go b/app.go index 28bf78b..4d18d24 100644 --- a/app.go +++ b/app.go @@ -82,6 +82,8 @@ type App struct { Email string // Writer writer to write output to Writer io.Writer + // ErrWriter writes error output + ErrWriter io.Writer // Other custom info Metadata map[string]interface{} } @@ -228,11 +230,11 @@ func (a *App) Run(arguments []string) (err error) { // DEPRECATED: Another entry point to the cli app, takes care of passing arguments and error handling func (a *App) RunAndExitOnError() { - fmt.Fprintf(OutWriter, + fmt.Fprintf(a.errWriter(), "DEPRECATED cli.App.RunAndExitOnError. %s See %s\n", contactSysadmin, runAndExitOnErrorDeprecationURL) if err := a.Run(os.Args); err != nil { - fmt.Fprintln(OutWriter, err) + fmt.Fprintln(a.errWriter(), err) OsExiter(1) } } @@ -377,6 +379,16 @@ func (a *App) hasFlag(flag Flag) bool { return false } +func (a *App) errWriter() io.Writer { + + // When the app ErrWriter is nil use the package level one. + if a.ErrWriter == nil { + return ErrWriter + } + + return a.ErrWriter +} + func (a *App) appendFlag(flag Flag) { if !a.hasFlag(flag) { a.Flags = append(a.Flags, flag) @@ -422,7 +434,7 @@ func HandleAction(action interface{}, context *Context) (err error) { vals := reflect.ValueOf(action).Call([]reflect.Value{reflect.ValueOf(context)}) if len(vals) == 0 { - fmt.Fprintf(OutWriter, + fmt.Fprintf(ErrWriter, "DEPRECATED Action signature. Must be `cli.ActionFunc`. %s See %s\n", contactSysadmin, appActionDeprecationURL) return nil diff --git a/errors.go b/errors.go index c03e676..db46a83 100644 --- a/errors.go +++ b/errors.go @@ -2,15 +2,16 @@ package cli import ( "fmt" + "io" "os" "strings" ) var OsExiter = os.Exit -// OutWriter is used to write output to the user. This can be anything +// ErrWriter is used to write errors to the user. This can be anything // implementing the io.Writer interface and defaults to os.Stderr. -var OutWriter = os.Stderr +var ErrWriter io.Writer = os.Stderr type MultiError struct { Errors []error @@ -73,7 +74,7 @@ func HandleExitCoder(err error) { if exitErr, ok := err.(ExitCoder); ok { if err.Error() != "" { - fmt.Fprintln(OutWriter, err) + fmt.Fprintln(ErrWriter, err) } OsExiter(exitErr.ExitCode()) return diff --git a/flag.go b/flag.go index 7778a2d..8354de0 100644 --- a/flag.go +++ b/flag.go @@ -220,7 +220,7 @@ func (f IntSliceFlag) Apply(set *flag.FlagSet) { s = strings.TrimSpace(s) err := newVal.Set(s) if err != nil { - fmt.Fprintf(OutWriter, err.Error()) + fmt.Fprintf(ErrWriter, err.Error()) } } f.Value = newVal diff --git a/help.go b/help.go index 79f2e85..f4ea7a3 100644 --- a/help.go +++ b/help.go @@ -188,7 +188,7 @@ 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 OutWriter if we need. + // we can do to recover. We could send this to ErrWriter if we need. return } w.Flush() From dfa9a87bee6396f67a8d0b09e5ae47b19b985494 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 8 May 2016 23:54:12 -0400 Subject: [PATCH 6/7] 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 } From 28eb7b2cc4bf1b4d266fe55fe7d94f958170cdf8 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Sun, 8 May 2016 21:03:02 -0700 Subject: [PATCH 7/7] Added Hidden command support to CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7ea0a6..9834228 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ## [Unreleased] ### Added - Pluggable flag-level help text rendering via `cli.DefaultFlagStringFunc` +- Support for hiding commands by setting `Hidden: true` -- this will hide the + commands in help output ### Changed - `Float64Flag`, `IntFlag`, and `DurationFlag` default values are no longer