Skip to content

Commit d58f826

Browse files
committed
chore: render usage and help texts differently
Prior to this revision, -h and --help flags rendered complete help text. This revision adjusts this slightly, now differentiating between usage text ('-h') and longer help text ('--help'). The usage text offers a short command summary, and the help text offers longer doc-style text. With this, a few adjustments were made to the usage machinery and the relevant execute options. Usage/help text is now rendered to stdout by default. This makes it easier to pass the output through a paging program like 'less' or 'more'. The custom flag value types in the getopt package now feature short constructor functions (e.g. getopt.Time, getopt.Map). Examples have been updated. The 'http' example has been updated with improved flag descriptions.
1 parent e01ba25 commit d58f826

13 files changed

Lines changed: 188 additions & 119 deletions

‎example_exec_option_bind_env_test.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import (
1010
)
1111

1212
func ExampleWithEnvironmentBinding() {
13-
_ = os.Setenv("BINDENV_SHOW_FORMAT", "overidden-by-flag")
14-
_ = os.Setenv("BINDENV_SHOW_PAGECOUNT", "20")
13+
os.Setenv("BINDENV_SHOW_FORMAT", "overidden-by-flag")
14+
os.Setenv("BINDENV_SHOW_PAGECOUNT", "20")
1515

1616
args := []string{"show", "--format=pretty"}
1717

‎examples/http/main.go‎

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func main() {
2323
err := cmder.Execute(ctx, &ServerCommand{})
2424
cancel()
2525

26-
if err != nil {
26+
if err != nil && !errors.Is(err, cmder.ErrShowUsage) && !errors.Is(err, cmder.ErrShowUsage) {
2727
fmt.Printf("unexpected error occurred: %v\n", err)
2828
os.Exit(1)
2929
}
@@ -91,13 +91,20 @@ type ServerCommand struct {
9191
}
9292

9393
func (c *ServerCommand) InitializeFlags(fs *flag.FlagSet) {
94-
fs.StringVar(&c.addr, "http.bind-addr", ":8080", "bind address for the web server")
95-
fs.DurationVar(&c.readTimeout, "http.read-timeout", time.Duration(0), "read timeout for requests")
96-
fs.DurationVar(&c.writeTimeout, "http.write-timeout", time.Duration(0), "write timeout for responses")
97-
fs.IntVar(&c.maxHeaderBytes, "http.max-header-size", http.DefaultMaxHeaderBytes, "max permitted size of the headers in a request")
98-
fs.Int64Var(&c.maxBodySize, "http.max-body-size", 1<<26, "max permitted size of the headers in a request")
99-
fs.StringVar(&c.basicAuth, "http.auth-basic", "", "basic auth credentials (in format user:pass)")
100-
fs.BoolVar(&c.noAuth, "http.no-auth", false, "disable basic auth")
94+
fs.StringVar(&c.addr, "http.bind-addr", ":8080",
95+
"Sets the `address:port` on which the server will accept requests. The address may be an IPv4 (e.g. 127.0.0.1) or IPv6 (e.g. [2001:db8::1]) address. The address may be empty, in which case the local system is implied (0.0.0.0). If the port is empty or '0' (e.g. ':0'), a port number is automatically chosen.")
96+
fs.DurationVar(&c.readTimeout, "http.read-timeout", time.Duration(0),
97+
"Configures the maximum duration for reading the entire request, including the body (e.g. 10s). Negative or zero (e.g. 0s) disables the timeout.")
98+
fs.DurationVar(&c.writeTimeout, "http.write-timeout", time.Duration(0),
99+
"Configures the maximum duration for writing a client response. Negative or zero (e.g. 0s) disables the timeout.")
100+
fs.IntVar(&c.maxHeaderBytes, "http.max-header-size", http.DefaultMaxHeaderBytes,
101+
"Set the maximum header size, in bytes. Negative or zero disables the limit.")
102+
fs.Int64Var(&c.maxBodySize, "http.max-body-size", 1<<26,
103+
"Set the maximum request body size, in bytes. Negative or zero disables the limit.")
104+
fs.StringVar(&c.basicAuth, "http.auth-basic", "",
105+
"Configure basic auth credentials with format `user:pass`.")
106+
fs.BoolVar(&c.noAuth, "http.no-auth", false,
107+
"Disable basic auth, making the server available to all.")
101108
}
102109

103110
func (c *ServerCommand) Initialize(ctx context.Context, args []string) error {

‎execute.go‎

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,13 @@ var ErrEnvironmentBindFailure = errors.New("cmder: failed to update flag from en
7070
//
7171
// # Usage and Help Texts
7272
//
73-
// Whenever the user provides the '-h' or '--help' flag at the command line and the command doesn't register custom help
74-
// flags, Execute will display command usage and return [ErrShowUsage]. The format of the help text can be adjusted with
75-
// [WithUsageTemplate]. By default, usage information will be written to stderr, but this can be adjusted by setting
76-
// [WithUsageOutput].
73+
// Unless explicitly overridden by the command, the '-h' flag instructs Execute to render command usage information to
74+
// stdout and return [ErrShowUsage]. The default usage text includes a usage synopsis, subcommands and flags. The
75+
// format of the usage text can be adjusted (see [WithUsageTemplate]). Returning [ErrShowUsage] from a command's
76+
// Initialize or Run routines will also instruct Execute to render usage.
7777
//
78-
// If a command's Run routine returns [ErrShowUsage] (or an error wrapping [ErrShowUsage]), Execute will render
79-
// help text and return the error.
78+
// Likewise, the '--help' flag instructs Execute to render extended help usage information to stdout, returning
79+
// [ErrShowHelp]. The format may be adjusted (see [WithHelpTemplate]).
8080
func Execute(ctx context.Context, cmd Command, op ...ExecuteOption) error {
8181
// do some checks
8282
if cmd == nil {
@@ -86,8 +86,9 @@ func Execute(ctx context.Context, cmd Command, op ...ExecuteOption) error {
8686
// prepare executor options
8787
ops := &ExecuteOptions{
8888
args: os.Args[1:],
89-
usageTemplate: CobraUsageTemplate,
90-
usageWriter: os.Stderr,
89+
usageTemplate: DefaultUsageTemplate,
90+
helpTemplate: DefaultHelpTemplate,
91+
outputWriter: os.Stdout,
9192
}
9293
for _, f := range op {
9394
f(ops)
@@ -144,18 +145,22 @@ func execute(ctx context.Context, stack []command, ops *ExecuteOptions) error {
144145
type command struct {
145146
Command
146147

147-
fs *flag.FlagSet
148-
args []string
149-
showHelp bool
148+
fs *flag.FlagSet
149+
args []string
150+
showUsage bool
151+
showHelp bool
150152
}
151153

152154
// onInit calls the [Initializer] init routine if present on c.
153155
func (c command) onInit(ctx context.Context, ops *ExecuteOptions) error {
154156
var err error
155157

156-
if c.showHelp {
158+
if c.showUsage {
157159
return errors.Join(ErrShowUsage, usage(c, ops))
158160
}
161+
if c.showHelp {
162+
return errors.Join(ErrShowUsage, help(c, ops))
163+
}
159164

160165
if cmd, ok := c.Command.(Initializer); ok {
161166
err = cmd.Initialize(ctx, c.args)
@@ -170,6 +175,13 @@ func (c command) onInit(ctx context.Context, ops *ExecuteOptions) error {
170175

171176
// run calls the [Runnable] run routine of c.
172177
func (c command) run(ctx context.Context, ops *ExecuteOptions) error {
178+
if c.showUsage {
179+
return errors.Join(ErrShowUsage, usage(c, ops))
180+
}
181+
if c.showHelp {
182+
return errors.Join(ErrShowUsage, help(c, ops))
183+
}
184+
173185
err := c.Run(ctx, c.args)
174186
if errors.Is(err, ErrShowUsage) {
175187
return errors.Join(err, usage(c, ops))
@@ -214,9 +226,11 @@ func buildCallStack(cmd Command, ops *ExecuteOptions) ([]command, error) {
214226
}
215227

216228
// add help flags
217-
if this.fs.Lookup("h") == nil && this.fs.Lookup("help") == nil {
218-
this.fs.BoolVar(&this.showHelp, "h", false, "show command help and usage information")
219-
this.fs.BoolVar(&this.showHelp, "help", false, "show command help and usage information")
229+
if this.fs.Lookup("h") == nil {
230+
this.fs.BoolVar(&this.showUsage, "h", false, "show command usage information")
231+
}
232+
if this.fs.Lookup("help") == nil {
233+
this.fs.BoolVar(&this.showHelp, "help", false, "show command help information")
220234
}
221235

222236
// bind environment variables

‎flags.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import (
88
"github.com/brandon1024/cmder/getopt"
99
)
1010

11-
// FlagInitializer is an interface implemented by commands that need to register flags.
11+
// FlagInitializer is an interface implemented by a [Command] that need to register flags.
1212
//
13-
// InitializeFlags will be invoked during [Execute], prior to Initialize/Run/Destroy routines. You can use this to
13+
// InitializeFlags will be invoked during [Execute], prior to Initialize()/Run()/Destroy() routines. You can use this to
1414
// register flags for your command.
1515
//
16-
// If the command does not define help flags '-h' and '--help', they will be registered automatically and will instruct
16+
// If the command does not define help flags '-h' or '--help', they will be registered automatically and will instruct
1717
// [Execute] to render command usage.
1818
type FlagInitializer interface {
1919
InitializeFlags(*flag.FlagSet)

‎getopt/example_mapvar_test.go‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,20 @@ import (
1212
// This example demonstrates usage of [getopt.MapVar] for string maps. You'll often find map flags on commands that
1313
// perform templating of text files, for example.
1414
func ExampleMapVar() {
15-
variables := getopt.MapVar{}
16-
1715
fs := flag.NewFlagSet("map", flag.ContinueOnError)
16+
17+
// option 1: use MapVar directly
18+
variables := getopt.MapVar{}
1819
fs.Var(&variables, "variable", "specify runtime variables")
19-
fs.Var(&variables, "v", "specify runtime variables")
20+
21+
// option 2: wrap an existing map with Map
22+
arg := map[string]string{}
23+
fs.Var(getopt.Map(arg), "arg", "specify runtime args")
2024

2125
args := []string{
2226
"--variable", "key1=value1",
23-
"-v", "key2=value2,key3=value3",
24-
`--variable="hello= HI, WORLD "`,
27+
"--variable", "key2=value2,key3=value3",
28+
`--arg="hello= HI, WORLD "`,
2529
}
2630

2731
if err := fs.Parse(args); err != nil {
@@ -31,9 +35,12 @@ func ExampleMapVar() {
3135
for _, k := range slices.Sorted(maps.Keys(variables)) {
3236
fmt.Printf("%s: '%s'\n", k, variables[k])
3337
}
38+
for _, k := range slices.Sorted(maps.Keys(arg)) {
39+
fmt.Printf("%s: '%s'\n", k, arg[k])
40+
}
3441
// Output:
35-
// hello: ' HI, WORLD '
3642
// key1: 'value1'
3743
// key2: 'value2'
3844
// key3: 'value3'
45+
// hello: ' HI, WORLD '
3946
}

‎getopt/example_stringsvar_test.go‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,19 @@ func ExampleStringsVar() {
1616
var hosts getopt.StringsVar
1717
fs.Var(&hosts, "broker", "connect to a broker")
1818

19-
// option 2: wrap an existing slice
19+
// option 2: wrap an existing slice with Strings
2020
var args []string
21-
fs.Var((*getopt.StringsVar)(&args), "a", "provide args")
21+
fs.Var(getopt.Strings(&args), "a", "provide args")
22+
23+
// option 3: wrap an existing slice by pointer casting
24+
var patterns []string
25+
fs.Var((*getopt.StringsVar)(&patterns), "p", "provide patterns")
2226

2327
fs.Parse([]string{
2428
"--broker", "tls://broker-1.domain.example.com,tls://broker-2.domain.example.com",
2529
"-a", "CLIENT_USER",
2630
"-a", "CLIENT_PASS",
31+
"-p", "**/*.go,*.mod,*.sum",
2732
})
2833

2934
for _, host := range hosts {
@@ -32,9 +37,15 @@ func ExampleStringsVar() {
3237
for _, arg := range args {
3338
fmt.Printf("arg: '%s'\n", arg)
3439
}
40+
for _, patt := range patterns {
41+
fmt.Printf("patterns: '%s'\n", patt)
42+
}
3543
// Output:
3644
// broker: 'tls://broker-1.domain.example.com'
3745
// broker: 'tls://broker-2.domain.example.com'
3846
// arg: 'CLIENT_USER'
3947
// arg: 'CLIENT_PASS'
48+
// patterns: '**/*.go'
49+
// patterns: '*.mod'
50+
// patterns: '*.sum'
4051
}

‎getopt/example_timevar_test.go‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,35 @@ package getopt_test
33
import (
44
"flag"
55
"fmt"
6+
"time"
67

78
"github.com/brandon1024/cmder/getopt"
89
)
910

1011
// This example demonstrates the usage of [getopt.TimeVar].
1112
func ExampleTimeVar() {
12-
var since getopt.TimeVar
13-
1413
fs := flag.NewFlagSet("custom", flag.ContinueOnError)
14+
15+
// option 1: using TimeVar directly
16+
var since getopt.TimeVar
1517
fs.Var(&since, "since", "show items since")
1618

19+
// option 2: with Time
20+
var until time.Time
21+
fs.Var(getopt.Time(&until), "until", "show items until")
22+
1723
args := []string{
1824
"-since", "2025-01-01T00:00:00Z",
25+
"-until", "2026-01-01T00:00:00Z",
1926
}
2027

2128
if err := fs.Parse(args); err != nil {
2229
panic(err)
2330
}
2431

2532
fmt.Printf("since: %s\n", since.String())
33+
fmt.Printf("until: %s\n", until.String())
2634
// Output:
2735
// since: 2025-01-01T00:00:00Z
36+
// until: 2026-01-01 00:00:00 +0000 UTC
2837
}

‎getopt/mapvar.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ import (
2020
// key1=v=1,key2=v=2
2121
type MapVar map[string]string
2222

23+
// Map returns a [MapVar] for ss.
24+
func Map(m map[string]string) MapVar {
25+
return MapVar(m)
26+
}
27+
2328
// String returns the map, formatted as a set of key-value pairs.
2429
func (m MapVar) String() string {
2530
var entries []string

‎getopt/stringsvar.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import (
1717
// "value, 1","value, 2"
1818
type StringsVar []string
1919

20+
// Strings returns a [StringsVar] for ss.
21+
func Strings(ss *[]string) *StringsVar {
22+
return (*StringsVar)(ss)
23+
}
24+
2025
// String returns the slice, formatted as comma-separated values.
2126
func (s StringsVar) String() string {
2227
var builder strings.Builder

‎getopt/timevar.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import (
88
// [flag.Getter].
99
type TimeVar time.Time
1010

11+
// Time returns a [TimeVar] for tm.
12+
func Time(tm *time.Time) *TimeVar {
13+
return (*TimeVar)(tm)
14+
}
15+
1116
// String returns the [time.RFC3339] representation of the timestamp flag.
1217
func (t *TimeVar) String() string {
1318
return time.Time(*t).Format(time.RFC3339)

0 commit comments

Comments
 (0)