Skip to content

Commit e66a261

Browse files
committed
fix: unknown command handling
1 parent 51dee0d commit e66a261

3 files changed

Lines changed: 147 additions & 6 deletions

File tree

cmd/root.go

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,45 @@ func cleanVersion(v string) string {
2323
return v
2424
}
2525

26+
// FormatExecuteError returns styled output for known cobra errors
27+
// (unknown command, unknown flag). Returns empty string for unrecognized errors.
28+
func FormatExecuteError(err error) string {
29+
if err == nil {
30+
return ""
31+
}
32+
msg := err.Error()
33+
34+
var b strings.Builder
35+
36+
switch {
37+
case strings.HasPrefix(msg, "unknown command"):
38+
lines := strings.SplitN(msg, "\n", 2)
39+
b.WriteString(display.FailLine(lines[0]))
40+
if len(lines) > 1 {
41+
// Preserve cobra's "Did you mean this?" suggestion lines
42+
for line := range strings.SplitSeq(lines[1], "\n") {
43+
trimmed := strings.TrimSpace(line)
44+
if trimmed != "" {
45+
b.WriteString("\n")
46+
b.WriteString(display.HintLine(trimmed))
47+
}
48+
}
49+
}
50+
b.WriteString("\n")
51+
b.WriteString(display.HintLine("Run 'archcore --help' to see available commands"))
52+
return b.String()
53+
54+
case strings.HasPrefix(msg, "unknown flag"), strings.HasPrefix(msg, "unknown shorthand flag"):
55+
b.WriteString(display.FailLine(msg))
56+
b.WriteString("\n")
57+
b.WriteString(display.HintLine("Run 'archcore --help' to see available options"))
58+
return b.String()
59+
60+
default:
61+
return ""
62+
}
63+
}
64+
2665
func NewRootCmd(version string) *cobra.Command {
2766
cleaned := cleanVersion(version)
2867
root := &cobra.Command{
@@ -32,22 +71,22 @@ func NewRootCmd(version string) *cobra.Command {
3271
SilenceErrors: true,
3372
SilenceUsage: true,
3473
Run: func(cmd *cobra.Command, args []string) {
35-
fmt.Println(display.WelcomeBanner())
36-
fmt.Println()
74+
fmt.Fprintln(cmd.OutOrStdout(), display.WelcomeBanner())
75+
fmt.Fprintln(cmd.OutOrStdout())
3776
_ = cmd.Usage()
3877
},
3978
CompletionOptions: cobra.CompletionOptions{
4079
DisableDefaultCmd: true,
4180
},
4281
}
4382

44-
root.SetVersionTemplate(fmt.Sprintf("%s\n", cleaned))
83+
root.SetVersionTemplate("{{.Version}}\n")
4584

4685
defaultHelp := root.HelpFunc()
4786
root.SetHelpFunc(func(cmd *cobra.Command, args []string) {
4887
if cmd == root {
49-
fmt.Println(display.WelcomeBanner())
50-
fmt.Println()
88+
fmt.Fprintln(cmd.OutOrStdout(), display.WelcomeBanner())
89+
fmt.Fprintln(cmd.OutOrStdout())
5190
_ = cmd.Usage()
5291
return
5392
}

cmd/root_test.go

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package cmd
22

3-
import "testing"
3+
import (
4+
"errors"
5+
"strings"
6+
"testing"
7+
)
48

59
func TestCleanVersion(t *testing.T) {
610
tests := []struct {
@@ -25,3 +29,95 @@ func TestCleanVersion(t *testing.T) {
2529
})
2630
}
2731
}
32+
33+
func TestFormatExecuteError(t *testing.T) {
34+
tests := []struct {
35+
name string
36+
err error
37+
wantEmpty bool
38+
wantSubstr []string
39+
}{
40+
{
41+
name: "nil error",
42+
err: nil,
43+
wantEmpty: true,
44+
},
45+
{
46+
name: "unknown command",
47+
err: errors.New(`unknown command "foo" for "archcore"`),
48+
wantSubstr: []string{`unknown command "foo"`, "--help", "available commands"},
49+
},
50+
{
51+
name: "unknown command with suggestion",
52+
err: errors.New("unknown command \"ini\" for \"archcore\"\n\nDid you mean this?\n\tinit"),
53+
wantSubstr: []string{`unknown command "ini"`, "Did you mean this?", "init", "--help"},
54+
},
55+
{
56+
name: "unknown flag",
57+
err: errors.New("unknown flag: --bogus"),
58+
wantSubstr: []string{"unknown flag: --bogus", "--help", "available options"},
59+
},
60+
{
61+
name: "unknown shorthand flag",
62+
err: errors.New(`unknown shorthand flag: 'x' in -x`),
63+
wantSubstr: []string{"unknown shorthand flag", "--help", "available options"},
64+
},
65+
{
66+
name: "unrelated error",
67+
err: errors.New("something else went wrong"),
68+
wantEmpty: true,
69+
},
70+
}
71+
for _, tt := range tests {
72+
t.Run(tt.name, func(t *testing.T) {
73+
got := FormatExecuteError(tt.err)
74+
if tt.wantEmpty {
75+
if got != "" {
76+
t.Errorf("expected empty string, got %q", got)
77+
}
78+
return
79+
}
80+
for _, sub := range tt.wantSubstr {
81+
if !strings.Contains(got, sub) {
82+
t.Errorf("output missing %q\ngot: %s", sub, got)
83+
}
84+
}
85+
})
86+
}
87+
}
88+
89+
func TestUnknownCommandOutput(t *testing.T) {
90+
root := NewRootCmd("test")
91+
root.SetArgs([]string{"foo"})
92+
93+
err := root.ExecuteContext(t.Context())
94+
if err == nil {
95+
t.Fatal("expected error for unknown command")
96+
}
97+
98+
msg := FormatExecuteError(err)
99+
if !strings.Contains(msg, `"foo"`) {
100+
t.Errorf("output should mention the unknown command, got: %s", msg)
101+
}
102+
if !strings.Contains(msg, "--help") {
103+
t.Errorf("output should mention --help, got: %s", msg)
104+
}
105+
}
106+
107+
func TestUnknownFlagOutput(t *testing.T) {
108+
root := NewRootCmd("test")
109+
root.SetArgs([]string{"--bogus"})
110+
111+
err := root.ExecuteContext(t.Context())
112+
if err == nil {
113+
t.Fatal("expected error for unknown flag")
114+
}
115+
116+
msg := FormatExecuteError(err)
117+
if !strings.Contains(msg, "--bogus") {
118+
t.Errorf("output should mention the unknown flag, got: %s", msg)
119+
}
120+
if !strings.Contains(msg, "--help") {
121+
t.Errorf("output should mention --help, got: %s", msg)
122+
}
123+
}

main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"context"
5+
"fmt"
56
"os"
67
"os/signal"
78
"runtime/debug"
@@ -32,6 +33,11 @@ func main() {
3233
resolveVersion()
3334

3435
if err := cmd.NewRootCmd(version).ExecuteContext(ctx); err != nil {
36+
if msg := cmd.FormatExecuteError(err); msg != "" {
37+
fmt.Println(msg)
38+
} else {
39+
fmt.Fprintln(os.Stderr, err)
40+
}
3541
os.Exit(1)
3642
}
3743
}

0 commit comments

Comments
 (0)