Skip to content

Commit 7344d9c

Browse files
authored
feat: add ParseAndRun convenience function (#10)
1 parent 7444a39 commit 7344d9c

5 files changed

Lines changed: 43 additions & 24 deletions

File tree

command.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ import (
99
"github.com/pressly/cli/pkg/suggest"
1010
)
1111

12+
// ErrHelp is returned by [Parse] when the -help or -h flag is invoked. It is identical to
13+
// [flag.ErrHelp] but re-exported here so callers using [Parse] and [Run] separately do not need to
14+
// import the flag package solely for error checking.
15+
//
16+
// Note: [ParseAndRun] handles this automatically and never surfaces ErrHelp to the caller.
17+
var ErrHelp = flag.ErrHelp
18+
1219
// Command represents a CLI command or subcommand within the application's command hierarchy.
1320
type Command struct {
1421
// Name is always a single word representing the command's name. It is used to identify the

examples/cmd/echo/main.go

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,7 @@ func main() {
3636
return nil
3737
},
3838
}
39-
if err := cli.Parse(root, os.Args[1:]); err != nil {
40-
if errors.Is(err, flag.ErrHelp) {
41-
fmt.Fprintf(os.Stdout, "%s\n", cli.DefaultUsage(root))
42-
return
43-
}
44-
fmt.Fprintf(os.Stderr, "error: %v\n", err)
45-
os.Exit(1)
46-
}
47-
if err := cli.Run(context.Background(), root, nil); err != nil {
39+
if err := cli.ParseAndRun(context.Background(), root, os.Args[1:], nil); err != nil {
4840
fmt.Fprintf(os.Stderr, "error: %v\n", err)
4941
os.Exit(1)
5042
}

examples/cmd/task/main.go

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,7 @@ func main() {
5050
},
5151
}
5252

53-
if err := cli.Parse(root, os.Args[1:]); err != nil {
54-
if errors.Is(err, flag.ErrHelp) {
55-
fmt.Fprintf(os.Stdout, "%s\n", cli.DefaultUsage(root))
56-
return
57-
}
58-
fmt.Fprintf(os.Stderr, "error: %v\n", err)
59-
os.Exit(1)
60-
}
61-
if err := cli.Run(context.Background(), root, nil); err != nil {
53+
if err := cli.ParseAndRun(context.Background(), root, os.Args[1:], nil); err != nil {
6254
fmt.Fprintf(os.Stderr, "error: %v\n", err)
6355
os.Exit(1)
6456
}

parse.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func Parse(root *Command, args []string) error {
5050
if arg == "-h" || arg == "--h" || arg == "-help" || arg == "--help" {
5151
// Combine flags first so the help message includes all inherited flags
5252
combineFlags(root.state.path)
53-
return flag.ErrHelp
53+
return ErrHelp
5454
}
5555
}
5656

run.go

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,34 @@ func Run(ctx context.Context, root *Command, options *RunOptions) error {
4949
return run(ctx, cmd, root.state)
5050
}
5151

52+
// ParseAndRun is a convenience function that combines [Parse] and [Run] into a single call. It
53+
// parses the command hierarchy, handles help flags automatically (printing usage to stdout and
54+
// returning nil), and then executes the resolved command.
55+
//
56+
// This is the recommended entry point for most CLI applications:
57+
//
58+
// if err := cli.ParseAndRun(ctx, root, os.Args[1:], nil); err != nil {
59+
// fmt.Fprintf(os.Stderr, "error: %v\n", err)
60+
// os.Exit(1)
61+
// }
62+
//
63+
// The options parameter may be nil, in which case default values are used. See [RunOptions] for
64+
// more details.
65+
//
66+
// For applications that need to perform work between parsing and execution (e.g., initializing
67+
// resources based on parsed flags), use [Parse] and [Run] separately.
68+
func ParseAndRun(ctx context.Context, root *Command, args []string, options *RunOptions) error {
69+
if err := Parse(root, args); err != nil {
70+
if errors.Is(err, ErrHelp) {
71+
options = checkAndSetRunOptions(options)
72+
_, _ = fmt.Fprintln(options.Stdout, DefaultUsage(root))
73+
return nil
74+
}
75+
return err
76+
}
77+
return Run(ctx, root, options)
78+
}
79+
5280
func run(ctx context.Context, cmd *Command, state *State) (retErr error) {
5381
defer func() {
5482
if r := recover(); r != nil {
@@ -121,12 +149,12 @@ func location(skip int) string {
121149

122150
frame, _ := runtime.CallersFrames(pcs[:n]).Next()
123151

124-
// Trim the module name from function and file paths for cleaner output.
125-
// Function names use the module path directly (e.g., "github.com/pressly/cli.Run").
152+
// Trim the module name from function and file paths for cleaner output. Function names use the
153+
// module path directly (e.g., "github.com/pressly/cli.Run").
126154
fn := strings.TrimPrefix(frame.Function, getGoModuleName()+"/")
127-
// File paths from runtime are absolute (e.g., "/Users/.../cli/run.go"). We want a relative
128-
// path for cleaner output. Try to find the module's import path in the filesystem path
129-
// (works with GOPATH-style layouts), otherwise fall back to just the base filename.
155+
// File paths from runtime are absolute (e.g., "/Users/.../cli/run.go"). We want a relative path
156+
// for cleaner output. Try to find the module's import path in the filesystem path (works with
157+
// GOPATH-style layouts), otherwise fall back to just the base filename.
130158
file := frame.File
131159
mod := getGoModuleName()
132160
if mod != "" {

0 commit comments

Comments
 (0)