Skip to content

Commit a6313e2

Browse files
committed
Fail early when custom templates are used with urfave_cli_no_template
With urfave_cli_no_template tag, custom templates are not supported, but using them results in a working binary which just prints an error instead of help. Make such misuse fail as early as possible: - with the tag, make RootCommandHelpTemplate, CommandHelpTemplate, SubcommandHelpTemplate and FishCompletionTemplate constants, so an attempt to modify them results in a compile error; - with the tag, return an error from Run if CustomRootCommandHelpTemplate or CustomHelpTemplate is set for any command. The only remaining case is calling DefaultPrintHelpCustom with a custom template, which still prints an error instead of help. To keep the template text in one place, the default values are now defined as unexported constants, which the exported variables (or, with the tag, constants) are initialized from. Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
1 parent 6d1b921 commit a6313e2

14 files changed

Lines changed: 222 additions & 269 deletions

‎command_run.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context
111111
}
112112

113113
if cmd.parent == nil {
114+
if err := checkNoCustomTemplates(cmd); err != nil {
115+
return ctx, err
116+
}
114117
if cmd.ReadArgsFromStdin {
115118
if args, err := cmd.parseArgsFromStdin(); err != nil {
116119
return ctx, err

‎docs/v3/binary-size.md‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,17 @@ For larger programs, this can noticeably increase the binary size.
164164

165165
With the `urfave_cli_no_template` build tag, the default help and fish
166166
completion are rendered without `text/template`, producing the same output.
167-
Custom templates (`CustomRootCommandHelpTemplate`, `CustomHelpTemplate`,
168-
modified `RootCommandHelpTemplate` etc.) are not supported in this mode:
169-
instead of help, an error is printed to `ErrWriter`, and `ToFishCompletion`
170-
returns an error. Custom template functions other than `wrap` and `wrapAt`
171-
are ignored.
167+
Custom templates are not supported in this mode:
168+
169+
- `RootCommandHelpTemplate`, `CommandHelpTemplate`, `SubcommandHelpTemplate`
170+
and `FishCompletionTemplate` are constants, so modifying them results in
171+
a compile error;
172+
- if `CustomRootCommandHelpTemplate` or `CustomHelpTemplate` is set for any
173+
command, `Run` returns an error;
174+
- if `DefaultPrintHelpCustom` is called with a custom template, an error is
175+
printed to `ErrWriter` instead of help.
176+
177+
Custom template functions other than `wrap` and `wrapAt` are ignored.
172178

173179
```sh-session
174180
go build -tags urfave_cli_no_template -o myapp ./cmd/myapp

‎examples_template_test.go‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,44 @@ func ExampleCommand_Suggest() {
3737
//
3838
// (this space intentionally left blank)
3939
}
40+
41+
func ExampleCommand_Suggest_command() {
42+
cmd := &cli.Command{
43+
ErrWriter: os.Stdout,
44+
Name: "greet",
45+
Flags: []cli.Flag{
46+
&cli.StringFlag{Name: "name", Value: "squirrel", Usage: "a name to say"},
47+
},
48+
Action: func(_ context.Context, cmd *cli.Command) error {
49+
fmt.Printf("Hello %v\n", cmd.String("name"))
50+
return nil
51+
},
52+
Commands: []*cli.Command{
53+
{
54+
Name: "neighbors",
55+
HideHelp: true,
56+
HideHelpCommand: true,
57+
Suggest: true,
58+
CustomHelpTemplate: "(this space intentionally left blank)\n",
59+
Flags: []cli.Flag{
60+
&cli.BoolFlag{Name: "smiling"},
61+
},
62+
Action: func(_ context.Context, cmd *cli.Command) error {
63+
if cmd.Bool("smiling") {
64+
fmt.Println("😀")
65+
}
66+
fmt.Println("Hello, neighbors")
67+
return nil
68+
},
69+
},
70+
},
71+
}
72+
73+
if cmd.Run(context.Background(), []string{"greet", "neighbors", "--sliming"}) == nil {
74+
fmt.Println("Expected error")
75+
}
76+
// Output:
77+
// Incorrect Usage: flag provided but not defined: -sliming
78+
//
79+
// Did you mean "--smiling"?
80+
}

‎examples_test.go‎

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -539,44 +539,3 @@ func ExampleBoolWithInverseFlag() {
539539
// no-env is set
540540
// flags: 2
541541
}
542-
543-
func ExampleCommand_Suggest_command() {
544-
cmd := &cli.Command{
545-
ErrWriter: os.Stdout,
546-
Name: "greet",
547-
Flags: []cli.Flag{
548-
&cli.StringFlag{Name: "name", Value: "squirrel", Usage: "a name to say"},
549-
},
550-
Action: func(_ context.Context, cmd *cli.Command) error {
551-
fmt.Printf("Hello %v\n", cmd.String("name"))
552-
return nil
553-
},
554-
Commands: []*cli.Command{
555-
{
556-
Name: "neighbors",
557-
HideHelp: true,
558-
HideHelpCommand: true,
559-
Suggest: true,
560-
CustomHelpTemplate: "(this space intentionally left blank)\n",
561-
Flags: []cli.Flag{
562-
&cli.BoolFlag{Name: "smiling"},
563-
},
564-
Action: func(_ context.Context, cmd *cli.Command) error {
565-
if cmd.Bool("smiling") {
566-
fmt.Println("😀")
567-
}
568-
fmt.Println("Hello, neighbors")
569-
return nil
570-
},
571-
},
572-
},
573-
}
574-
575-
if cmd.Run(context.Background(), []string{"greet", "neighbors", "--sliming"}) == nil {
576-
fmt.Println("Expected error")
577-
}
578-
// Output:
579-
// Incorrect Usage: flag provided but not defined: -sliming
580-
//
581-
// Did you mean "--smiling"?
582-
}

‎fish_template_test.go‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//go:build !urfave_cli_no_template
2+
3+
package cli
4+
5+
import (
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestFishCompletionTemplateError(t *testing.T) {
12+
defer func(old string) { FishCompletionTemplate = old }(FishCompletionTemplate)
13+
FishCompletionTemplate = "{{something"
14+
15+
_, err := buildExtendedTestCommand().ToFishCompletion()
16+
assert.Error(t, err)
17+
}

‎fish_test.go‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,6 @@ func TestFishCompletion(t *testing.T) {
2323
})
2424
cmd.setupCommandGraph()
2525

26-
oldTemplate := FishCompletionTemplate
27-
defer func() { FishCompletionTemplate = oldTemplate }()
28-
FishCompletionTemplate = "{{something"
29-
30-
// test error case
31-
_, err1 := cmd.ToFishCompletion()
32-
assert.Error(t, err1)
33-
34-
// reset the template
35-
FishCompletionTemplate = oldTemplate
3626
// When
3727
res, err := cmd.ToFishCompletion()
3828

‎godoc-current.txt‎

Lines changed: 5 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -61,24 +61,7 @@ var ArgsUsageCommandHelp = "[command]"
6161
ArgsUsageCommandHelp is a short description of the arguments of the help
6262
command
6363

64-
var CommandHelpTemplate = `NAME:
65-
{{template "helpNameTemplate" .}}
66-
67-
USAGE:
68-
{{template "usageTemplate" .}}{{if .Category}}
69-
70-
CATEGORY:
71-
{{.Category}}{{end}}{{if .Description}}
72-
73-
DESCRIPTION:
74-
{{template "descriptionTemplate" .}}{{end}}{{if .VisibleFlagCategories}}
75-
76-
OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}}
77-
78-
OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}{{if .VisiblePersistentFlags}}
79-
80-
GLOBAL OPTIONS:{{template "visiblePersistentFlagTemplate" .}}{{end}}
81-
`
64+
var CommandHelpTemplate = commandHelpTemplate
8265
CommandHelpTemplate is the text template for the command help topic. cli.go
8366
uses text/template to render templates. You can render custom help text by
8467
setting this variable.
@@ -92,49 +75,16 @@ var ErrWriter io.Writer = os.Stderr
9275
ErrWriter is used to write errors to the user. This can be anything
9376
implementing the io.Writer interface and defaults to os.Stderr.
9477

95-
var FishCompletionTemplate = `# {{ .Command.Name }} fish shell completion
78+
var FishCompletionTemplate = fishCompletionTemplate
79+
FishCompletionTemplate is the text template for fish shell completion.
9680

97-
function __fish_{{ .Command.Name }}_no_subcommand --description 'Test if there has been any subcommand yet'
98-
for i in (commandline -opc)
99-
if contains -- $i{{ range $v := .AllCommands }} {{ $v }}{{ end }}
100-
return 1
101-
end
102-
end
103-
return 0
104-
end
105-
106-
{{ range $v := .Completions }}{{ $v }}
107-
{{ end }}`
10881
var NewStringMap = NewMapBase[string, StringConfig, stringValue]
10982
var NewStringSlice = NewSliceBase[string, StringConfig, stringValue]
11083
var OsExiter = os.Exit
11184
OsExiter is the function used when the app exits. If not set defaults to
11285
os.Exit.
11386

114-
var RootCommandHelpTemplate = `NAME:
115-
{{template "helpNameTemplate" .}}
116-
117-
USAGE:
118-
{{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.FullName}} {{if .VisibleFlags}}[global options]{{end}}{{if .VisibleCommands}} [command [command options]]{{end}}{{if .ArgsUsage}} {{.ArgsUsage}}{{else}}{{if .Arguments}} [arguments...]{{end}}{{end}}{{end}}{{if .Version}}{{if not .HideVersion}}
119-
120-
VERSION:
121-
{{.Version}}{{end}}{{end}}{{if .Description}}
122-
123-
DESCRIPTION:
124-
{{template "descriptionTemplate" .}}{{end}}
125-
{{- if len .Authors}}
126-
127-
AUTHOR{{template "authorsTemplate" .}}{{end}}{{if .VisibleCommands}}
128-
129-
COMMANDS:{{template "visibleCommandCategoryTemplate" .}}{{end}}{{if .VisibleFlagCategories}}
130-
131-
GLOBAL OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}}
132-
133-
GLOBAL OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}{{if .Copyright}}
134-
135-
COPYRIGHT:
136-
{{template "copyrightTemplate" .}}{{end}}
137-
`
87+
var RootCommandHelpTemplate = rootCommandHelpTemplate
13888
RootCommandHelpTemplate is the text template for the Default help topic.
13989
cli.go uses text/template to render templates. You can render custom help
14090
text by setting this variable.
@@ -154,26 +104,7 @@ var ShowRootCommandHelp = DefaultShowRootCommandHelp
154104
var ShowSubcommandHelp = DefaultShowSubcommandHelp
155105
ShowSubcommandHelp prints help for the given subcommand
156106

157-
var SubcommandHelpTemplate = `NAME:
158-
{{template "helpNameTemplate" .}}
159-
160-
USAGE:
161-
{{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.FullName}}{{if .VisibleCommands}} [command [command options]]{{end}}{{if .ArgsUsage}} {{.ArgsUsage}}{{else}}{{if .Arguments}} [arguments...]{{end}}{{end}}{{end}}{{if .Category}}
162-
163-
CATEGORY:
164-
{{.Category}}{{end}}{{if .Description}}
165-
166-
DESCRIPTION:
167-
{{template "descriptionTemplate" .}}{{end}}{{if .VisibleCommands}}
168-
169-
COMMANDS:{{template "visibleCommandTemplate" .}}{{end}}{{if .VisibleFlagCategories}}
170-
171-
OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}}
172-
173-
OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}{{if .VisiblePersistentFlags}}
174-
175-
GLOBAL OPTIONS:{{template "visiblePersistentFlagTemplate" .}}{{end}}
176-
`
107+
var SubcommandHelpTemplate = subcommandHelpTemplate
177108
SubcommandHelpTemplate is the text template for the subcommand help topic.
178109
cli.go uses text/template to render templates. You can render custom help
179110
text by setting this variable.

‎help_notemplate.go‎

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@ import (
1414
// templatesSupported is true unless built with the urfave_cli_no_template tag.
1515
const templatesSupported = false
1616

17-
// Default template values, used to recognize which help is being requested.
18-
// Package-level variable initialization happens before any init function,
19-
// so these are the values before any modifications by the user.
20-
var (
21-
defaultRootCommandHelpTemplate = RootCommandHelpTemplate
22-
defaultCommandHelpTemplate = CommandHelpTemplate
23-
defaultSubcommandHelpTemplate = SubcommandHelpTemplate
24-
defaultFishCompletionTemplate = FishCompletionTemplate
17+
// With urfave_cli_no_template tag, the default templates are constants,
18+
// so an attempt to modify them results in a compile error.
19+
const (
20+
// RootCommandHelpTemplate is the text template for the Default help topic.
21+
RootCommandHelpTemplate = rootCommandHelpTemplate
22+
// CommandHelpTemplate is the text template for the command help topic.
23+
CommandHelpTemplate = commandHelpTemplate
24+
// SubcommandHelpTemplate is the text template for the subcommand help topic.
25+
SubcommandHelpTemplate = subcommandHelpTemplate
26+
// FishCompletionTemplate is the text template for fish shell completion.
27+
FishCompletionTemplate = fishCompletionTemplate
2528
)
2629

2730
var errNoTemplate = errors.New("custom templates are not supported when built with urfave_cli_no_template tag")
@@ -62,11 +65,11 @@ func DefaultPrintHelpCustom(out io.Writer, templ string, data any, customFuncs m
6265
}
6366

6467
switch templ {
65-
case defaultRootCommandHelpTemplate:
68+
case RootCommandHelpTemplate:
6669
h.root()
67-
case defaultCommandHelpTemplate:
70+
case CommandHelpTemplate:
6871
h.command()
69-
case defaultSubcommandHelpTemplate:
72+
case SubcommandHelpTemplate:
7073
h.subcommand()
7174
default:
7275
reportNoTemplate(cmd)
@@ -79,6 +82,20 @@ func DefaultPrintHelpCustom(out io.Writer, templ string, data any, customFuncs m
7982
_ = w.Flush()
8083
}
8184

85+
// checkNoCustomTemplates returns an error if cmd or any of its subcommands
86+
// has a custom help template set, as these are not supported.
87+
func checkNoCustomTemplates(cmd *Command) error {
88+
if cmd.CustomRootCommandHelpTemplate != "" || cmd.CustomHelpTemplate != "" {
89+
return fmt.Errorf("command %q: %w", cmd.Name, errNoTemplate)
90+
}
91+
for _, sub := range cmd.Commands {
92+
if err := checkNoCustomTemplates(sub); err != nil {
93+
return err
94+
}
95+
}
96+
return nil
97+
}
98+
8299
// reportNoTemplate reports that a custom template can't be used,
83100
// so it's not silently ignored.
84101
func reportNoTemplate(cmd *Command) {
@@ -324,10 +341,6 @@ func printableValue(v any) string {
324341
}
325342

326343
func renderFishCompletion(w io.Writer, data *fishCommandCompletionTemplate) error {
327-
if FishCompletionTemplate != defaultFishCompletionTemplate {
328-
return errNoTemplate
329-
}
330-
331344
name := data.Command.Name
332345
var b strings.Builder
333346
b.WriteString("# " + name + " fish shell completion\n\n" +

0 commit comments

Comments
 (0)