From e2a8dfe314fa429e04a63152ee3c97806214095c Mon Sep 17 00:00:00 2001 From: Robert Liebowitz Date: Sun, 27 Jan 2019 10:38:23 -0500 Subject: [PATCH 1/5] Decouple help printer for custom templates --- help.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/help.go b/help.go index 7a4ef69259..06c526f7ef 100644 --- a/help.go +++ b/help.go @@ -187,7 +187,7 @@ func ShowCommandHelp(ctx *Context, command string) error { for _, c := range ctx.App.Commands { if c.HasName(command) { if c.CustomHelpTemplate != "" { - HelpPrinterCustom(ctx.App.Writer, c.CustomHelpTemplate, c, nil) + HelpPrinter(ctx.App.Writer, c.CustomHelpTemplate, c) } else { HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) } @@ -261,7 +261,7 @@ func printHelpCustom(out io.Writer, templ string, data interface{}, customFunc m } func printHelp(out io.Writer, templ string, data interface{}) { - printHelpCustom(out, templ, data, nil) + HelpPrinterCustom(out, templ, data, nil) } func checkVersion(c *Context) bool { From b52cca036aadab37c86db59b0e04b1a79d7ac380 Mon Sep 17 00:00:00 2001 From: Robert Liebowitz Date: Wed, 16 Oct 2019 07:24:21 -0400 Subject: [PATCH 2/5] Simplify duplicated function call --- help.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/help.go b/help.go index 06c526f7ef..01c19aaf4a 100644 --- a/help.go +++ b/help.go @@ -186,11 +186,13 @@ 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) - } else { - HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) + templ := c.CustomHelpTemplate + if templ == "" { + templ = CommandHelpTemplate } + + HelpPrinter(ctx.App.Writer, templ, c) + return nil } } From a14194aa33517e16dd5884625da8723c5d77acc8 Mon Sep 17 00:00:00 2001 From: Robert Liebowitz Date: Wed, 16 Oct 2019 07:57:33 -0400 Subject: [PATCH 3/5] Improve documentation for HelpPrinter/HelpPrinterCustom --- help.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/help.go b/help.go index 01c19aaf4a..d5f1c87d9f 100644 --- a/help.go +++ b/help.go @@ -47,13 +47,15 @@ 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{}) +// HelpPrinter is a function that writes the help output. If not set explicitly, +// this calls HelpPrinterCustom using only the default template functions. var HelpPrinter helpPrinter = printHelp -// HelpPrinterCustom is same as HelpPrinter but -// takes a custom function for template function map. +// HelpPrinterCustom is a function that writes the help output. If not set +// explicitly, a default is used. +// +// The customFuncs map will be combined with a default template.FuncMap to +// allow using arbitrary functions in template rendering. var HelpPrinterCustom helpPrinterCustom = printHelpCustom // VersionPrinter prints the version for the App @@ -240,11 +242,11 @@ func ShowCommandCompletions(ctx *Context, command string) { } -func printHelpCustom(out io.Writer, templ string, data interface{}, customFunc map[string]interface{}) { +func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) { funcMap := template.FuncMap{ "join": strings.Join, } - for key, value := range customFunc { + for key, value := range customFuncs { funcMap[key] = value } From bab428af09e0373e1deda9dec24d30cc4c5c7026 Mon Sep 17 00:00:00 2001 From: Robert Liebowitz Date: Thu, 17 Oct 2019 05:12:17 -0400 Subject: [PATCH 4/5] Ensure command help always prints with overridden print functions --- help_test.go | 174 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/help_test.go b/help_test.go index b13eef4c03..870b32ec05 100644 --- a/help_test.go +++ b/help_test.go @@ -4,6 +4,7 @@ import ( "bytes" "flag" "fmt" + "io" "runtime" "strings" "testing" @@ -210,6 +211,179 @@ func TestShowAppHelp_CommandAliases(t *testing.T) { } } +func TestShowCommandHelp_HelpPrinter(t *testing.T) { + doublecho := func(text string) string { + return text + " " + text + } + + tests := []struct { + name string + template string + printer helpPrinter + command string + wantTemplate string + wantOutput string + }{ + { + name: "no-command", + template: "", + printer: func(w io.Writer, templ string, data interface{}) { + fmt.Fprint(w, "yo") + }, + command: "", + wantTemplate: SubcommandHelpTemplate, + wantOutput: "yo", + }, + { + name: "standard-command", + template: "", + printer: func(w io.Writer, templ string, data interface{}) { + fmt.Fprint(w, "yo") + }, + command: "my-command", + wantTemplate: CommandHelpTemplate, + wantOutput: "yo", + }, + { + name: "custom-template-command", + template: "{{doublecho .Name}}", + printer: func(w io.Writer, templ string, data interface{}) { + // Pass a custom function to ensure it gets used + fm := map[string]interface{}{"doublecho": doublecho} + HelpPrinterCustom(w, templ, data, fm) + }, + command: "my-command", + wantTemplate: "{{doublecho .Name}}", + wantOutput: "my-command my-command", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func(old helpPrinter) { + HelpPrinter = old + }(HelpPrinter) + HelpPrinter = func(w io.Writer, templ string, data interface{}) { + if templ != tt.wantTemplate { + t.Errorf("want template:\n%s\ngot template:\n%s", tt.wantTemplate, templ) + } + + tt.printer(w, templ, data) + } + + var buf bytes.Buffer + app := &App{ + Name: "my-app", + Writer: &buf, + Commands: []Command{ + { + Name: "my-command", + CustomHelpTemplate: tt.template, + }, + }, + } + + err := app.Run([]string{"my-app", "help", tt.command}) + if err != nil { + t.Fatal(err) + } + + got := buf.String() + if got != tt.wantOutput { + t.Errorf("want output %q, got %q", tt.wantOutput, got) + } + }) + } +} +func TestShowCommandHelp_HelpPrinterCustom(t *testing.T) { + doublecho := func(text string) string { + return text + " " + text + } + + tests := []struct { + name string + template string + printer helpPrinterCustom + command string + wantTemplate string + wantOutput string + }{ + { + name: "no-command", + template: "", + printer: func(w io.Writer, templ string, data interface{}, fm map[string]interface{}) { + fmt.Fprint(w, "yo") + }, + command: "", + wantTemplate: SubcommandHelpTemplate, + wantOutput: "yo", + }, + { + name: "standard-command", + template: "", + printer: func(w io.Writer, templ string, data interface{}, fm map[string]interface{}) { + fmt.Fprint(w, "yo") + }, + command: "my-command", + wantTemplate: CommandHelpTemplate, + wantOutput: "yo", + }, + { + name: "custom-template-command", + template: "{{doublecho .Name}}", + printer: func(w io.Writer, templ string, data interface{}, _ map[string]interface{}) { + // Pass a custom function to ensure it gets used + fm := map[string]interface{}{"doublecho": doublecho} + printHelpCustom(w, templ, data, fm) + }, + command: "my-command", + wantTemplate: "{{doublecho .Name}}", + wantOutput: "my-command my-command", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func(old helpPrinterCustom) { + HelpPrinterCustom = old + }(HelpPrinterCustom) + HelpPrinterCustom = func(w io.Writer, templ string, data interface{}, fm map[string]interface{}) { + if fm != nil { + t.Error("unexpected function map passed") + } + + if templ != tt.wantTemplate { + t.Errorf("want template:\n%s\ngot template:\n%s", tt.wantTemplate, templ) + } + + tt.printer(w, templ, data, fm) + } + + var buf bytes.Buffer + app := &App{ + Name: "my-app", + Writer: &buf, + Commands: []Command{ + { + Name: "my-command", + CustomHelpTemplate: tt.template, + }, + }, + } + + err := app.Run([]string{"my-app", "help", tt.command}) + if err != nil { + t.Fatal(err) + } + + got := buf.String() + if got != tt.wantOutput { + t.Errorf("want output %q, got %q", tt.wantOutput, got) + } + }) + } +} + func TestShowCommandHelp_CommandAliases(t *testing.T) { app := &App{ Commands: []Command{ From 9ab178d17445f6737817c5265a72a630391fab68 Mon Sep 17 00:00:00 2001 From: Robert Liebowitz Date: Thu, 17 Oct 2019 05:50:38 -0400 Subject: [PATCH 5/5] Make app help behavior consistent with commands --- help.go | 37 +++++++++----- help_test.go | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 13 deletions(-) diff --git a/help.go b/help.go index d5f1c87d9f..2280e338ef 100644 --- a/help.go +++ b/help.go @@ -49,13 +49,16 @@ type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customF // HelpPrinter is a function that writes the help output. If not set explicitly, // this calls HelpPrinterCustom using only the default template functions. +// +// If custom logic for printing help is required, this function can be +// overridden. If the ExtraInfo field is defined on an App, this function +// should not be modified, as HelpPrinterCustom will be used directly in order +// to capture the extra information. var HelpPrinter helpPrinter = printHelp -// HelpPrinterCustom is a function that writes the help output. If not set -// explicitly, a default is used. -// -// The customFuncs map will be combined with a default template.FuncMap to -// allow using arbitrary functions in template rendering. +// HelpPrinterCustom is a function that writes the help output. It is used as +// the default implementation of HelpPrinter, and may be called directly if +// the ExtraInfo field is set on an App. var HelpPrinterCustom helpPrinterCustom = printHelpCustom // VersionPrinter prints the version for the App @@ -68,20 +71,24 @@ func ShowAppHelpAndExit(c *Context, exitCode int) { } // ShowAppHelp is an action that displays the help. -func ShowAppHelp(c *Context) (err error) { - if c.App.CustomAppHelpTemplate == "" { - HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) - return +func ShowAppHelp(c *Context) error { + template := c.App.CustomAppHelpTemplate + if template == "" { + template = AppHelpTemplate + } + + if c.App.ExtraInfo == nil { + HelpPrinter(c.App.Writer, template, c.App) + return nil } + 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()) + HelpPrinterCustom(c.App.Writer, template, c.App, customAppData()) + return nil } @@ -242,6 +249,10 @@ func ShowCommandCompletions(ctx *Context, command string) { } +// printHelpCustom is the default implementation of HelpPrinterCustom. +// +// The customFuncs map will be combined with a default template.FuncMap to +// allow using arbitrary functions in template rendering. func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) { funcMap := template.FuncMap{ "join": strings.Join, diff --git a/help_test.go b/help_test.go index 870b32ec05..e903494fbb 100644 --- a/help_test.go +++ b/help_test.go @@ -295,6 +295,7 @@ func TestShowCommandHelp_HelpPrinter(t *testing.T) { }) } } + func TestShowCommandHelp_HelpPrinterCustom(t *testing.T) { doublecho := func(text string) string { return text + " " + text @@ -550,6 +551,144 @@ func TestShowAppHelp_HiddenCommand(t *testing.T) { } } +func TestShowAppHelp_HelpPrinter(t *testing.T) { + doublecho := func(text string) string { + return text + " " + text + } + + tests := []struct { + name string + template string + printer helpPrinter + wantTemplate string + wantOutput string + }{ + { + name: "standard-command", + template: "", + printer: func(w io.Writer, templ string, data interface{}) { + fmt.Fprint(w, "yo") + }, + wantTemplate: AppHelpTemplate, + wantOutput: "yo", + }, + { + name: "custom-template-command", + template: "{{doublecho .Name}}", + printer: func(w io.Writer, templ string, data interface{}) { + // Pass a custom function to ensure it gets used + fm := map[string]interface{}{"doublecho": doublecho} + printHelpCustom(w, templ, data, fm) + }, + wantTemplate: "{{doublecho .Name}}", + wantOutput: "my-app my-app", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func(old helpPrinter) { + HelpPrinter = old + }(HelpPrinter) + HelpPrinter = func(w io.Writer, templ string, data interface{}) { + if templ != tt.wantTemplate { + t.Errorf("want template:\n%s\ngot template:\n%s", tt.wantTemplate, templ) + } + + tt.printer(w, templ, data) + } + + var buf bytes.Buffer + app := &App{ + Name: "my-app", + Writer: &buf, + CustomAppHelpTemplate: tt.template, + } + + err := app.Run([]string{"my-app", "help"}) + if err != nil { + t.Fatal(err) + } + + got := buf.String() + if got != tt.wantOutput { + t.Errorf("want output %q, got %q", tt.wantOutput, got) + } + }) + } +} + +func TestShowAppHelp_HelpPrinterCustom(t *testing.T) { + doublecho := func(text string) string { + return text + " " + text + } + + tests := []struct { + name string + template string + printer helpPrinterCustom + wantTemplate string + wantOutput string + }{ + { + name: "standard-command", + template: "", + printer: func(w io.Writer, templ string, data interface{}, fm map[string]interface{}) { + fmt.Fprint(w, "yo") + }, + wantTemplate: AppHelpTemplate, + wantOutput: "yo", + }, + { + name: "custom-template-command", + template: "{{doublecho .Name}}", + printer: func(w io.Writer, templ string, data interface{}, _ map[string]interface{}) { + // Pass a custom function to ensure it gets used + fm := map[string]interface{}{"doublecho": doublecho} + printHelpCustom(w, templ, data, fm) + }, + wantTemplate: "{{doublecho .Name}}", + wantOutput: "my-app my-app", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func(old helpPrinterCustom) { + HelpPrinterCustom = old + }(HelpPrinterCustom) + HelpPrinterCustom = func(w io.Writer, templ string, data interface{}, fm map[string]interface{}) { + if fm != nil { + t.Error("unexpected function map passed") + } + + if templ != tt.wantTemplate { + t.Errorf("want template:\n%s\ngot template:\n%s", tt.wantTemplate, templ) + } + + tt.printer(w, templ, data, fm) + } + + var buf bytes.Buffer + app := &App{ + Name: "my-app", + Writer: &buf, + CustomAppHelpTemplate: tt.template, + } + + err := app.Run([]string{"my-app", "help"}) + if err != nil { + t.Fatal(err) + } + + got := buf.String() + if got != tt.wantOutput { + t.Errorf("want output %q, got %q", tt.wantOutput, got) + } + }) + } +} + func TestShowAppHelp_CustomAppTemplate(t *testing.T) { app := &App{ Commands: []Command{