Skip to content

Commit 0dfa582

Browse files
committed
fix: skip hidden subcommands inn rendered usage texts
The usage renderer failed to respect the hidden status on subcommands. This has been corrected. Additionally, [Hidden] has been removed from the Documented interface and given it's own interface, [HiddenCommand], since it's generally rare for subcommands to be hidden and adds unecessary bloat. Teach [Execute] to detect [ErrShowUsage], which allows a command's lifecycle routines to trigger rendering of command usage. A few small fixes throughout the project.
1 parent 6e470ec commit 0dfa582

File tree

12 files changed

+245
-341
lines changed

12 files changed

+245
-341
lines changed

command.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type Command interface {
2222
// rendering usage and help texts.
2323
Documented
2424

25-
// Name returns the name of this command.
25+
// Name returns the name of this command or subcommand.
2626
Name() string
2727
}
2828

@@ -113,7 +113,10 @@ type Documented interface {
113113

114114
// ExampleText returns motivating usage examples for your command.
115115
ExampleText() string
116+
}
116117

118+
// HiddenCommand is implemented by commands which are not user facing. Hidden commands are not displayed in help texts.
119+
type HiddenCommand interface {
117120
// Hidden returns a flag indicating whether to mark this command as hidden, preventing it from being rendered in
118121
// help output.
119122
Hidden() bool

example_lifecycle_test.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,6 @@ func (c *LifecycleCommand) ExampleText() string {
5858
return LifecycleCommandExamples
5959
}
6060

61-
func (c *LifecycleCommand) Hidden() bool {
62-
return false
63-
}
64-
6561
func ExampleRunnableLifecycle() {
6662
args := []string{}
6763

example_subcommands_test.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,6 @@ func (c *ParentCommand) ExampleText() string {
8181
return ParentCommandExamples
8282
}
8383

84-
func (c *ParentCommand) Hidden() bool {
85-
return false
86-
}
87-
8884
func (c *ParentCommand) Subcommands() []cmder.Command {
8985
return c.subcommands
9086
}
@@ -144,10 +140,6 @@ func (c *ChildCommand) ExampleText() string {
144140
return ChildCommandExamples
145141
}
146142

147-
func (c *ChildCommand) Hidden() bool {
148-
return false
149-
}
150-
151143
// === EXAMPLE ===
152144

153145
func ExampleRootCommand() {

example_test.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,6 @@ func (c *HelloWorldCommand) ExampleText() string {
4848
return HelloWorldCommandExamples
4949
}
5050

51-
func (c *HelloWorldCommand) Hidden() bool {
52-
return false
53-
}
54-
5551
func ExampleCommand() {
5652
args := []string{"from", "cmder"}
5753
cmd := &HelloWorldCommand{}

examples/hello-world/child.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,3 @@ func (c *WorldCommand) HelpText() string {
5757
func (c *WorldCommand) ExampleText() string {
5858
return WorldCommandExamples
5959
}
60-
61-
func (c *WorldCommand) Hidden() bool {
62-
return false
63-
}

examples/hello-world/parent.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,6 @@ func (c *HelloCommand) ExampleText() string {
8080
return HelloCommandExamples
8181
}
8282

83-
func (c *HelloCommand) Hidden() bool {
84-
return false
85-
}
86-
8783
func (c *HelloCommand) Subcommands() []cmder.Command {
8884
return c.subcommands
8985
}

examples/http/main.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ func (c *ServerCommand) InitializeFlags(fs *flag.FlagSet) {
9090

9191
func (c *ServerCommand) Initialize(ctx context.Context, args []string) error {
9292
if len(args) != 0 {
93-
return fmt.Errorf("too many arguments: %v", args)
93+
fmt.Fprintf(os.Stderr, "error: too many arguments: %v\n", args)
94+
return cmder.ErrShowUsage
9495
}
9596

9697
if !c.noAuth && c.basicAuth == "" {
@@ -161,10 +162,6 @@ func (c *ServerCommand) ExampleText() string {
161162
return ServerCommandExamples
162163
}
163164

164-
func (c *ServerCommand) Hidden() bool {
165-
return false
166-
}
167-
168165
func main() {
169166
cmd := &ServerCommand{}
170167

execute.go

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"flag"
7+
"fmt"
78
"os"
89
"regexp"
910
"strings"
@@ -18,7 +19,7 @@ var (
1819
// Returned when an [ExecuteOption] provided to [Execute] is illegal.
1920
ErrIllegalExecuteOptions = errors.New("cmder: illegal command execution option")
2021

21-
// Returned when an [ExecuteOption] provided to [Execute] is illegal.
22+
// Returned when failed to update flag value from environment variable.
2223
ErrEnvironmentBindFailure = errors.New("cmder: failed to update flag from environment variable")
2324
)
2425

@@ -77,6 +78,9 @@ var (
7778
// Whenever the user provides the '-h' or '--help' flag at the command line, [Execute] will display command usage and
7879
// exit. The format of the help text can be adjusted by configuring [UsageTemplate]. By default, usage information will
7980
// be written to stderr, but this can be adjusted by setting [UsageOutputWriter].
81+
//
82+
// If a command's [Run] routine returns [ErrShowUsage] (or an error wrapping [ErrShowUsage]), [Execute] will render
83+
// help text and exit with status 2.
8084
func Execute(ctx context.Context, cmd Command, op ...ExecuteOption) error {
8185
// do some checks
8286
if cmd == nil {
@@ -154,25 +158,45 @@ type command struct {
154158

155159
// onInit calls the [RunnableLifecycle] init routine if present on c.
156160
func (c command) onInit(ctx context.Context) error {
161+
var err error
162+
157163
if cmd, ok := c.Command.(RunnableLifecycle); ok {
158-
return cmd.Initialize(ctx, c.args)
164+
err = cmd.Initialize(ctx, c.args)
159165
}
160166

161-
return nil
167+
if errors.Is(err, ErrShowUsage) {
168+
_ = usage(c)
169+
os.Exit(2)
170+
}
171+
172+
return err
162173
}
163174

164175
// run calls the [Runnable] run routine of c.
165176
func (c command) run(ctx context.Context) error {
166-
return c.Run(ctx, c.args)
177+
err := c.Run(ctx, c.args)
178+
if errors.Is(err, ErrShowUsage) {
179+
_ = usage(c)
180+
os.Exit(2)
181+
}
182+
183+
return err
167184
}
168185

169186
// onDestroy calls the [RunnableLifecycle] destroy routine if present on c.
170187
func (c command) onDestroy(ctx context.Context) error {
188+
var err error
189+
171190
if cmd, ok := c.Command.(RunnableLifecycle); ok {
172-
return cmd.Destroy(ctx, c.args)
191+
err = cmd.Destroy(ctx, c.args)
173192
}
174193

175-
return nil
194+
if errors.Is(err, ErrShowUsage) {
195+
_ = usage(c)
196+
os.Exit(2)
197+
}
198+
199+
return err
176200
}
177201

178202
// buildCallStack builds a slice representing the command call stack. The first element in the slice is the root
@@ -287,7 +311,11 @@ func bindEnvironmentFlags(stack []command, cmd command, ops *ExecuteOptions) err
287311

288312
if value, ok := os.LookupEnv(variable); ok {
289313
if err := flag.Value.Set(value); err != nil {
290-
return errors.Join(ErrEnvironmentBindFailure, err)
314+
return errors.Join(
315+
ErrEnvironmentBindFailure,
316+
fmt.Errorf("cmder: failed to set flag %s from variable %s", flag.Name, variable),
317+
err,
318+
)
291319
}
292320
}
293321
}

execute_test.go

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package cmder
33
import (
44
"context"
55
"flag"
6-
"fmt"
7-
"slices"
86
"testing"
97
)
108

@@ -89,26 +87,3 @@ func TestExecute(t *testing.T) {
8987
})
9088
})
9189
}
92-
93-
type result struct {
94-
res bool
95-
msg string
96-
}
97-
98-
func assert(t *testing.T, res result) {
99-
if !res.res {
100-
t.Fatalf("expectation failed: %s", res.msg)
101-
}
102-
}
103-
104-
func eq[T comparable](expected, actual T) result {
105-
return result{expected == actual, fmt.Sprintf("values not equal: expected %v but was %v", expected, actual)}
106-
}
107-
108-
func nilerr(err error) result {
109-
return result{err == nil, fmt.Sprintf("unexpected error: %v", err)}
110-
}
111-
112-
func match[S ~[]E, E comparable](expected, actual S) result {
113-
return result{slices.Equal(expected, actual), fmt.Sprintf("slices not equal: expected %v but was %v", expected, actual)}
114-
}

testutils_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package cmder
2+
3+
import (
4+
"fmt"
5+
"slices"
6+
"testing"
7+
)
8+
9+
// result represents the result of an assertion. res is false if the assertion failed. msg is a descriptive message for
10+
// the failed assertion.
11+
type result struct {
12+
res bool
13+
msg string
14+
}
15+
16+
// assert fails the test if assertion res failed.
17+
func assert(t *testing.T, res result) {
18+
if !res.res {
19+
t.Fatalf("expectation failed: %s", res.msg)
20+
}
21+
}
22+
23+
// eq asserts that the given values are equal (==).
24+
func eq[T comparable](expected, actual T) result {
25+
return result{expected == actual, fmt.Sprintf("values not equal: expected %v but was %v", expected, actual)}
26+
}
27+
28+
// nilerr asserts that the given error is nil.
29+
func nilerr(err error) result {
30+
return result{err == nil, fmt.Sprintf("unexpected error: %v", err)}
31+
}
32+
33+
// match asserts that the given slices have the same values.
34+
func match[S ~[]E, E comparable](expected, actual S) result {
35+
return result{slices.Equal(expected, actual), fmt.Sprintf("slices not equal: expected %v but was %v", expected, actual)}
36+
}

0 commit comments

Comments
 (0)