From ce428377024188ae8ce8aae411881b7e993f9553 Mon Sep 17 00:00:00 2001 From: HIROSE Masaaki Date: Fri, 30 Sep 2016 19:23:44 +0900 Subject: [PATCH 01/30] Call HandleExitCoder for all members of MultiError.Errors --- errors.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/errors.go b/errors.go index c7d8c2f..fd67b96 100644 --- a/errors.go +++ b/errors.go @@ -86,8 +86,9 @@ func HandleExitCoder(err error) { if multiErr, ok := err.(MultiError); ok { for _, merr := range multiErr.Errors { - HandleExitCoder(merr) + fmt.Fprintln(ErrWriter, merr) } + OsExiter(1) return } From 6c50b15a273d29dc3820b5e4d50d78eeb113d335 Mon Sep 17 00:00:00 2001 From: HIROSE Masaaki Date: Fri, 11 Nov 2016 13:11:50 +0900 Subject: [PATCH 02/30] Exit with the code of ExitCoder if exists --- errors.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/errors.go b/errors.go index fd67b96..583f89f 100644 --- a/errors.go +++ b/errors.go @@ -85,10 +85,8 @@ func HandleExitCoder(err error) { } if multiErr, ok := err.(MultiError); ok { - for _, merr := range multiErr.Errors { - fmt.Fprintln(ErrWriter, merr) - } - OsExiter(1) + code := handleMultiError(multiErr) + OsExiter(code) return } @@ -97,3 +95,18 @@ func HandleExitCoder(err error) { } OsExiter(1) } + +func handleMultiError(multiErr MultiError) int { + code := 1 + for _, merr := range multiErr.Errors { + if multiErr2, ok := merr.(MultiError); ok { + code = handleMultiError(multiErr2) + } else { + fmt.Fprintln(ErrWriter, merr) + if exitErr, ok := merr.(ExitCoder); ok { + code = exitErr.ExitCode() + } + } + } + return code +} From 4267cd827cbfb066cf031bcb01ddbecdf7d0c49e Mon Sep 17 00:00:00 2001 From: Bo-Yi Wu Date: Wed, 4 Jan 2017 10:28:58 +0800 Subject: [PATCH 03/30] [ci skip] Fix template syntax error --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bb5f61e..6ba60c4 100644 --- a/README.md +++ b/README.md @@ -940,16 +940,13 @@ SUPPORT: support@awesometown.example.com cli.AppHelpTemplate = `NAME: {{.Name}} - {{.Usage}} USAGE: - {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command -[command options]{{end}} {{if -.ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} + {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} {{if len .Authors}} -AUTHOR(S): +AUTHOR: {{range .Authors}}{{ . }}{{end}} {{end}}{{if .Commands}} COMMANDS: -{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t" -}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} +{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} GLOBAL OPTIONS: {{range .VisibleFlags}}{{.}} {{end}}{{end}}{{if .Copyright }} From ac772237b96509d5150567da31ba6c6c3897d452 Mon Sep 17 00:00:00 2001 From: Maximilian Meister Date: Sun, 18 Dec 2016 17:43:48 +0100 Subject: [PATCH 04/30] command: enable ordering commands by name --- README.md | 24 ++++++++++++++++++++++-- command.go | 14 ++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6ba60c4..2bbbd8e 100644 --- a/README.md +++ b/README.md @@ -455,13 +455,13 @@ error. Flags for the application and commands are shown in the order they are defined. However, it's possible to sort them from outside this library by using `FlagsByName` -with `sort`. +or `CommandsByName` with `sort`. For example this: ``` go package main @@ -488,7 +488,27 @@ func main() { }, } + app.Commands = []cli.Command{ + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(c *cli.Context) error { + return nil + }, + }, + { + Name: "add", + Aliases: []string{"a"}, + Usage: "add a task to the list", + Action: func(c *cli.Context) error { + return nil + }, + }, + } + sort.Sort(cli.FlagsByName(app.Flags)) + sort.Sort(cli.CommandsByName(app.Commands)) app.Run(os.Args) } diff --git a/command.go b/command.go index 2628fbf..d297eb9 100644 --- a/command.go +++ b/command.go @@ -61,6 +61,20 @@ type Command struct { commandNamePath []string } +type CommandsByName []Command + +func (c CommandsByName) Len() int { + return len(c) +} + +func (c CommandsByName) Less(i, j int) bool { + return c[i].Name < c[j].Name +} + +func (c CommandsByName) Swap(i, j int) { + c[i], c[j] = c[j], c[i] +} + // FullName returns the full name of the command. // For subcommands this ensures that parent commands are part of the command path func (c Command) FullName() string { From 0083ae8732e4b5d6911729b5e60c99a19d6179ac Mon Sep 17 00:00:00 2001 From: Joe Richey Date: Mon, 9 Jan 2017 15:57:49 -0800 Subject: [PATCH 05/30] Usage/Description/ArgsUsage correctly copied when using subcommand --- command.go | 8 +++----- help.go | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/command.go b/command.go index 2628fbf..68c760e 100644 --- a/command.go +++ b/command.go @@ -230,11 +230,9 @@ func (c Command) startApp(ctx *Context) error { app.HelpName = app.Name } - if c.Description != "" { - app.Usage = c.Description - } else { - app.Usage = c.Usage - } + app.Usage = c.Usage + app.Description = c.Description + app.ArgsUsage = c.ArgsUsage // set CommandNotFound app.CommandNotFound = ctx.App.CommandNotFound diff --git a/help.go b/help.go index c8c1aee..d00e4da 100644 --- a/help.go +++ b/help.go @@ -64,7 +64,7 @@ OPTIONS: // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. var SubcommandHelpTemplate = `NAME: - {{.HelpName}} - {{.Usage}} + {{.HelpName}} - {{if .Description}}{{.Description}}{{else}}{{.Usage}}{{end}} USAGE: {{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} From acc622e5fb6f273c26946ded483aa813bebdcee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Wed, 11 Jan 2017 14:38:54 +0200 Subject: [PATCH 06/30] Invalidate context.setFlags cache on modification. --- context.go | 2 ++ context_test.go | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/context.go b/context.go index cb89e92..db94191 100644 --- a/context.go +++ b/context.go @@ -39,11 +39,13 @@ func (c *Context) NumFlags() int { // Set sets a context flag to a value. func (c *Context) Set(name, value string) error { + c.setFlags = nil return c.flagSet.Set(name, value) } // GlobalSet sets a context flag to a value on the global flagset func (c *Context) GlobalSet(name, value string) error { + globalContext(c).setFlags = nil return globalContext(c).flagSet.Set(name, value) } diff --git a/context_test.go b/context_test.go index a1ab05b..7acca10 100644 --- a/context_test.go +++ b/context_test.go @@ -375,8 +375,10 @@ func TestContext_Set(t *testing.T) { set.Int("int", 5, "an int") c := NewContext(nil, set, nil) + expect(t, c.IsSet("int"), false) c.Set("int", "1") expect(t, c.Int("int"), 1) + expect(t, c.IsSet("int"), true) } func TestContext_GlobalSet(t *testing.T) { @@ -393,7 +395,9 @@ func TestContext_GlobalSet(t *testing.T) { expect(t, c.Int("int"), 1) expect(t, c.GlobalInt("int"), 5) + expect(t, c.GlobalIsSet("int"), false) c.GlobalSet("int", "1") expect(t, c.Int("int"), 1) expect(t, c.GlobalInt("int"), 1) + expect(t, c.GlobalIsSet("int"), true) } From 71ced406af64ee9961533d518dab28d455a66666 Mon Sep 17 00:00:00 2001 From: HIROSE Masaaki Date: Thu, 12 Jan 2017 18:59:38 +0900 Subject: [PATCH 07/30] Treat `rc` first called as exit code Because default OsExiter is os.Exit. --- errors_test.go | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/errors_test.go b/errors_test.go index 131bd38..d3764b7 100644 --- a/errors_test.go +++ b/errors_test.go @@ -12,8 +12,10 @@ func TestHandleExitCoder_nil(t *testing.T) { called := false OsExiter = func(rc int) { - exitCode = rc - called = true + if !called { + exitCode = rc + called = true + } } defer func() { OsExiter = fakeOsExiter }() @@ -29,8 +31,10 @@ func TestHandleExitCoder_ExitCoder(t *testing.T) { called := false OsExiter = func(rc int) { - exitCode = rc - called = true + if !called { + exitCode = rc + called = true + } } defer func() { OsExiter = fakeOsExiter }() @@ -46,8 +50,10 @@ func TestHandleExitCoder_MultiErrorWithExitCoder(t *testing.T) { called := false OsExiter = func(rc int) { - exitCode = rc - called = true + if !called { + exitCode = rc + called = true + } } defer func() { OsExiter = fakeOsExiter }() @@ -65,8 +71,10 @@ func TestHandleExitCoder_ErrorWithMessage(t *testing.T) { called := false OsExiter = func(rc int) { - exitCode = rc - called = true + if !called { + exitCode = rc + called = true + } } ErrWriter = &bytes.Buffer{} @@ -88,8 +96,10 @@ func TestHandleExitCoder_ErrorWithoutMessage(t *testing.T) { called := false OsExiter = func(rc int) { - exitCode = rc - called = true + if !called { + exitCode = rc + called = true + } } ErrWriter = &bytes.Buffer{} @@ -123,7 +133,9 @@ func TestHandleExitCoder_ErrorWithFormat(t *testing.T) { called := false OsExiter = func(rc int) { - called = true + if !called { + called = true + } } ErrWriter = &bytes.Buffer{} @@ -143,7 +155,9 @@ func TestHandleExitCoder_MultiErrorWithFormat(t *testing.T) { called := false OsExiter = func(rc int) { - called = true + if !called { + called = true + } } ErrWriter = &bytes.Buffer{} From 286b4b56d988e0ac6da075615f1da496db94da87 Mon Sep 17 00:00:00 2001 From: HIROSE Masaaki Date: Thu, 12 Jan 2017 19:12:17 +0900 Subject: [PATCH 08/30] Document on exit code in case of MultiError is given --- errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/errors.go b/errors.go index 8aa8d9e..f9d648e 100644 --- a/errors.go +++ b/errors.go @@ -74,7 +74,7 @@ func (ee *ExitError) ExitCode() int { // HandleExitCoder checks if the error fulfills the ExitCoder interface, and if // so prints the error to stderr (if it is non-empty) and calls OsExiter with the // given exit code. If the given error is a MultiError, then this func is -// called on all members of the Errors slice. +// called on all members of the Errors slice and calls OsExiter with the last exit code. func HandleExitCoder(err error) { if err == nil { return From adec15acf537c7b696c299223a16c60cb3eeeed8 Mon Sep 17 00:00:00 2001 From: HIROSE Masaaki Date: Fri, 13 Jan 2017 15:37:09 +0900 Subject: [PATCH 09/30] Add another ExitCoder to assert that it uses last one --- errors_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/errors_test.go b/errors_test.go index d3764b7..d9e0da6 100644 --- a/errors_test.go +++ b/errors_test.go @@ -59,10 +59,11 @@ func TestHandleExitCoder_MultiErrorWithExitCoder(t *testing.T) { defer func() { OsExiter = fakeOsExiter }() exitErr := NewExitError("galactic perimeter breach", 9) - err := NewMultiError(errors.New("wowsa"), errors.New("egad"), exitErr) + exitErr2 := NewExitError("last ExitCoder", 11) + err := NewMultiError(errors.New("wowsa"), errors.New("egad"), exitErr, exitErr2) HandleExitCoder(err) - expect(t, exitCode, 9) + expect(t, exitCode, 11) expect(t, called, true) } From 7b912d9f8f8a78f192061ed5b0f40918dfda5a07 Mon Sep 17 00:00:00 2001 From: Antonio Fdez Date: Thu, 17 Nov 2016 16:58:46 +0100 Subject: [PATCH 10/30] allow to load YAML configuration files on Windows The funtion `loadDataFrom` does not take care of Windows users since any of the conditions met and it returns an error. The change looks for the runtime where it's running and then if the filePath contains a `\` --- altsrc/yaml_file_loader.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/altsrc/yaml_file_loader.go b/altsrc/yaml_file_loader.go index 335356f..433023d 100644 --- a/altsrc/yaml_file_loader.go +++ b/altsrc/yaml_file_loader.go @@ -78,6 +78,11 @@ func loadDataFrom(filePath string) ([]byte, error) { return nil, fmt.Errorf("Cannot read from file: '%s' because it does not exist.", filePath) } return ioutil.ReadFile(filePath) + } else if runtime.GOOS == "windows" && strings.Contains(u.String(), "\\") { + if _, notFoundFileErr := os.Stat(filePath); notFoundFileErr != nil { + return nil, fmt.Errorf("Cannot read from file: '%s' because it does not exist.", filePath) + } + return ioutil.ReadFile(filePath) } else { return nil, fmt.Errorf("unable to determine how to load from path %s", filePath) } From 341ca93b01a20fe6fc361e005b45d968fd559aa5 Mon Sep 17 00:00:00 2001 From: Antonio Fdez Date: Thu, 17 Nov 2016 17:08:01 +0100 Subject: [PATCH 11/30] fix imports Sorry, forgot to add imports correctly, needed to edit the file and make the commit using the github online editor, since I can't access from my current location from git. --- altsrc/yaml_file_loader.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/altsrc/yaml_file_loader.go b/altsrc/yaml_file_loader.go index 433023d..3965fe4 100644 --- a/altsrc/yaml_file_loader.go +++ b/altsrc/yaml_file_loader.go @@ -11,6 +11,8 @@ import ( "net/http" "net/url" "os" + "runtime" + "strings" "gopkg.in/urfave/cli.v1" From f1be59ff3d239f0942b201619030d302bce912cc Mon Sep 17 00:00:00 2001 From: Antonio Fdez Date: Sat, 19 Nov 2016 22:37:11 +0100 Subject: [PATCH 12/30] added comment to windows filePath check --- altsrc/yaml_file_loader.go | 1 + 1 file changed, 1 insertion(+) diff --git a/altsrc/yaml_file_loader.go b/altsrc/yaml_file_loader.go index 3965fe4..dd808d5 100644 --- a/altsrc/yaml_file_loader.go +++ b/altsrc/yaml_file_loader.go @@ -81,6 +81,7 @@ func loadDataFrom(filePath string) ([]byte, error) { } return ioutil.ReadFile(filePath) } else if runtime.GOOS == "windows" && strings.Contains(u.String(), "\\") { + // on Windows systems u.Path is always empty, so we need to check the string directly. if _, notFoundFileErr := os.Stat(filePath); notFoundFileErr != nil { return nil, fmt.Errorf("Cannot read from file: '%s' because it does not exist.", filePath) } From f8347a97c68c1b2a11f418da0225953c9b007708 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Sun, 8 Jan 2017 18:11:26 -0500 Subject: [PATCH 13/30] Fix altsrc slice inputs so that they correctly parse Was previously attempting to cast directly from []interface{} to []string or []int which Go doesn't support. Instead, we iterate over and cast each value (error'ing if the value is not the correct format). Fixes #580 --- altsrc/flag_test.go | 12 ++++---- altsrc/map_input_source.go | 58 +++++++++++++++++++++++--------------- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/altsrc/flag_test.go b/altsrc/flag_test.go index 9e9c96d..cd18294 100644 --- a/altsrc/flag_test.go +++ b/altsrc/flag_test.go @@ -59,7 +59,7 @@ func TestStringSliceApplyInputSourceValue(t *testing.T) { c := runTest(t, testApplyInputSource{ Flag: NewStringSliceFlag(cli.StringSliceFlag{Name: "test"}), FlagName: "test", - MapValue: []string{"hello", "world"}, + MapValue: []interface{}{"hello", "world"}, }) expect(t, c.StringSlice("test"), []string{"hello", "world"}) } @@ -68,7 +68,7 @@ func TestStringSliceApplyInputSourceMethodContextSet(t *testing.T) { c := runTest(t, testApplyInputSource{ Flag: NewStringSliceFlag(cli.StringSliceFlag{Name: "test"}), FlagName: "test", - MapValue: []string{"hello", "world"}, + MapValue: []interface{}{"hello", "world"}, ContextValueString: "ohno", }) expect(t, c.StringSlice("test"), []string{"ohno"}) @@ -78,7 +78,7 @@ func TestStringSliceApplyInputSourceMethodEnvVarSet(t *testing.T) { c := runTest(t, testApplyInputSource{ Flag: NewStringSliceFlag(cli.StringSliceFlag{Name: "test", EnvVar: "TEST"}), FlagName: "test", - MapValue: []string{"hello", "world"}, + MapValue: []interface{}{"hello", "world"}, EnvVarName: "TEST", EnvVarValue: "oh,no", }) @@ -89,7 +89,7 @@ func TestIntSliceApplyInputSourceValue(t *testing.T) { c := runTest(t, testApplyInputSource{ Flag: NewIntSliceFlag(cli.IntSliceFlag{Name: "test"}), FlagName: "test", - MapValue: []int{1, 2}, + MapValue: []interface{}{1, 2}, }) expect(t, c.IntSlice("test"), []int{1, 2}) } @@ -98,7 +98,7 @@ func TestIntSliceApplyInputSourceMethodContextSet(t *testing.T) { c := runTest(t, testApplyInputSource{ Flag: NewIntSliceFlag(cli.IntSliceFlag{Name: "test"}), FlagName: "test", - MapValue: []int{1, 2}, + MapValue: []interface{}{1, 2}, ContextValueString: "3", }) expect(t, c.IntSlice("test"), []int{3}) @@ -108,7 +108,7 @@ func TestIntSliceApplyInputSourceMethodEnvVarSet(t *testing.T) { c := runTest(t, testApplyInputSource{ Flag: NewIntSliceFlag(cli.IntSliceFlag{Name: "test", EnvVar: "TEST"}), FlagName: "test", - MapValue: []int{1, 2}, + MapValue: []interface{}{1, 2}, EnvVarName: "TEST", EnvVarValue: "3,4", }) diff --git a/altsrc/map_input_source.go b/altsrc/map_input_source.go index b720995..b3169e0 100644 --- a/altsrc/map_input_source.go +++ b/altsrc/map_input_source.go @@ -130,45 +130,59 @@ func (fsm *MapInputSource) String(name string) (string, error) { // StringSlice returns an []string from the map if it exists otherwise returns nil func (fsm *MapInputSource) StringSlice(name string) ([]string, error) { otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.([]string) - if !isType { - return nil, incorrectTypeForFlagError(name, "[]string", otherGenericValue) + if !exists { + otherGenericValue, exists = nestedVal(name, fsm.valueMap) + if !exists { + return nil, nil } - return otherValue, nil } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - otherValue, isType := nestedGenericValue.([]string) + + otherValue, isType := otherGenericValue.([]interface{}) + if !isType { + return nil, incorrectTypeForFlagError(name, "[]interface{}", otherGenericValue) + } + + var stringSlice = make([]string, 0, len(otherValue)) + for i, v := range otherValue { + stringValue, isType := v.(string) + if !isType { - return nil, incorrectTypeForFlagError(name, "[]string", nestedGenericValue) + return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "string", v) } - return otherValue, nil + + stringSlice = append(stringSlice, stringValue) } - return nil, nil + return stringSlice, nil } // IntSlice returns an []int from the map if it exists otherwise returns nil func (fsm *MapInputSource) IntSlice(name string) ([]int, error) { otherGenericValue, exists := fsm.valueMap[name] - if exists { - otherValue, isType := otherGenericValue.([]int) - if !isType { - return nil, incorrectTypeForFlagError(name, "[]int", otherGenericValue) + if !exists { + otherGenericValue, exists = nestedVal(name, fsm.valueMap) + if !exists { + return nil, nil } - return otherValue, nil } - nestedGenericValue, exists := nestedVal(name, fsm.valueMap) - if exists { - otherValue, isType := nestedGenericValue.([]int) + + otherValue, isType := otherGenericValue.([]interface{}) + if !isType { + return nil, incorrectTypeForFlagError(name, "[]interface{}", otherGenericValue) + } + + var intSlice = make([]int, 0, len(otherValue)) + for i, v := range otherValue { + intValue, isType := v.(int) + if !isType { - return nil, incorrectTypeForFlagError(name, "[]int", nestedGenericValue) + return nil, incorrectTypeForFlagError(fmt.Sprintf("%s[%d]", name, i), "int", v) } - return otherValue, nil + + intSlice = append(intSlice, intValue) } - return nil, nil + return intSlice, nil } // Generic returns an cli.Generic from the map if it exists otherwise returns nil From e87dfbb6bb85250206841a9576e51b1323910356 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Mon, 16 Jan 2017 17:34:12 -0800 Subject: [PATCH 14/30] altsrc: Support slices for TOML --- altsrc/toml_file_loader.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/altsrc/toml_file_loader.go b/altsrc/toml_file_loader.go index 39c124f..37870fc 100644 --- a/altsrc/toml_file_loader.go +++ b/altsrc/toml_file_loader.go @@ -57,8 +57,8 @@ func unmarshalMap(i interface{}) (ret map[interface{}]interface{}, err error) { } else { return nil, err } - case reflect.Array: - fallthrough // [todo] - Support array type + case reflect.Array, reflect.Slice: + ret[key] = val.([]interface{}) default: return nil, fmt.Errorf("Unsupported: type = %#v", v.Kind()) } From 4ed366e2011dfb9efa3399c709af0976d5e87db4 Mon Sep 17 00:00:00 2001 From: Robert Bittle Date: Wed, 18 Jan 2017 23:29:26 -0500 Subject: [PATCH 15/30] Pass the ErrWriter on the root app to subcommands --- command.go | 1 + command_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/command.go b/command.go index d297eb9..4a409d4 100644 --- a/command.go +++ b/command.go @@ -264,6 +264,7 @@ func (c Command) startApp(ctx *Context) error { app.Author = ctx.App.Author app.Email = ctx.App.Email app.Writer = ctx.App.Writer + app.ErrWriter = ctx.App.ErrWriter app.categories = CommandCategories{} for _, command := range c.Subcommands { diff --git a/command_test.go b/command_test.go index 5e0e8de..5710184 100644 --- a/command_test.go +++ b/command_test.go @@ -153,3 +153,32 @@ func TestCommand_OnUsageError_WithWrongFlagValue(t *testing.T) { t.Errorf("Expect an intercepted error, but got \"%v\"", err) } } + +func TestCommand_Run_SubcommandsCanUseErrWriter(t *testing.T) { + app := NewApp() + app.ErrWriter = ioutil.Discard + app.Commands = []Command{ + { + Name: "bar", + Usage: "this is for testing", + Subcommands: []Command{ + { + Name: "baz", + Usage: "this is for testing", + Action: func(c *Context) error { + if c.App.ErrWriter != ioutil.Discard { + return fmt.Errorf("ErrWriter not passed") + } + + return nil + }, + }, + }, + }, + } + + err := app.Run([]string{"foo", "bar", "baz"}) + if err != nil { + t.Fatal(err) + } +} From 2526b57c56f30b50466c96c4133b1a4ad0f0191f Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Tue, 14 Feb 2017 21:17:05 -0800 Subject: [PATCH 16/30] Allow slightly longer lines in Python scripts --- .flake8 | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..6deafc2 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 From 8d8f927bcb0447918aaa09c5b87160ddf2e5842b Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Sat, 4 Mar 2017 14:28:24 -0800 Subject: [PATCH 17/30] Change flag test error checking to use regexp rather than strings As the error messages changed in 1.8 --- flag_test.go | 92 +++++++++++++++++++++++++++++----------------------- 1 file changed, 52 insertions(+), 40 deletions(-) diff --git a/flag_test.go b/flag_test.go index 0dd8654..1ccb639 100644 --- a/flag_test.go +++ b/flag_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "reflect" + "regexp" "runtime" "strings" "testing" @@ -31,57 +32,57 @@ func TestBoolFlagHelpOutput(t *testing.T) { func TestFlagsFromEnv(t *testing.T) { var flagTests = []struct { - input string - output interface{} - flag Flag - err error + input string + output interface{} + flag Flag + errRegexp string }{ - {"", false, BoolFlag{Name: "debug", EnvVar: "DEBUG"}, nil}, - {"1", true, BoolFlag{Name: "debug", EnvVar: "DEBUG"}, nil}, - {"false", false, BoolFlag{Name: "debug", EnvVar: "DEBUG"}, nil}, - {"foobar", true, BoolFlag{Name: "debug", EnvVar: "DEBUG"}, fmt.Errorf(`could not parse foobar as bool value for flag debug: strconv.ParseBool: parsing "foobar": invalid syntax`)}, + {"", false, BoolFlag{Name: "debug", EnvVar: "DEBUG"}, ""}, + {"1", true, BoolFlag{Name: "debug", EnvVar: "DEBUG"}, ""}, + {"false", false, BoolFlag{Name: "debug", EnvVar: "DEBUG"}, ""}, + {"foobar", true, BoolFlag{Name: "debug", EnvVar: "DEBUG"}, fmt.Sprintf(`could not parse foobar as bool value for flag debug: .*`)}, - {"", false, BoolTFlag{Name: "debug", EnvVar: "DEBUG"}, nil}, - {"1", true, BoolTFlag{Name: "debug", EnvVar: "DEBUG"}, nil}, - {"false", false, BoolTFlag{Name: "debug", EnvVar: "DEBUG"}, nil}, - {"foobar", true, BoolTFlag{Name: "debug", EnvVar: "DEBUG"}, fmt.Errorf(`could not parse foobar as bool value for flag debug: strconv.ParseBool: parsing "foobar": invalid syntax`)}, + {"", false, BoolTFlag{Name: "debug", EnvVar: "DEBUG"}, ""}, + {"1", true, BoolTFlag{Name: "debug", EnvVar: "DEBUG"}, ""}, + {"false", false, BoolTFlag{Name: "debug", EnvVar: "DEBUG"}, ""}, + {"foobar", true, BoolTFlag{Name: "debug", EnvVar: "DEBUG"}, fmt.Sprintf(`could not parse foobar as bool value for flag debug: .*`)}, - {"1s", 1 * time.Second, DurationFlag{Name: "time", EnvVar: "TIME"}, nil}, - {"foobar", false, DurationFlag{Name: "time", EnvVar: "TIME"}, fmt.Errorf(`could not parse foobar as duration for flag time: time: invalid duration foobar`)}, + {"1s", 1 * time.Second, DurationFlag{Name: "time", EnvVar: "TIME"}, ""}, + {"foobar", false, DurationFlag{Name: "time", EnvVar: "TIME"}, fmt.Sprintf(`could not parse foobar as duration for flag time: .*`)}, - {"1.2", 1.2, Float64Flag{Name: "seconds", EnvVar: "SECONDS"}, nil}, - {"1", 1.0, Float64Flag{Name: "seconds", EnvVar: "SECONDS"}, nil}, - {"foobar", 0, Float64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse foobar as float64 value for flag seconds: strconv.ParseFloat: parsing "foobar": invalid syntax`)}, + {"1.2", 1.2, Float64Flag{Name: "seconds", EnvVar: "SECONDS"}, ""}, + {"1", 1.0, Float64Flag{Name: "seconds", EnvVar: "SECONDS"}, ""}, + {"foobar", 0, Float64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse foobar as float64 value for flag seconds: .*`)}, - {"1", int64(1), Int64Flag{Name: "seconds", EnvVar: "SECONDS"}, nil}, - {"1.2", 0, Int64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse 1.2 as int value for flag seconds: strconv.ParseInt: parsing "1.2": invalid syntax`)}, - {"foobar", 0, Int64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse foobar as int value for flag seconds: strconv.ParseInt: parsing "foobar": invalid syntax`)}, + {"1", int64(1), Int64Flag{Name: "seconds", EnvVar: "SECONDS"}, ""}, + {"1.2", 0, Int64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse 1.2 as int value for flag seconds: .*`)}, + {"foobar", 0, Int64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse foobar as int value for flag seconds: .*`)}, - {"1", 1, IntFlag{Name: "seconds", EnvVar: "SECONDS"}, nil}, - {"1.2", 0, IntFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse 1.2 as int value for flag seconds: strconv.ParseInt: parsing "1.2": invalid syntax`)}, - {"foobar", 0, IntFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse foobar as int value for flag seconds: strconv.ParseInt: parsing "foobar": invalid syntax`)}, + {"1", 1, IntFlag{Name: "seconds", EnvVar: "SECONDS"}, ""}, + {"1.2", 0, IntFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse 1.2 as int value for flag seconds: .*`)}, + {"foobar", 0, IntFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse foobar as int value for flag seconds: .*`)}, - {"1,2", IntSlice{1, 2}, IntSliceFlag{Name: "seconds", EnvVar: "SECONDS"}, nil}, - {"1.2,2", IntSlice{}, IntSliceFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse 1.2,2 as int slice value for flag seconds: strconv.ParseInt: parsing "1.2": invalid syntax`)}, - {"foobar", IntSlice{}, IntSliceFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse foobar as int slice value for flag seconds: strconv.ParseInt: parsing "foobar": invalid syntax`)}, + {"1,2", IntSlice{1, 2}, IntSliceFlag{Name: "seconds", EnvVar: "SECONDS"}, ""}, + {"1.2,2", IntSlice{}, IntSliceFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse 1.2,2 as int slice value for flag seconds: .*`)}, + {"foobar", IntSlice{}, IntSliceFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse foobar as int slice value for flag seconds: .*`)}, - {"1,2", Int64Slice{1, 2}, Int64SliceFlag{Name: "seconds", EnvVar: "SECONDS"}, nil}, - {"1.2,2", Int64Slice{}, Int64SliceFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse 1.2,2 as int64 slice value for flag seconds: strconv.ParseInt: parsing "1.2": invalid syntax`)}, - {"foobar", Int64Slice{}, Int64SliceFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse foobar as int64 slice value for flag seconds: strconv.ParseInt: parsing "foobar": invalid syntax`)}, + {"1,2", Int64Slice{1, 2}, Int64SliceFlag{Name: "seconds", EnvVar: "SECONDS"}, ""}, + {"1.2,2", Int64Slice{}, Int64SliceFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse 1.2,2 as int64 slice value for flag seconds: .*`)}, + {"foobar", Int64Slice{}, Int64SliceFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse foobar as int64 slice value for flag seconds: .*`)}, - {"foo", "foo", StringFlag{Name: "name", EnvVar: "NAME"}, nil}, + {"foo", "foo", StringFlag{Name: "name", EnvVar: "NAME"}, ""}, - {"foo,bar", StringSlice{"foo", "bar"}, StringSliceFlag{Name: "names", EnvVar: "NAMES"}, nil}, + {"foo,bar", StringSlice{"foo", "bar"}, StringSliceFlag{Name: "names", EnvVar: "NAMES"}, ""}, - {"1", uint(1), UintFlag{Name: "seconds", EnvVar: "SECONDS"}, nil}, - {"1.2", 0, UintFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse 1.2 as uint value for flag seconds: strconv.ParseUint: parsing "1.2": invalid syntax`)}, - {"foobar", 0, UintFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse foobar as uint value for flag seconds: strconv.ParseUint: parsing "foobar": invalid syntax`)}, + {"1", uint(1), UintFlag{Name: "seconds", EnvVar: "SECONDS"}, ""}, + {"1.2", 0, UintFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse 1.2 as uint value for flag seconds: .*`)}, + {"foobar", 0, UintFlag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse foobar as uint value for flag seconds: .*`)}, - {"1", uint64(1), Uint64Flag{Name: "seconds", EnvVar: "SECONDS"}, nil}, - {"1.2", 0, Uint64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse 1.2 as uint64 value for flag seconds: strconv.ParseUint: parsing "1.2": invalid syntax`)}, - {"foobar", 0, Uint64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Errorf(`could not parse foobar as uint64 value for flag seconds: strconv.ParseUint: parsing "foobar": invalid syntax`)}, + {"1", uint64(1), Uint64Flag{Name: "seconds", EnvVar: "SECONDS"}, ""}, + {"1.2", 0, Uint64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse 1.2 as uint64 value for flag seconds: .*`)}, + {"foobar", 0, Uint64Flag{Name: "seconds", EnvVar: "SECONDS"}, fmt.Sprintf(`could not parse foobar as uint64 value for flag seconds: .*`)}, - {"foo,bar", &Parser{"foo", "bar"}, GenericFlag{Name: "names", Value: &Parser{}, EnvVar: "NAMES"}, nil}, + {"foo,bar", &Parser{"foo", "bar"}, GenericFlag{Name: "names", Value: &Parser{}, EnvVar: "NAMES"}, ""}, } for _, test := range flagTests { @@ -98,8 +99,19 @@ func TestFlagsFromEnv(t *testing.T) { } err := a.Run([]string{"run"}) - if !reflect.DeepEqual(test.err, err) { - t.Errorf("expected error %s, got error %s", test.err, err) + + if test.errRegexp != "" { + if err == nil { + t.Errorf("expected error to match %s, got none", test.errRegexp) + } else { + if matched, _ := regexp.MatchString(test.errRegexp, err.Error()); !matched { + t.Errorf("expected error to match %s, got error %s", test.errRegexp, err) + } + } + } else { + if err != nil && test.errRegexp == "" { + t.Errorf("expected no error got %s", err) + } } } } From 9e5b04886c4bfee2ceba1465b8121057355c4e53 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Sat, 4 Mar 2017 14:33:36 -0800 Subject: [PATCH 18/30] Remove logic that exited even if the error was not an OsExiter This was introduced in #496, but was discovered to be a regression in the behavior of the library. --- errors.go | 9 --------- errors_test.go | 52 +------------------------------------------------- 2 files changed, 1 insertion(+), 60 deletions(-) diff --git a/errors.go b/errors.go index f9d648e..562b295 100644 --- a/errors.go +++ b/errors.go @@ -97,15 +97,6 @@ func HandleExitCoder(err error) { OsExiter(code) return } - - if err.Error() != "" { - if _, ok := err.(ErrorFormatter); ok { - fmt.Fprintf(ErrWriter, "%+v\n", err) - } else { - fmt.Fprintln(ErrWriter, err) - } - } - OsExiter(1) } func handleMultiError(multiErr MultiError) int { diff --git a/errors_test.go b/errors_test.go index d9e0da6..9b609c5 100644 --- a/errors_test.go +++ b/errors_test.go @@ -67,56 +67,6 @@ func TestHandleExitCoder_MultiErrorWithExitCoder(t *testing.T) { expect(t, called, true) } -func TestHandleExitCoder_ErrorWithMessage(t *testing.T) { - exitCode := 0 - called := false - - OsExiter = func(rc int) { - if !called { - exitCode = rc - called = true - } - } - ErrWriter = &bytes.Buffer{} - - defer func() { - OsExiter = fakeOsExiter - ErrWriter = fakeErrWriter - }() - - err := errors.New("gourd havens") - HandleExitCoder(err) - - expect(t, exitCode, 1) - expect(t, called, true) - expect(t, ErrWriter.(*bytes.Buffer).String(), "gourd havens\n") -} - -func TestHandleExitCoder_ErrorWithoutMessage(t *testing.T) { - exitCode := 0 - called := false - - OsExiter = func(rc int) { - if !called { - exitCode = rc - called = true - } - } - ErrWriter = &bytes.Buffer{} - - defer func() { - OsExiter = fakeOsExiter - ErrWriter = fakeErrWriter - }() - - err := errors.New("") - HandleExitCoder(err) - - expect(t, exitCode, 1) - expect(t, called, true) - expect(t, ErrWriter.(*bytes.Buffer).String(), "") -} - // make a stub to not import pkg/errors type ErrorWithFormat struct { error @@ -145,7 +95,7 @@ func TestHandleExitCoder_ErrorWithFormat(t *testing.T) { ErrWriter = fakeErrWriter }() - err := NewErrorWithFormat("I am formatted") + err := NewExitError(NewErrorWithFormat("I am formatted"), 1) HandleExitCoder(err) expect(t, called, true) From b9c535392027a122ff9776531002d450821426e5 Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 6 Apr 2017 15:11:23 +0200 Subject: [PATCH 19/30] Unset PROG variable to fix issue when mulitple apps run autocomplete from etc/bash_completion.d directory --- autocomplete/bash_autocomplete | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) mode change 100644 => 100755 autocomplete/bash_autocomplete diff --git a/autocomplete/bash_autocomplete b/autocomplete/bash_autocomplete old mode 100644 new mode 100755 index 21a232f..37d9c14 --- a/autocomplete/bash_autocomplete +++ b/autocomplete/bash_autocomplete @@ -3,12 +3,14 @@ : ${PROG:=$(basename ${BASH_SOURCE})} _cli_bash_autocomplete() { - local cur opts base - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - } - - complete -F _cli_bash_autocomplete $PROG + local cur opts base + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 +} + +complete -F _cli_bash_autocomplete $PROG + +unset PROG From 572dc46db5570298570b06ed63ae261086c8c7a4 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Mon, 24 Apr 2017 10:39:43 -0700 Subject: [PATCH 20/30] Explicitly fetch `goimports` Fetching the whole tree was failing on some Go versions and we really only need goimports. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 94836d7..4417251 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,7 +26,7 @@ matrix: before_script: - go get github.com/urfave/gfmrun/... || true -- go get golang.org/x/tools/... || true +- go get golang.org/x/tools/cmd/goimports - if [ ! -f node_modules/.bin/markdown-toc ] ; then npm install markdown-toc ; fi From 1bf0bb96b7b005507c19a2d71f4f3cadcc1202e2 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Mon, 24 Apr 2017 10:54:32 -0700 Subject: [PATCH 21/30] Only support supported Go versions As described on https://github.com/golang/go/wiki/Go-Release-Cycle The maintainers do not test compatibility for libraries (e.g. in golang.org/x/tools) on older versions. --- .travis.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4417251..890e185 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,12 +7,9 @@ cache: - node_modules go: -- 1.2.x -- 1.3.x -- 1.4.2 -- 1.5.x - 1.6.x - 1.7.x +- 1.8.x - master matrix: @@ -23,6 +20,8 @@ matrix: os: osx - go: 1.7.x os: osx + - go: 1.8.x + os: osx before_script: - go get github.com/urfave/gfmrun/... || true From 87fe13079e3f452a366ae8d7f991261f9324d630 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Mon, 24 Apr 2017 15:19:34 -0700 Subject: [PATCH 22/30] Rely on Command context in Run() Was previously relying on the parent context which caused things like `.Command` to not be available to OnUsageError(). Fixes #609 --- command.go | 16 ++++++++-------- command_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/command.go b/command.go index 40ebdb6..63f183a 100644 --- a/command.go +++ b/command.go @@ -154,19 +154,20 @@ func (c Command) Run(ctx *Context) (err error) { } context := NewContext(ctx.App, set, ctx) + context.Command = c if checkCommandCompletions(context, c.Name) { return nil } if err != nil { if c.OnUsageError != nil { - err := c.OnUsageError(ctx, err, false) + err := c.OnUsageError(context, err, false) HandleExitCoder(err) return err } - fmt.Fprintln(ctx.App.Writer, "Incorrect Usage:", err.Error()) - fmt.Fprintln(ctx.App.Writer) - ShowCommandHelp(ctx, c.Name) + fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error()) + fmt.Fprintln(context.App.Writer) + ShowCommandHelp(context, c.Name) return err } @@ -191,9 +192,9 @@ func (c Command) Run(ctx *Context) (err error) { if c.Before != nil { err = c.Before(context) if err != nil { - fmt.Fprintln(ctx.App.Writer, err) - fmt.Fprintln(ctx.App.Writer) - ShowCommandHelp(ctx, c.Name) + fmt.Fprintln(context.App.Writer, err) + fmt.Fprintln(context.App.Writer) + ShowCommandHelp(context, c.Name) HandleExitCoder(err) return err } @@ -203,7 +204,6 @@ func (c Command) Run(ctx *Context) (err error) { c.Action = helpSubcommand.Action } - context.Command = c err = HandleAction(c.Action, context) if err != nil { diff --git a/command_test.go b/command_test.go index 5710184..10fff2d 100644 --- a/command_test.go +++ b/command_test.go @@ -127,6 +127,30 @@ func TestCommand_Run_BeforeSavesMetadata(t *testing.T) { } } +func TestCommand_OnUsageError_hasCommandContext(t *testing.T) { + app := NewApp() + app.Commands = []Command{ + { + Name: "bar", + Flags: []Flag{ + IntFlag{Name: "flag"}, + }, + OnUsageError: func(c *Context, err error, _ bool) error { + return fmt.Errorf("intercepted in %s: %s", c.Command.Name, err.Error()) + }, + }, + } + + err := app.Run([]string{"foo", "bar", "--flag=wrong"}) + if err == nil { + t.Fatalf("expected to receive error from Run, got none") + } + + if !strings.HasPrefix(err.Error(), "intercepted in bar") { + t.Errorf("Expect an intercepted error, but got \"%v\"", err) + } +} + func TestCommand_OnUsageError_WithWrongFlagValue(t *testing.T) { app := NewApp() app.Commands = []Command{ From 1794792adfc0f1cea5a4e56eefc36fd849018d05 Mon Sep 17 00:00:00 2001 From: "Joe Richey joerichey@google.com" Date: Fri, 5 May 2017 20:07:18 -0700 Subject: [PATCH 23/30] Add ability to use custom Flag types Users can now use custom flags types (conforming to the Flag interface) in their applications. They can also use custom flags for the three global flags (Help, Version, bash completion). --- app_test.go | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++-- flag.go | 12 +++++------ help.go | 10 ++++----- 3 files changed, 70 insertions(+), 13 deletions(-) diff --git a/app_test.go b/app_test.go index 10f1562..e14ddaf 100644 --- a/app_test.go +++ b/app_test.go @@ -1520,6 +1520,63 @@ func TestApp_OnUsageError_WithWrongFlagValue_ForSubcommand(t *testing.T) { } } +// A custom flag that conforms to the relevant interfaces, but has none of the +// fields that the other flag types do. +type customBoolFlag struct { + Nombre string +} + +// Don't use the normal FlagStringer +func (c *customBoolFlag) String() string { + return "***" + c.Nombre + "***" +} + +func (c *customBoolFlag) GetName() string { + return c.Nombre +} + +func (c *customBoolFlag) Apply(set *flag.FlagSet) { + set.String(c.Nombre, c.Nombre, "") +} + +func TestCustomFlagsUnused(t *testing.T) { + app := NewApp() + app.Flags = []Flag{&customBoolFlag{"custom"}} + + err := app.Run([]string{"foo"}) + if err != nil { + t.Errorf("Run returned unexpected error: %v", err) + } +} + +func TestCustomFlagsUsed(t *testing.T) { + app := NewApp() + app.Flags = []Flag{&customBoolFlag{"custom"}} + + err := app.Run([]string{"foo", "--custom=bar"}) + if err != nil { + t.Errorf("Run returned unexpected error: %v", err) + } +} + +func TestCustomHelpVersionFlags(t *testing.T) { + app := NewApp() + + // Be sure to reset the global flags + defer func(helpFlag Flag, versionFlag Flag) { + HelpFlag = helpFlag + VersionFlag = versionFlag + }(HelpFlag, VersionFlag) + + HelpFlag = &customBoolFlag{"help-custom"} + VersionFlag = &customBoolFlag{"version-custom"} + + err := app.Run([]string{"foo", "--help-custom=bar"}) + if err != nil { + t.Errorf("Run returned unexpected error: %v", err) + } +} + func TestHandleAction_WithNonFuncAction(t *testing.T) { app := NewApp() app.Action = 42 @@ -1642,7 +1699,7 @@ func TestShellCompletionForIncompleteFlags(t *testing.T) { for _, flag := range ctx.App.Flags { for _, name := range strings.Split(flag.GetName(), ",") { - if name == BashCompletionFlag.Name { + if name == BashCompletionFlag.GetName() { continue } @@ -1659,7 +1716,7 @@ func TestShellCompletionForIncompleteFlags(t *testing.T) { app.Action = func(ctx *Context) error { return fmt.Errorf("should not get here") } - err := app.Run([]string{"", "--test-completion", "--" + BashCompletionFlag.Name}) + err := app.Run([]string{"", "--test-completion", "--" + BashCompletionFlag.GetName()}) if err != nil { t.Errorf("app should not return an error: %s", err) } diff --git a/flag.go b/flag.go index 7dd8a2c..877ff35 100644 --- a/flag.go +++ b/flag.go @@ -14,13 +14,13 @@ import ( const defaultPlaceholder = "value" // BashCompletionFlag enables bash-completion for all commands and subcommands -var BashCompletionFlag = BoolFlag{ +var BashCompletionFlag Flag = BoolFlag{ Name: "generate-bash-completion", Hidden: true, } // VersionFlag prints the version for the application -var VersionFlag = BoolFlag{ +var VersionFlag Flag = BoolFlag{ Name: "version, v", Usage: "print the version", } @@ -28,7 +28,7 @@ var VersionFlag = BoolFlag{ // HelpFlag prints the help for all commands and subcommands // Set to the zero value (BoolFlag{}) to disable flag -- keeps subcommand // unless HideHelp is set to true) -var HelpFlag = BoolFlag{ +var HelpFlag Flag = BoolFlag{ Name: "help, h", Usage: "show help", } @@ -630,7 +630,8 @@ func (f Float64Flag) ApplyWithError(set *flag.FlagSet) error { func visibleFlags(fl []Flag) []Flag { visible := []Flag{} for _, flag := range fl { - if !flagValue(flag).FieldByName("Hidden").Bool() { + field := flagValue(flag).FieldByName("Hidden") + if !field.IsValid() || !field.Bool() { visible = append(visible, flag) } } @@ -723,9 +724,8 @@ func stringifyFlag(f Flag) string { needsPlaceholder := false defaultValueString := "" - val := fv.FieldByName("Value") - if val.IsValid() { + if val := fv.FieldByName("Value"); val.IsValid() { needsPlaceholder = true defaultValueString = fmt.Sprintf(" (default: %v)", val.Interface()) diff --git a/help.go b/help.go index d00e4da..df4cb56 100644 --- a/help.go +++ b/help.go @@ -212,8 +212,8 @@ func printHelp(out io.Writer, templ string, data interface{}) { func checkVersion(c *Context) bool { found := false - if VersionFlag.Name != "" { - eachName(VersionFlag.Name, func(name string) { + if VersionFlag.GetName() != "" { + eachName(VersionFlag.GetName(), func(name string) { if c.GlobalBool(name) || c.Bool(name) { found = true } @@ -224,8 +224,8 @@ func checkVersion(c *Context) bool { func checkHelp(c *Context) bool { found := false - if HelpFlag.Name != "" { - eachName(HelpFlag.Name, func(name string) { + if HelpFlag.GetName() != "" { + eachName(HelpFlag.GetName(), func(name string) { if c.GlobalBool(name) || c.Bool(name) { found = true } @@ -260,7 +260,7 @@ func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) { pos := len(arguments) - 1 lastArg := arguments[pos] - if lastArg != "--"+BashCompletionFlag.Name { + if lastArg != "--"+BashCompletionFlag.GetName() { return false, arguments } From f7d6a07f2d060ba198dc9c00a48fa6dbe03fb7b5 Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Fri, 25 Nov 2016 00:16:48 -0800 Subject: [PATCH 24/30] Add support for custom help templates. --- app.go | 4 ++++ command.go | 6 ++++++ context.go | 16 +++++++++++++++- help.go | 24 ++++++++++++++++++++++-- 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/app.go b/app.go index 95ffc0b..8f84cb3 100644 --- a/app.go +++ b/app.go @@ -85,6 +85,10 @@ type App struct { ErrWriter io.Writer // Other custom info Metadata map[string]interface{} + // CustomAppHelpTemplate the text template for app help topic. + // cli.go uses text/template to render templates. You can + // render custom help text by setting this variable. + CustomAppHelpTemplate string didSetup bool } diff --git a/command.go b/command.go index 63f183a..a83495e 100644 --- a/command.go +++ b/command.go @@ -59,6 +59,11 @@ type Command struct { // Full name of command for help, defaults to full command name, including parent commands. HelpName string commandNamePath []string + + // CustomHelpTemplate the text template for the command help topic. + // cli.go uses text/template to render templates. You can + // render custom help text by setting this variable. + CustomHelpTemplate string } type CommandsByName []Command @@ -250,6 +255,7 @@ func (c Command) startApp(ctx *Context) error { // set CommandNotFound app.CommandNotFound = ctx.App.CommandNotFound + app.CustomAppHelpTemplate = c.CustomHelpTemplate // set the flags and commands app.Commands = c.Subcommands diff --git a/context.go b/context.go index cb89e92..021e5e5 100644 --- a/context.go +++ b/context.go @@ -186,9 +186,23 @@ func (a Args) First() string { return a.Get(0) } +// Last - Return the last argument, or else a blank string +func (a Args) Last() string { + return a.Get(len(a) - 1) +} + +// Head - Return the rest of the arguments (not the last one) +// or else an empty string slice +func (a Args) Head() Args { + if len(a) == 1 { + return a + } + return []string(a)[:len(a)-1] +} + // Tail returns the rest of the arguments (not the first one) // or else an empty string slice -func (a Args) Tail() []string { +func (a Args) Tail() Args { if len(a) >= 2 { return []string(a)[1:] } diff --git a/help.go b/help.go index d00e4da..78f84f5 100644 --- a/help.go +++ b/help.go @@ -120,9 +120,19 @@ var HelpPrinter helpPrinter = printHelp // VersionPrinter prints the version for the App var VersionPrinter = printVersion +// ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code. +func ShowAppHelpAndExit(c *Context, exitCode int) { + ShowAppHelp(c) + os.Exit(exitCode) +} + // ShowAppHelp is an action that displays the help. func ShowAppHelp(c *Context) error { - HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) + if c.App.CustomAppHelpTemplate != "" { + HelpPrinter(c.App.Writer, c.App.CustomAppHelpTemplate, c.App) + } else { + HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) + } return nil } @@ -138,6 +148,12 @@ func DefaultAppComplete(c *Context) { } } +// ShowCommandHelpAndExit - exits with code after showing help +func ShowCommandHelpAndExit(c *Context, command string, code int) { + ShowCommandHelp(c, command) + os.Exit(code) +} + // ShowCommandHelp prints help for the given command func ShowCommandHelp(ctx *Context, command string) error { // show the subcommand help for a command with subcommands @@ -148,7 +164,11 @@ func ShowCommandHelp(ctx *Context, command string) error { for _, c := range ctx.App.Commands { if c.HasName(command) { - HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) + if c.CustomHelpTemplate != "" { + HelpPrinter(ctx.App.Writer, c.CustomHelpTemplate, c) + } else { + HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) + } return nil } } From baa33cb888078362b0b955d6f8715445ad2cf662 Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Fri, 25 Nov 2016 01:07:42 -0800 Subject: [PATCH 25/30] Add support for ExtraInfo. --- app.go | 2 ++ help.go | 26 ++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/app.go b/app.go index 8f84cb3..26a4d4d 100644 --- a/app.go +++ b/app.go @@ -85,6 +85,8 @@ type App struct { ErrWriter io.Writer // Other custom info Metadata map[string]interface{} + // Carries a function which returns app specific info. + ExtraInfo func() map[string]string // CustomAppHelpTemplate the text template for app help topic. // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. diff --git a/help.go b/help.go index 78f84f5..b8ffee0 100644 --- a/help.go +++ b/help.go @@ -112,11 +112,18 @@ var helpSubcommand = Command{ // Prints help for the App or Command type helpPrinter func(w io.Writer, templ string, data interface{}) +// Prints help for the App or Command with custom template function. +type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{}) + // HelpPrinter is a function that writes the help output. If not set a default // is used. The function signature is: // func(w io.Writer, templ string, data interface{}) var HelpPrinter helpPrinter = printHelp +// HelPrinterCustom is same as HelpPrinter but +// takes a custom function for template function map. +var HelpPrinterCustom helpPrinterCustom = printHelpCustom + // VersionPrinter prints the version for the App var VersionPrinter = printVersion @@ -129,7 +136,13 @@ func ShowAppHelpAndExit(c *Context, exitCode int) { // ShowAppHelp is an action that displays the help. func ShowAppHelp(c *Context) error { if c.App.CustomAppHelpTemplate != "" { - HelpPrinter(c.App.Writer, c.App.CustomAppHelpTemplate, c.App) + if c.App.ExtraInfo != nil { + HelpPrinterCustom(c.App.Writer, c.App.CustomAppHelpTemplate, c.App, map[string]interface{}{ + "ExtraInfo": c.App.ExtraInfo, + }) + } else { + HelpPrinter(c.App.Writer, c.App.CustomAppHelpTemplate, c.App) + } } else { HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) } @@ -211,10 +224,15 @@ func ShowCommandCompletions(ctx *Context, command string) { } } -func printHelp(out io.Writer, templ string, data interface{}) { +func printHelpCustom(out io.Writer, templ string, data interface{}, customFunc map[string]interface{}) { funcMap := template.FuncMap{ "join": strings.Join, } + if customFunc != nil { + for key, value := range customFunc { + funcMap[key] = value + } + } w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0) t := template.Must(template.New("help").Funcs(funcMap).Parse(templ)) @@ -230,6 +248,10 @@ func printHelp(out io.Writer, templ string, data interface{}) { w.Flush() } +func printHelp(out io.Writer, templ string, data interface{}) { + printHelpCustom(out, templ, data, nil) +} + func checkVersion(c *Context) bool { found := false if VersionFlag.Name != "" { From dd3849a7e602d4506eace87bed5202d9d416f44f Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Wed, 15 Feb 2017 01:44:04 -0800 Subject: [PATCH 26/30] Add tests as requested. --- context.go | 16 +------ help.go | 26 +++++------ help_test.go | 120 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 27 deletions(-) diff --git a/context.go b/context.go index 021e5e5..cb89e92 100644 --- a/context.go +++ b/context.go @@ -186,23 +186,9 @@ func (a Args) First() string { return a.Get(0) } -// Last - Return the last argument, or else a blank string -func (a Args) Last() string { - return a.Get(len(a) - 1) -} - -// Head - Return the rest of the arguments (not the last one) -// or else an empty string slice -func (a Args) Head() Args { - if len(a) == 1 { - return a - } - return []string(a)[:len(a)-1] -} - // Tail returns the rest of the arguments (not the first one) // or else an empty string slice -func (a Args) Tail() Args { +func (a Args) Tail() []string { if len(a) >= 2 { return []string(a)[1:] } diff --git a/help.go b/help.go index b8ffee0..e5602d8 100644 --- a/help.go +++ b/help.go @@ -120,7 +120,7 @@ type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customF // func(w io.Writer, templ string, data interface{}) var HelpPrinter helpPrinter = printHelp -// HelPrinterCustom is same as HelpPrinter but +// HelpPrinterCustom is same as HelpPrinter but // takes a custom function for template function map. var HelpPrinterCustom helpPrinterCustom = printHelpCustom @@ -134,18 +134,20 @@ func ShowAppHelpAndExit(c *Context, exitCode int) { } // ShowAppHelp is an action that displays the help. -func ShowAppHelp(c *Context) error { - if c.App.CustomAppHelpTemplate != "" { - if c.App.ExtraInfo != nil { - HelpPrinterCustom(c.App.Writer, c.App.CustomAppHelpTemplate, c.App, map[string]interface{}{ - "ExtraInfo": c.App.ExtraInfo, - }) - } else { - HelpPrinter(c.App.Writer, c.App.CustomAppHelpTemplate, c.App) - } - } else { +func ShowAppHelp(c *Context) (err error) { + if c.App.CustomAppHelpTemplate == "" { HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) + return + } + customAppData := func() map[string]interface{} { + if c.App.ExtraInfo == nil { + return nil + } + return map[string]interface{}{ + "ExtraInfo": c.App.ExtraInfo, + } } + HelpPrinterCustom(c.App.Writer, c.App.CustomAppHelpTemplate, c.App, customAppData()) return nil } @@ -178,7 +180,7 @@ func ShowCommandHelp(ctx *Context, command string) error { for _, c := range ctx.App.Commands { if c.HasName(command) { if c.CustomHelpTemplate != "" { - HelpPrinter(ctx.App.Writer, c.CustomHelpTemplate, c) + HelpPrinterCustom(ctx.App.Writer, c.CustomHelpTemplate, c, nil) } else { HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) } diff --git a/help_test.go b/help_test.go index 7c15400..78d8973 100644 --- a/help_test.go +++ b/help_test.go @@ -3,6 +3,8 @@ package cli import ( "bytes" "flag" + "fmt" + "runtime" "strings" "testing" ) @@ -256,6 +258,49 @@ func TestShowSubcommandHelp_CommandAliases(t *testing.T) { } } +func TestShowCommandHelp_Customtemplate(t *testing.T) { + app := &App{ + Commands: []Command{ + { + Name: "frobbly", + Action: func(ctx *Context) error { + return nil + }, + HelpName: "foo frobbly", + CustomHelpTemplate: `NAME: + {{.HelpName}} - {{.Usage}} + +USAGE: + {{.HelpName}} [FLAGS] TARGET [TARGET ...] + +FLAGS: + {{range .VisibleFlags}}{{.}} + {{end}} +EXAMPLES: + 1. Frobbly runs with this param locally. + $ {{.HelpName}} wobbly +`, + }, + }, + } + + output := &bytes.Buffer{} + app.Writer = output + app.Run([]string{"foo", "help", "frobbly"}) + + if strings.Contains(output.String(), "2. Frobbly runs without this param locally.") { + t.Errorf("expected output to exclude \"2. Frobbly runs without this param locally.\"; got: %q", output.String()) + } + + if !strings.Contains(output.String(), "1. Frobbly runs with this param locally.") { + t.Errorf("expected output to include \"1. Frobbly runs with this param locally.\"; got: %q", output.String()) + } + + if !strings.Contains(output.String(), "$ foo frobbly wobbly") { + t.Errorf("expected output to include \"$ foo frobbly wobbly\"; got: %q", output.String()) + } +} + func TestShowAppHelp_HiddenCommand(t *testing.T) { app := &App{ Commands: []Command{ @@ -287,3 +332,78 @@ func TestShowAppHelp_HiddenCommand(t *testing.T) { t.Errorf("expected output to include \"frobbly\"; got: %q", output.String()) } } + +func TestShowAppHelp_CustomAppTemplate(t *testing.T) { + app := &App{ + Commands: []Command{ + { + Name: "frobbly", + Action: func(ctx *Context) error { + return nil + }, + }, + { + Name: "secretfrob", + Hidden: true, + Action: func(ctx *Context) error { + return nil + }, + }, + }, + ExtraInfo: func() map[string]string { + platform := fmt.Sprintf("OS: %s | Arch: %s", runtime.GOOS, runtime.GOARCH) + goruntime := fmt.Sprintf("Version: %s | CPUs: %d", runtime.Version(), runtime.NumCPU()) + return map[string]string{ + "PLATFORM": platform, + "RUNTIME": goruntime, + } + }, + CustomAppHelpTemplate: `NAME: + {{.Name}} - {{.Usage}} + +USAGE: + {{.Name}} {{if .VisibleFlags}}[FLAGS] {{end}}COMMAND{{if .VisibleFlags}} [COMMAND FLAGS | -h]{{end}} [ARGUMENTS...] + +COMMANDS: + {{range .VisibleCommands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} + {{end}}{{if .VisibleFlags}} +GLOBAL FLAGS: + {{range .VisibleFlags}}{{.}} + {{end}}{{end}} +VERSION: + 2.0.0 +{{"\n"}}{{range $key, $value := ExtraInfo}} +{{$key}}: + {{$value}} +{{end}}`, + } + + output := &bytes.Buffer{} + app.Writer = output + app.Run([]string{"app", "--help"}) + + if strings.Contains(output.String(), "secretfrob") { + t.Errorf("expected output to exclude \"secretfrob\"; got: %q", output.String()) + } + + if !strings.Contains(output.String(), "frobbly") { + t.Errorf("expected output to include \"frobbly\"; got: %q", output.String()) + } + + if !strings.Contains(output.String(), "PLATFORM:") || + !strings.Contains(output.String(), "OS:") || + !strings.Contains(output.String(), "Arch:") { + t.Errorf("expected output to include \"PLATFORM:, OS: and Arch:\"; got: %q", output.String()) + } + + if !strings.Contains(output.String(), "RUNTIME:") || + !strings.Contains(output.String(), "Version:") || + !strings.Contains(output.String(), "CPUs:") { + t.Errorf("expected output to include \"RUNTIME:, Version: and CPUs:\"; got: %q", output.String()) + } + + if !strings.Contains(output.String(), "VERSION:") || + !strings.Contains(output.String(), "2.0.0") { + t.Errorf("expected output to include \"VERSION:, 2.0.0\"; got: %q", output.String()) + } +} From 291122b8f0ad73134ddee3cdc27b5164e4480ab3 Mon Sep 17 00:00:00 2001 From: "Joe Richey joerichey@google.com" Date: Fri, 5 May 2017 15:01:44 -0700 Subject: [PATCH 27/30] Subcommand OnUsageError should be passed to app Not all of the Command components were being passed to the created App in the startApp function. --- command.go | 1 + command_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/command.go b/command.go index a83495e..bb0733c 100644 --- a/command.go +++ b/command.go @@ -291,6 +291,7 @@ func (c Command) startApp(ctx *Context) error { } else { app.Action = helpSubcommand.Action } + app.OnUsageError = c.OnUsageError for index, cc := range app.Commands { app.Commands[index].commandNamePath = []string{c.Name, cc.Name} diff --git a/command_test.go b/command_test.go index 10fff2d..4ad994c 100644 --- a/command_test.go +++ b/command_test.go @@ -178,6 +178,38 @@ func TestCommand_OnUsageError_WithWrongFlagValue(t *testing.T) { } } +func TestCommand_OnUsageError_WithSubcommand(t *testing.T) { + app := NewApp() + app.Commands = []Command{ + { + Name: "bar", + Subcommands: []Command{ + { + Name: "baz", + }, + }, + Flags: []Flag{ + IntFlag{Name: "flag"}, + }, + OnUsageError: func(c *Context, err error, _ bool) error { + if !strings.HasPrefix(err.Error(), "invalid value \"wrong\"") { + t.Errorf("Expect an invalid value error, but got \"%v\"", err) + } + return errors.New("intercepted: " + err.Error()) + }, + }, + } + + err := app.Run([]string{"foo", "bar", "--flag=wrong"}) + if err == nil { + t.Fatalf("expected to receive error from Run, got none") + } + + if !strings.HasPrefix(err.Error(), "intercepted: invalid value") { + t.Errorf("Expect an intercepted error, but got \"%v\"", err) + } +} + func TestCommand_Run_SubcommandsCanUseErrWriter(t *testing.T) { app := NewApp() app.ErrWriter = ioutil.Discard From c64d74a5d989d144e33854e303f20981f9e8f049 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Mon, 24 Apr 2017 14:54:35 -0700 Subject: [PATCH 28/30] Do not double print errors from Before() They should be handled by HandleExitCoder() as `After()` errors are. --- app.go | 1 - command.go | 2 -- 2 files changed, 3 deletions(-) diff --git a/app.go b/app.go index 26a4d4d..51fc45d 100644 --- a/app.go +++ b/app.go @@ -240,7 +240,6 @@ func (a *App) Run(arguments []string) (err error) { if a.Before != nil { beforeErr := a.Before(context) if beforeErr != nil { - fmt.Fprintf(a.Writer, "%v\n\n", beforeErr) ShowAppHelp(context) HandleExitCoder(beforeErr) err = beforeErr diff --git a/command.go b/command.go index bb0733c..23de294 100644 --- a/command.go +++ b/command.go @@ -197,8 +197,6 @@ func (c Command) Run(ctx *Context) (err error) { if c.Before != nil { err = c.Before(context) if err != nil { - fmt.Fprintln(context.App.Writer, err) - fmt.Fprintln(context.App.Writer) ShowCommandHelp(context, c.Name) HandleExitCoder(err) return err From 48db8e24356a42996e7d6b0b62c55e0df1d4492e Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Sun, 29 Jan 2017 21:05:21 -0800 Subject: [PATCH 29/30] Display UsageText on Commands and Subcommands If it is set, otherwise build the usage. Fixes #592 --- help.go | 4 ++-- help_test.go | 45 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/help.go b/help.go index 216ff78..57ec98d 100644 --- a/help.go +++ b/help.go @@ -47,7 +47,7 @@ var CommandHelpTemplate = `NAME: {{.HelpName}} - {{.Usage}} USAGE: - {{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{if .Category}} + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} CATEGORY: {{.Category}}{{end}}{{if .Description}} @@ -67,7 +67,7 @@ var SubcommandHelpTemplate = `NAME: {{.HelpName}} - {{if .Description}}{{.Description}}{{else}}{{.Usage}}{{end}} USAGE: - {{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{end}}{{range .VisibleCommands}} diff --git a/help_test.go b/help_test.go index 78d8973..70b6300 100644 --- a/help_test.go +++ b/help_test.go @@ -283,7 +283,6 @@ EXAMPLES: }, }, } - output := &bytes.Buffer{} app.Writer = output app.Run([]string{"foo", "help", "frobbly"}) @@ -301,6 +300,50 @@ EXAMPLES: } } +func TestShowSubcommandHelp_CommandUsageText(t *testing.T) { + app := &App{ + Commands: []Command{ + { + Name: "frobbly", + UsageText: "this is usage text", + }, + }, + } + + output := &bytes.Buffer{} + app.Writer = output + + app.Run([]string{"foo", "frobbly", "--help"}) + + if !strings.Contains(output.String(), "this is usage text") { + t.Errorf("expected output to include usage text; got: %q", output.String()) + } +} + +func TestShowSubcommandHelp_SubcommandUsageText(t *testing.T) { + app := &App{ + Commands: []Command{ + { + Name: "frobbly", + Subcommands: []Command{ + { + Name: "bobbly", + UsageText: "this is usage text", + }, + }, + }, + }, + } + + output := &bytes.Buffer{} + app.Writer = output + app.Run([]string{"foo", "frobbly", "bobbly", "--help"}) + + if !strings.Contains(output.String(), "this is usage text") { + t.Errorf("expected output to include usage text; got: %q", output.String()) + } +} + func TestShowAppHelp_HiddenCommand(t *testing.T) { app := &App{ Commands: []Command{ From a6dd54e94bbb0c70ec72a144d24e24e2247b3b2e Mon Sep 17 00:00:00 2001 From: Nicolas Gailly Date: Thu, 6 Jul 2017 16:20:09 +0200 Subject: [PATCH 30/30] make the basic example build-able. --- cli.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cli.go b/cli.go index 74fd101..90c07eb 100644 --- a/cli.go +++ b/cli.go @@ -12,6 +12,7 @@ // app.Usage = "say a greeting" // app.Action = func(c *cli.Context) error { // println("Greetings") +// return nil // } // // app.Run(os.Args)