Skip to content

Commit f492217

Browse files
committed
chore: make usage template and writer configurable via exec options
Drop the package variables for configuring the usage render template and output writer. Instead, make this configurable via exec options avoid avoid package statefulness.
1 parent c9aad30 commit f492217

4 files changed

Lines changed: 62 additions & 51 deletions

File tree

execute.go

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,12 @@ import (
1212
"github.com/brandon1024/cmder/getopt"
1313
)
1414

15-
var (
16-
// ErrIllegalCommandConfiguration is an error returned when a [Command] provided to [Execute] is illegal.
17-
ErrIllegalCommandConfiguration = errors.New("cmder: illegal command configuration")
15+
// ErrIllegalCommandConfiguration is an error returned when a [Command] provided to [Execute] is illegal.
16+
var ErrIllegalCommandConfiguration = errors.New("cmder: illegal command configuration")
1817

19-
// ErrEnvironmentBindFailure is an error returned when [Execute] failed to update flag value from environment
20-
// variable (see [WithEnvironmentBinding]).
21-
ErrEnvironmentBindFailure = errors.New("cmder: failed to update flag from environment variable")
22-
)
18+
// ErrEnvironmentBindFailure is an error returned when [Execute] failed to update a flag value from environment
19+
// variables (see [WithEnvironmentBinding]).
20+
var ErrEnvironmentBindFailure = errors.New("cmder: failed to update flag from environment variable")
2321

2422
// Execute runs a [Command].
2523
//
@@ -73,8 +71,8 @@ var (
7371
// # Usage and Help Texts
7472
//
7573
// Whenever the user provides the '-h' or '--help' flag at the command line, [Execute] will display command usage and
76-
// exit. The format of the help text can be adjusted by configuring [UsageTemplate]. By default, usage information will
77-
// be written to stderr, but this can be adjusted by setting [UsageOutputWriter].
74+
// exit. The format of the help text can be adjusted with [WithUsageTemplate]. By default, usage information will
75+
// be written to stderr, but this can be adjusted by setting [WithUsageOutput].
7876
//
7977
// If a command's [Run] routine returns [ErrShowUsage] (or an error wrapping [ErrShowUsage]), [Execute] will render
8078
// help text and exit with status 2.
@@ -86,7 +84,9 @@ func Execute(ctx context.Context, cmd Command, op ...ExecuteOption) error {
8684

8785
// prepare executor options
8886
ops := &ExecuteOptions{
89-
args: os.Args[1:],
87+
args: os.Args[1:],
88+
usageTemplate: CobraUsageTemplate,
89+
usageWriter: os.Stderr,
9090
}
9191
for _, f := range op {
9292
f(ops)
@@ -100,14 +100,14 @@ func Execute(ctx context.Context, cmd Command, op ...ExecuteOption) error {
100100

101101
// if help was requested, display and exit
102102
if cmd, ok := helpRequested(stack); ok {
103-
return usage(*cmd)
103+
return usage(*cmd, ops)
104104
}
105105

106-
return execute(ctx, stack)
106+
return execute(ctx, stack, ops)
107107
}
108108

109109
// execute traverses the command stack recursively executing the lifecycle routines at each level.
110-
func execute(ctx context.Context, stack []command) error {
110+
func execute(ctx context.Context, stack []command, ops *ExecuteOptions) error {
111111
if len(stack) == 0 {
112112
return nil
113113
}
@@ -122,22 +122,22 @@ func execute(ctx context.Context, stack []command) error {
122122
)
123123

124124
// run init (if applicable)
125-
if err := this.onInit(ctx); err != nil {
125+
if err := this.onInit(ctx, ops); err != nil {
126126
return err
127127
}
128128

129129
// if this is a leaf, run, otherwise recurse
130130
if len(stack) == 1 {
131-
err = this.run(ctx)
131+
err = this.run(ctx, ops)
132132
} else {
133-
err = execute(ctx, stack[1:])
133+
err = execute(ctx, stack[1:], ops)
134134
}
135135
if err != nil {
136136
return err
137137
}
138138

139139
// run destroy (if applicable)
140-
if err := this.onDestroy(ctx); err != nil {
140+
if err := this.onDestroy(ctx, ops); err != nil {
141141
return err
142142
}
143143

@@ -154,42 +154,42 @@ type command struct {
154154
}
155155

156156
// onInit calls the [RunnableLifecycle] init routine if present on c.
157-
func (c command) onInit(ctx context.Context) error {
157+
func (c command) onInit(ctx context.Context, ops *ExecuteOptions) error {
158158
var err error
159159

160160
if cmd, ok := c.Command.(RunnableLifecycle); ok {
161161
err = cmd.Initialize(ctx, c.args)
162162
}
163163

164164
if errors.Is(err, ErrShowUsage) {
165-
_ = usage(c)
165+
_ = usage(c, ops)
166166
os.Exit(2)
167167
}
168168

169169
return err
170170
}
171171

172172
// run calls the [Runnable] run routine of c.
173-
func (c command) run(ctx context.Context) error {
173+
func (c command) run(ctx context.Context, ops *ExecuteOptions) error {
174174
err := c.Run(ctx, c.args)
175175
if errors.Is(err, ErrShowUsage) {
176-
_ = usage(c)
176+
_ = usage(c, ops)
177177
os.Exit(2)
178178
}
179179

180180
return err
181181
}
182182

183183
// onDestroy calls the [RunnableLifecycle] destroy routine if present on c.
184-
func (c command) onDestroy(ctx context.Context) error {
184+
func (c command) onDestroy(ctx context.Context, ops *ExecuteOptions) error {
185185
var err error
186186

187187
if cmd, ok := c.Command.(RunnableLifecycle); ok {
188188
err = cmd.Destroy(ctx, c.args)
189189
}
190190

191191
if errors.Is(err, ErrShowUsage) {
192-
_ = usage(c)
192+
_ = usage(c, ops)
193193
os.Exit(2)
194194
}
195195

options.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package cmder
22

3+
import "io"
4+
35
// ExecuteOptions configure the behaviour of [Execute].
46
type ExecuteOptions struct {
57
args []string
68
nativeFlags bool
79
bindEnv bool
810
bindEnvPrefix string
911
interspersed bool
12+
usageTemplate string
13+
usageWriter io.Writer
1014
}
1115

1216
// ExecuteOption is a single option passed to [Execute].
@@ -72,3 +76,22 @@ func WithInterspersedArgs() ExecuteOption {
7276
ops.interspersed = true
7377
}
7478
}
79+
80+
// WithUsageTemplate is used to provide an alternate template for rendering command usage help text. The template is
81+
// rendered by the standard [text/template] package. This is particularly useful for applications which prefer to format
82+
// command usage information differently than the cmder defaults.
83+
//
84+
// By default, the [CobraUsageTemplate] template is used.
85+
func WithUsageTemplate(tmpl string) ExecuteOption {
86+
return func(ops *ExecuteOptions) {
87+
ops.usageTemplate = tmpl
88+
}
89+
}
90+
91+
// WithUsageOutput is used to provide an alternate [io.Writer] to write rendered command usage help text. By default,
92+
// [os.Stderr] is used.
93+
func WithUsageOutput(output io.Writer) ExecuteOption {
94+
return func(ops *ExecuteOptions) {
95+
ops.usageWriter = output
96+
}
97+
}

usage.go

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import (
44
"bytes"
55
"cmp"
66
"flag"
7-
"io"
8-
"os"
97
"slices"
108
"strings"
119
"text/template"
@@ -85,19 +83,12 @@ Examples:
8583
const StdFlagUsageTemplate = `usage: {{ .Command.UsageLine }}
8684
{{ flagusage . }}`
8785

88-
// UsageTemplate is the text template for rendering command usage information.
89-
var UsageTemplate = CobraUsageTemplate
90-
91-
// UsageOutputWriter is the default writer for command usage information. Standard error is recommended, but you can
92-
// override this if needed (particularly useful in tests).
93-
var UsageOutputWriter io.Writer = os.Stderr
94-
95-
// ErrShowUsage instructs cmder to render usage and exit.
86+
// ErrShowUsage instructs cmder to render usage and exit (status 2).
9687
var ErrShowUsage = flag.ErrHelp
9788

9889
// usage renders usage text for a [Command] using the default template [UsageTemplate]. Output is written to
9990
// [UsageOutputWriter].
100-
func usage(cmd command) error {
91+
func usage(cmd command, ops *ExecuteOptions) error {
10192
tmpl, err := template.New("usage").Funcs(template.FuncMap{
10293
"commands": subcommands,
10394
"flags": flags,
@@ -111,12 +102,12 @@ func usage(cmd command) error {
111102
"contains": strings.Contains,
112103
"trim": strings.TrimSpace,
113104
"lines": strings.Lines,
114-
}).Parse(UsageTemplate)
105+
}).Parse(ops.usageTemplate)
115106
if err != nil {
116107
return err
117108
}
118109

119-
return tmpl.Execute(UsageOutputWriter, cmd)
110+
return tmpl.Execute(ops.usageWriter, cmd)
120111
}
121112

122113
// subcommands returns a map of (visible) child subcommands for cmd.

usage_test.go

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package cmder
33
import (
44
"bytes"
55
"flag"
6-
"os"
76
"testing"
87
"time"
98

@@ -169,18 +168,14 @@ func TestUsage(t *testing.T) {
169168
cmd.fs.String("web.telemetry-path", "/metrics", "path under which to expose metrics")
170169
cmd.fs.Bool("web.disable-exporter-metrics", false, "exclude metrics about the exporter itself (go_*)")
171170

172-
t.Cleanup(func() {
173-
UsageOutputWriter = os.Stderr
174-
UsageTemplate = CobraUsageTemplate
175-
})
176-
177171
t.Run("CobraUsageTemplate", func(t *testing.T) {
178172
t.Run("should render correctly", func(t *testing.T) {
179173
var buf bytes.Buffer
180-
UsageOutputWriter = &buf
181-
UsageTemplate = CobraUsageTemplate
182174

183-
err := usage(cmd)
175+
err := usage(cmd, &ExecuteOptions{
176+
usageTemplate: CobraUsageTemplate,
177+
usageWriter: &buf,
178+
})
184179
assert(t, nilerr(err))
185180

186181
if diff := cmp.Diff(ExpectedCobraUsageTemplate, buf.String()); diff != "" {
@@ -201,10 +196,11 @@ func TestUsage(t *testing.T) {
201196
})
202197

203198
var buf bytes.Buffer
204-
UsageOutputWriter = &buf
205-
UsageTemplate = CobraUsageTemplate
206199

207-
err := usage(cmd)
200+
err := usage(cmd, &ExecuteOptions{
201+
usageTemplate: CobraUsageTemplate,
202+
usageWriter: &buf,
203+
})
208204
assert(t, nilerr(err))
209205

210206
if diff := cmp.Diff(ExpectedCobraUsageTemplate, buf.String()); diff != "" {
@@ -216,10 +212,11 @@ func TestUsage(t *testing.T) {
216212
t.Run("StdFlagUsageTemplate", func(t *testing.T) {
217213
t.Run("should render correctly", func(t *testing.T) {
218214
var buf bytes.Buffer
219-
UsageOutputWriter = &buf
220-
UsageTemplate = StdFlagUsageTemplate
221215

222-
err := usage(cmd)
216+
err := usage(cmd, &ExecuteOptions{
217+
usageTemplate: StdFlagUsageTemplate,
218+
usageWriter: &buf,
219+
})
223220
assert(t, nilerr(err))
224221

225222
if diff := cmp.Diff(ExpectedStdFlagUsageTemplate, buf.String()); diff != "" {

0 commit comments

Comments
 (0)