From 5308b4cd0fd522bf1cc2ff61a5ac738b9e8bcf26 Mon Sep 17 00:00:00 2001 From: Peter Smit Date: Fri, 6 Feb 2015 10:46:32 +0200 Subject: [PATCH 01/12] 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 02/12] 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 03/12] 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 04/12] 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 05/12] 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 cd230f3a88267fff46df1f1763978494c08c8b29 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Fri, 6 May 2016 12:19:01 -0400 Subject: [PATCH 06/12] Update travis config for Go versions - Added Go 1.6 testing - Updated 1.5.x and 1.4.x to latest point releases --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 133722f..1707c50 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,9 @@ go: - 1.1.2 - 1.2.2 - 1.3.3 -- 1.4.2 -- 1.5.1 +- 1.4.3 +- 1.5.4 +- 1.6.2 - tip matrix: From e9970b7b13333aba175bb7cb4c3b3ca07e3fba34 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Fri, 6 May 2016 12:24:51 -0400 Subject: [PATCH 07/12] Adding Go Report Card badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2ac96fd..c1709ce 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Build Status](https://travis-ci.org/codegangsta/cli.svg?branch=master)](https://travis-ci.org/codegangsta/cli) [![GoDoc](https://godoc.org/github.com/codegangsta/cli?status.svg)](https://godoc.org/github.com/codegangsta/cli) [![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-codegangsta-cli) +[![Go Report Card](https://goreportcard.com/badge/codegangsta/cli)](https://goreportcard.com/report/codegangsta/cli) # cli From 0a3c5e751657e5ab9ff65bedefb07a29cea6a5b9 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Fri, 6 May 2016 12:30:30 -0400 Subject: [PATCH 08/12] Letting Travis CI select the patch version of Go 1.4 to use The last release of Go 1.4 when installed on Travis CI does not have go vet installed. Letting Travis CI select the patch version instead. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1707c50..76f38a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ go: - 1.1.2 - 1.2.2 - 1.3.3 -- 1.4.3 +- 1.4 - 1.5.4 - 1.6.2 - tip From e3ace79a91d4333dc0f5cdc5bf5e1a3e58dddd21 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Sat, 7 May 2016 16:06:28 -0700 Subject: [PATCH 09/12] Add GlobalBoolT Fixes #206 --- CHANGELOG.md | 1 + context.go | 8 ++++++++ context_test.go | 24 ++++++++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7ea0a6..3d07131 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## [Unreleased] ### Added - Pluggable flag-level help text rendering via `cli.DefaultFlagStringFunc` +- `context.GlobalBoolT` was added as an analogue to `context.GlobalBool` ### Changed - `Float64Flag`, `IntFlag`, and `DurationFlag` default values are no longer diff --git a/context.go b/context.go index ef3d2fc..aad2812 100644 --- a/context.go +++ b/context.go @@ -104,6 +104,14 @@ func (c *Context) GlobalBool(name string) bool { return false } +// Looks up the value of a global bool flag, returns true if no bool flag exists +func (c *Context) GlobalBoolT(name string) bool { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupBoolT(name, fs) + } + return false +} + // Looks up the value of a global string flag, returns "" if no string flag exists func (c *Context) GlobalString(name string) string { if fs := lookupGlobalFlagSet(name, c); fs != nil { diff --git a/context_test.go b/context_test.go index 4c23271..7ba2ebd 100644 --- a/context_test.go +++ b/context_test.go @@ -82,6 +82,30 @@ func TestContext_BoolT(t *testing.T) { expect(t, c.BoolT("myflag"), true) } +func TestContext_GlobalBool(t *testing.T) { + set := flag.NewFlagSet("test", 0) + + globalSet := flag.NewFlagSet("test-global", 0) + globalSet.Bool("myflag", false, "doc") + globalCtx := NewContext(nil, globalSet, nil) + + c := NewContext(nil, set, globalCtx) + expect(t, c.GlobalBool("myflag"), false) + expect(t, c.GlobalBool("nope"), false) +} + +func TestContext_GlobalBoolT(t *testing.T) { + set := flag.NewFlagSet("test", 0) + + globalSet := flag.NewFlagSet("test-global", 0) + globalSet.Bool("myflag", true, "doc") + globalCtx := NewContext(nil, globalSet, nil) + + c := NewContext(nil, set, globalCtx) + expect(t, c.GlobalBoolT("myflag"), true) + expect(t, c.GlobalBoolT("nope"), false) +} + func TestContext_Args(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Bool("myflag", false, "doc") From 592f1d97e5a73a1f446de8675c01c9dcbfc3f6a5 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Sat, 7 May 2016 17:22:09 -0700 Subject: [PATCH 10/12] Exit non-zero if a unknown subcommand is given Currently it just prints the help message and exits 0. We do this by modifying the helpCommand and helpSubcommand cli.Commands to return an error if they are called with an unknown subcommand. This propogates up to the app which exits with 3 and prints the error. Thanks to @danslimmon for the initial approach! Fixes #276 --- CHANGELOG.md | 10 ++++++--- help.go | 32 ++++++++++++++--------------- help_test.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7ea0a6..d4de0b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,15 @@ ### Changed - `Float64Flag`, `IntFlag`, and `DurationFlag` default values are no longer -quoted in help text output. + quoted in help text output. - All flag types now include `(default: {value})` strings following usage when a -default value can be (reasonably) detected. + default value can be (reasonably) detected. - `IntSliceFlag` and `StringSliceFlag` usage strings are now more consistent -with non-slice flag types + with non-slice flag types +- Apps now exit with a code of 3 if an unknown subcommand is specified + (previously they printed "No help topic for...", but still exited 0. This + makes it easier to script around apps built using `cli` since they can trust + that a 0 exit code indicated a successful execution. ## [1.16.0] - 2016-05-02 ### Added diff --git a/help.go b/help.go index 45e8603..259e452 100644 --- a/help.go +++ b/help.go @@ -81,10 +81,10 @@ var helpCommand = Command{ Action: func(c *Context) error { args := c.Args() if args.Present() { - ShowCommandHelp(c, args.First()) - } else { - ShowAppHelp(c) + return ShowCommandHelp(c, args.First()) } + + ShowAppHelp(c) return nil }, } @@ -97,11 +97,10 @@ var helpSubcommand = Command{ Action: func(c *Context) error { args := c.Args() if args.Present() { - ShowCommandHelp(c, args.First()) - } else { - ShowSubcommandHelp(c) + return ShowCommandHelp(c, args.First()) } - return nil + + return ShowSubcommandHelp(c) }, } @@ -127,30 +126,31 @@ func DefaultAppComplete(c *Context) { } // Prints help for the given command -func ShowCommandHelp(ctx *Context, command string) { +func ShowCommandHelp(ctx *Context, command string) error { // show the subcommand help for a command with subcommands if command == "" { HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App) - return + return nil } for _, c := range ctx.App.Commands { if c.HasName(command) { HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) - return + return nil } } - if ctx.App.CommandNotFound != nil { - ctx.App.CommandNotFound(ctx, command) - } else { - fmt.Fprintf(ctx.App.Writer, "No help topic for '%v'\n", command) + if ctx.App.CommandNotFound == nil { + return NewExitError(fmt.Sprintf("No help topic for '%v'", command), 3) } + + ctx.App.CommandNotFound(ctx, command) + return nil } // Prints help for the given subcommand -func ShowSubcommandHelp(c *Context) { - ShowCommandHelp(c, c.Command.Name) +func ShowSubcommandHelp(c *Context) error { + return ShowCommandHelp(c, c.Command.Name) } // Prints the version number of the App diff --git a/help_test.go b/help_test.go index ee5c25c..0fabdba 100644 --- a/help_test.go +++ b/help_test.go @@ -2,6 +2,8 @@ package cli import ( "bytes" + "flag" + "strings" "testing" ) @@ -110,3 +112,59 @@ func Test_Version_Custom_Flags(t *testing.T) { t.Errorf("unexpected output: %s", output.String()) } } + +func Test_helpCommand_Action_ErrorIfNoTopic(t *testing.T) { + app := NewApp() + + set := flag.NewFlagSet("test", 0) + set.Parse([]string{"foo"}) + + c := NewContext(app, set, nil) + + err := helpCommand.Action.(func(*Context) error)(c) + + if err == nil { + t.Fatalf("expected error from helpCommand.Action(), but got nil") + } + + exitErr, ok := err.(*ExitError) + if !ok { + t.Fatalf("expected ExitError from helpCommand.Action(), but instead got: %v", err.Error()) + } + + if !strings.HasPrefix(exitErr.Error(), "No help topic for") { + t.Fatalf("expected an unknown help topic error, but got: %v", exitErr.Error()) + } + + if exitErr.exitCode != 3 { + t.Fatalf("expected exit value = 3, got %d instead", exitErr.exitCode) + } +} + +func Test_helpSubcommand_Action_ErrorIfNoTopic(t *testing.T) { + app := NewApp() + + set := flag.NewFlagSet("test", 0) + set.Parse([]string{"foo"}) + + c := NewContext(app, set, nil) + + err := helpSubcommand.Action.(func(*Context) error)(c) + + if err == nil { + t.Fatalf("expected error from helpCommand.Action(), but got nil") + } + + exitErr, ok := err.(*ExitError) + if !ok { + t.Fatalf("expected ExitError from helpCommand.Action(), but instead got: %v", err.Error()) + } + + if !strings.HasPrefix(exitErr.Error(), "No help topic for") { + t.Fatalf("expected an unknown help topic error, but got: %v", exitErr.Error()) + } + + if exitErr.exitCode != 3 { + t.Fatalf("expected exit value = 3, got %d instead", exitErr.exitCode) + } +} From dfa9a87bee6396f67a8d0b09e5ae47b19b985494 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sun, 8 May 2016 23:54:12 -0400 Subject: [PATCH 11/12] 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 12/12] 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