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 01/14] 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 b9c535392027a122ff9776531002d450821426e5 Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 6 Apr 2017 15:11:23 +0200 Subject: [PATCH 02/14] 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 87fe13079e3f452a366ae8d7f991261f9324d630 Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Mon, 24 Apr 2017 15:19:34 -0700 Subject: [PATCH 03/14] 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 04/14] 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 05/14] 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 06/14] 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 07/14] 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 08/14] 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 09/14] 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 10/14] 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 11/14] 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) From 8164aa88ca94f405d0c73ddca4db3a57424bcf69 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Thu, 3 Aug 2017 15:07:56 -0400 Subject: [PATCH 12/14] Update travis config to only test on 1.8.x, and ensure we route to Ubuntu Trusty and a recent macOS image. --- .travis.yml | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 890e185..1ee4604 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,27 +1,18 @@ language: go - sudo: false +dist: trusty cache: directories: - node_modules -go: -- 1.6.x -- 1.7.x -- 1.8.x -- master +go: 1.8.x matrix: - allow_failures: - - go: master include: - - go: 1.6.x - os: osx - - go: 1.7.x - os: osx - go: 1.8.x os: osx + osx_image: xcode8.3 before_script: - go get github.com/urfave/gfmrun/... || true From 184ffb5e573ba0656a123462715d360faaa0c897 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Thu, 3 Aug 2017 15:38:19 -0400 Subject: [PATCH 13/14] Use `os` matrix instead of explicit matrix.include --- .travis.yml | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1ee4604..cf8d098 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,17 @@ language: go sudo: false dist: trusty +osx_image: xcode8.3 +go: 1.8.x + +os: +- linux +- osx cache: directories: - node_modules -go: 1.8.x - -matrix: - include: - - go: 1.8.x - os: osx - osx_image: xcode8.3 - before_script: - go get github.com/urfave/gfmrun/... || true - go get golang.org/x/tools/cmd/goimports From f59114b4100b20e092035199640f5fe8bce07daa Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Thu, 3 Aug 2017 22:59:38 -0400 Subject: [PATCH 14/14] Update os, image, golang, and python versions used in appveyor --- appveyor.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 698b188..1e1489c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,14 +1,16 @@ version: "{build}" -os: Windows Server 2012 R2 +os: Windows Server 2016 + +image: Visual Studio 2017 clone_folder: c:\gopath\src\github.com\urfave\cli environment: GOPATH: C:\gopath - GOVERSION: 1.6 - PYTHON: C:\Python27-x64 - PYTHON_VERSION: 2.7.x + GOVERSION: 1.8.x + PYTHON: C:\Python36-x64 + PYTHON_VERSION: 3.6.x PYTHON_ARCH: 64 install: