Skip to content

Commit 68e2cca

Browse files
committed
Improve panic location
1 parent 54fa8dc commit 68e2cca

File tree

2 files changed

+59
-4
lines changed

2 files changed

+59
-4
lines changed

run.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import (
66
"fmt"
77
"io"
88
"os"
9+
"runtime"
10+
"runtime/debug"
11+
"strconv"
12+
"strings"
913
)
1014

1115
// RunOptions specifies options for running a command.
@@ -46,9 +50,15 @@ func run(ctx context.Context, cmd *Command, state *State) (retErr error) {
4650
if r := recover(); r != nil {
4751
switch err := r.(type) {
4852
case error:
49-
retErr = fmt.Errorf("internal: %v", err)
53+
// If error is from cli package (e.g., flag type mismatch), don't add location info
54+
var intErr *internalError
55+
if errors.As(err, &intErr) {
56+
retErr = err
57+
} else {
58+
retErr = fmt.Errorf("panic: %v\n\n%s", err, location(4))
59+
}
5060
default:
51-
retErr = fmt.Errorf("recover: %v", r)
61+
retErr = fmt.Errorf("panic: %v", r)
5262
}
5363
}
5464
}()
@@ -82,3 +92,34 @@ func checkAndSetRunOptions(opt *RunOptions) *RunOptions {
8292
}
8393
return opt
8494
}
95+
96+
var (
97+
goModuleName string
98+
)
99+
100+
// GoModuleName returns the Go module name of the current application.
101+
func GoModuleName() string {
102+
if goModuleName == "" {
103+
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Path != "" {
104+
goModuleName = info.Main.Path
105+
}
106+
}
107+
return goModuleName
108+
}
109+
110+
func location(skip int) string {
111+
var pcs [1]uintptr
112+
// Need to add 2 to skip to account for this function and runtime.Callers.
113+
n := runtime.Callers(skip+2, pcs[:])
114+
if n == 0 {
115+
return "unknown:0"
116+
}
117+
118+
frame, _ := runtime.CallersFrames(pcs[:n]).Next()
119+
120+
// Trim the module name from both function and file paths for cleaner output
121+
fn := strings.TrimPrefix(frame.Function, GoModuleName()+"/")
122+
file := strings.TrimPrefix(frame.File, GoModuleName()+"/")
123+
124+
return fn + " " + file + ":" + strconv.Itoa(frame.Line)
125+
}

state.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func GetFlag[T any](s *State, name string) T {
5252
*new(T),
5353
)
5454
// Flag exists but type doesn't match - this is an internal error
55-
panic(err)
55+
panic(&internalError{err: err})
5656
}
5757
}
5858
}
@@ -62,5 +62,19 @@ func GetFlag[T any](s *State, name string) T {
6262
formatFlagName(name),
6363
getCommandPath(s.path),
6464
)
65-
panic(err)
65+
panic(&internalError{err: err})
66+
}
67+
68+
// internalError is a marker type for errors that originate from the cli package itself. These are
69+
// programming errors (e.g., flag type mismatches) that should be caught during development.
70+
type internalError struct {
71+
err error
72+
}
73+
74+
func (e *internalError) Error() string {
75+
return e.err.Error()
76+
}
77+
78+
func (e *internalError) Unwrap() error {
79+
return e.err
6680
}

0 commit comments

Comments
 (0)