Skip to content

Commit 799540b

Browse files
committed
Fix root help flags and command-specific usage hints
- show root-local flags like --version in custom root help - use ExecuteC() so human usage errors know which command failed - suggest <failed command> --help instead of always fizzy --help - add regression tests for root help and usage hint behavior - regenerate SURFACE.txt for Cobra default version flag
1 parent 3b83561 commit 799540b

6 files changed

Lines changed: 121 additions & 5 deletions

File tree

SURFACE.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ FLAG fizzy --quiet type=bool
163163
FLAG fizzy --styled type=bool
164164
FLAG fizzy --token type=string
165165
FLAG fizzy --verbose type=bool
166+
FLAG fizzy --version type=bool
166167
FLAG fizzy account --agent type=bool
167168
FLAG fizzy account --api-url type=string
168169
FLAG fizzy account --count type=bool

internal/commands/help.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ func renderHelp(cmd *cobra.Command, w io.Writer) {
8989
}
9090

9191
func renderRootHelp(cmd *cobra.Command, w io.Writer) {
92+
cmd.InitDefaultHelpFlag()
93+
cmd.InitDefaultVersionFlag()
94+
9295
fmt.Fprintln(w, strings.TrimSpace(cmd.Long))
9396
fmt.Fprintln(w)
9497
fmt.Fprintln(w, "USAGE")
@@ -101,6 +104,12 @@ func renderRootHelp(cmd *cobra.Command, w io.Writer) {
101104
printCommandList(w, group.Commands)
102105
}
103106

107+
if flags := rootLocalFlags(cmd); len(flags) > 0 {
108+
fmt.Fprintln(w)
109+
fmt.Fprintln(w, "FLAGS")
110+
printFlags(w, flags)
111+
}
112+
104113
fmt.Fprintln(w)
105114
fmt.Fprintln(w, "GLOBAL OUTPUT FLAGS")
106115
printNamedFlags(w, cmd.PersistentFlags(), []string{"json", "quiet", "styled", "markdown", "ids-only", "count", "limit"})
@@ -122,6 +131,8 @@ func renderRootHelp(cmd *cobra.Command, w io.Writer) {
122131
}
123132

124133
func renderCommandHelp(cmd *cobra.Command, w io.Writer) {
134+
cmd.InitDefaultHelpFlag()
135+
125136
desc := strings.TrimSpace(cmd.Long)
126137
if desc == "" {
127138
desc = strings.TrimSpace(cmd.Short)
@@ -282,6 +293,24 @@ func printNamedFlags(w io.Writer, flags *pflag.FlagSet, names []string) {
282293
printFlags(w, selected)
283294
}
284295

296+
func rootLocalFlags(cmd *cobra.Command) []*pflag.Flag {
297+
excluded := map[string]bool{
298+
"agent": true, "api-url": true, "count": true, "ids-only": true,
299+
"json": true, "limit": true, "markdown": true, "profile": true,
300+
"quiet": true, "styled": true, "token": true, "verbose": true,
301+
}
302+
303+
flags := visibleFlags(cmd.Flags())
304+
result := make([]*pflag.Flag, 0, len(flags))
305+
for _, f := range flags {
306+
if excluded[f.Name] {
307+
continue
308+
}
309+
result = append(result, f)
310+
}
311+
return result
312+
}
313+
285314
func visibleFlags(flags *pflag.FlagSet) []*pflag.Flag {
286315
var result []*pflag.Flag
287316
flags.VisitAll(func(f *pflag.Flag) {

internal/commands/help_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func TestRenderRootHelp(t *testing.T) {
1515
renderHelp(rootCmd, &buf)
1616
out := buf.String()
1717

18-
for _, want := range []string{"CORE COMMANDS", "GLOBAL OUTPUT FLAGS", "LEARN MORE"} {
18+
for _, want := range []string{"CORE COMMANDS", "FLAGS", "--version", "GLOBAL OUTPUT FLAGS", "LEARN MORE"} {
1919
if !strings.Contains(out, want) {
2020
t.Fatalf("expected root help to contain %q, got:\n%s", want, out)
2121
}

internal/commands/root.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ func Execute() {
159159
// Default to Auto — PersistentPreRunE will re-resolve from parsed flags.
160160
outWriter = os.Stdout
161161
out = output.New(output.Options{Format: output.FormatAuto, Writer: os.Stdout})
162-
if err := rootCmd.Execute(); err != nil {
162+
cmd, err := rootCmd.ExecuteC()
163+
if err != nil {
163164
if format, formatErr := resolveFormat(); formatErr == nil {
164165
out = output.New(output.Options{Format: format, Writer: os.Stdout})
165166
}
@@ -170,7 +171,7 @@ func Execute() {
170171
e = &output.Error{Code: output.CodeUsage, Message: err.Error()}
171172
}
172173
if isHumanOutput() {
173-
printHumanError(e)
174+
printHumanError(cmd, e)
174175
} else {
175176
_ = out.Err(e)
176177
}
@@ -273,7 +274,7 @@ func requestedHumanOutput() bool {
273274
return false
274275
}
275276

276-
func printHumanError(err error) {
277+
func printHumanError(cmd *cobra.Command, err error) {
277278
e := output.AsError(err)
278279
msg := strings.TrimSpace(e.Message)
279280
if msg != "" {
@@ -283,8 +284,19 @@ func printHumanError(err error) {
283284
fmt.Fprintf(os.Stderr, "\nHint: %s\n", e.Hint)
284285
}
285286
if e.Code == output.CodeUsage && !strings.Contains(msg, "--help") {
286-
fmt.Fprintln(os.Stderr, "\nRun `fizzy --help` for usage.")
287+
fmt.Fprintf(os.Stderr, "\nRun `%s` for usage.\n", usageHelpCommand(cmd))
288+
}
289+
}
290+
291+
func usageHelpCommand(cmd *cobra.Command) string {
292+
if cmd == nil {
293+
return rootCmd.CommandPath() + " --help"
294+
}
295+
path := strings.TrimSpace(cmd.CommandPath())
296+
if path == "" {
297+
path = rootCmd.CommandPath()
287298
}
299+
return path + " --help"
288300
}
289301

290302
func init() {

internal/commands/root_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package commands
2+
3+
import (
4+
"io"
5+
"os"
6+
"strings"
7+
"testing"
8+
9+
"github.com/basecamp/cli/output"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
func TestUsageHelpCommand(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
cmd string
17+
want string
18+
}{
19+
{name: "nil falls back to root", cmd: "", want: "fizzy --help"},
20+
{name: "subcommand uses command path", cmd: "auth login", want: "fizzy auth login --help"},
21+
}
22+
23+
for _, tt := range tests {
24+
t.Run(tt.name, func(t *testing.T) {
25+
var cmd *cobra.Command
26+
if tt.cmd != "" {
27+
parts := strings.Split(tt.cmd, " ")
28+
found, _, err := rootCmd.Find(parts)
29+
if err != nil {
30+
t.Fatalf("find command: %v", err)
31+
}
32+
cmd = found
33+
}
34+
35+
if got := usageHelpCommand(cmd); got != tt.want {
36+
t.Fatalf("expected %q, got %q", tt.want, got)
37+
}
38+
})
39+
}
40+
}
41+
42+
func TestPrintHumanErrorUsesCommandSpecificHelp(t *testing.T) {
43+
oldStderr := os.Stderr
44+
r, w, err := os.Pipe()
45+
if err != nil {
46+
t.Fatalf("pipe: %v", err)
47+
}
48+
os.Stderr = w
49+
defer func() {
50+
os.Stderr = oldStderr
51+
}()
52+
53+
cmd, _, err := rootCmd.Find([]string{"auth", "login"})
54+
if err != nil {
55+
t.Fatalf("find command: %v", err)
56+
}
57+
58+
printHumanError(cmd, &output.Error{Code: output.CodeUsage, Message: "accepts 1 arg(s), received 0"})
59+
_ = w.Close()
60+
61+
body, err := io.ReadAll(r)
62+
if err != nil {
63+
t.Fatalf("read stderr: %v", err)
64+
}
65+
66+
out := string(body)
67+
if !strings.Contains(out, "Run `fizzy auth login --help` for usage.") {
68+
t.Fatalf("expected command-specific usage hint, got:\n%s", out)
69+
}
70+
if strings.Contains(out, "Run `fizzy --help` for usage.") {
71+
t.Fatalf("expected root usage hint to be omitted, got:\n%s", out)
72+
}
73+
}

internal/commands/surface_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func TestSurfaceSnapshot(t *testing.T) {
5858
func initAllHelpFlags(cmd *cobra.Command) {
5959
cmd.InitDefaultHelpCmd()
6060
cmd.InitDefaultHelpFlag()
61+
cmd.InitDefaultVersionFlag()
6162
for _, sub := range cmd.Commands() {
6263
initAllHelpFlags(sub)
6364
}

0 commit comments

Comments
 (0)