From f7d6a07f2d060ba198dc9c00a48fa6dbe03fb7b5 Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Fri, 25 Nov 2016 00:16:48 -0800 Subject: [PATCH 1/3] 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 2/3] 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 3/3] 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()) + } +}