From 6e6bb4d2024cf5f68a297dc495802a32f2204197 Mon Sep 17 00:00:00 2001 From: Mike Fridman Date: Mon, 16 Feb 2026 14:32:04 -0500 Subject: [PATCH 1/4] feat: add ParseAndRun convenience function --- examples/cmd/echo/main.go | 10 +--------- examples/cmd/task/main.go | 10 +--------- run.go | 36 +++++++++++++++++++++++++++++++----- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/examples/cmd/echo/main.go b/examples/cmd/echo/main.go index 56e81eb..ac664a8 100644 --- a/examples/cmd/echo/main.go +++ b/examples/cmd/echo/main.go @@ -36,15 +36,7 @@ func main() { return nil }, } - if err := cli.Parse(root, os.Args[1:]); err != nil { - if errors.Is(err, flag.ErrHelp) { - fmt.Fprintf(os.Stdout, "%s\n", cli.DefaultUsage(root)) - return - } - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } - if err := cli.Run(context.Background(), root, nil); err != nil { + if err := cli.ParseAndRun(context.Background(), root, os.Args[1:], nil); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } diff --git a/examples/cmd/task/main.go b/examples/cmd/task/main.go index 30c6a14..5ba23b9 100644 --- a/examples/cmd/task/main.go +++ b/examples/cmd/task/main.go @@ -50,15 +50,7 @@ func main() { }, } - if err := cli.Parse(root, os.Args[1:]); err != nil { - if errors.Is(err, flag.ErrHelp) { - fmt.Fprintf(os.Stdout, "%s\n", cli.DefaultUsage(root)) - return - } - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } - if err := cli.Run(context.Background(), root, nil); err != nil { + if err := cli.ParseAndRun(context.Background(), root, os.Args[1:], nil); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } diff --git a/run.go b/run.go index 5f3cc38..f576294 100644 --- a/run.go +++ b/run.go @@ -3,6 +3,7 @@ package cli import ( "context" "errors" + "flag" "fmt" "io" "os" @@ -49,6 +50,31 @@ func Run(ctx context.Context, root *Command, options *RunOptions) error { return run(ctx, cmd, root.state) } +// ParseAndRun is a convenience function that combines [Parse] and [Run] into a single call. It +// parses the command hierarchy, handles help flags automatically (printing usage to stdout and +// returning nil), and then executes the resolved command. +// +// This is the recommended entry point for most CLI applications: +// +// if err := cli.ParseAndRun(ctx, root, os.Args[1:], nil); err != nil { +// fmt.Fprintf(os.Stderr, "error: %v\n", err) +// os.Exit(1) +// } +// +// For applications that need to perform work between parsing and execution (e.g., initializing +// resources based on parsed flags), use [Parse] and [Run] separately. +func ParseAndRun(ctx context.Context, root *Command, args []string, options *RunOptions) error { + if err := Parse(root, args); err != nil { + if errors.Is(err, flag.ErrHelp) { + options = checkAndSetRunOptions(options) + fmt.Fprintln(options.Stdout, DefaultUsage(root)) + return nil + } + return err + } + return Run(ctx, root, options) +} + func run(ctx context.Context, cmd *Command, state *State) (retErr error) { defer func() { if r := recover(); r != nil { @@ -121,12 +147,12 @@ func location(skip int) string { frame, _ := runtime.CallersFrames(pcs[:n]).Next() - // Trim the module name from function and file paths for cleaner output. - // Function names use the module path directly (e.g., "github.com/pressly/cli.Run"). + // Trim the module name from function and file paths for cleaner output. Function names use the + // module path directly (e.g., "github.com/pressly/cli.Run"). fn := strings.TrimPrefix(frame.Function, getGoModuleName()+"/") - // File paths from runtime are absolute (e.g., "/Users/.../cli/run.go"). We want a relative - // path for cleaner output. Try to find the module's import path in the filesystem path - // (works with GOPATH-style layouts), otherwise fall back to just the base filename. + // File paths from runtime are absolute (e.g., "/Users/.../cli/run.go"). We want a relative path + // for cleaner output. Try to find the module's import path in the filesystem path (works with + // GOPATH-style layouts), otherwise fall back to just the base filename. file := frame.File mod := getGoModuleName() if mod != "" { From 0fd37ee9be9e39897fb196e7be58d1a53ef204c2 Mon Sep 17 00:00:00 2001 From: Mike Fridman Date: Mon, 16 Feb 2026 14:33:57 -0500 Subject: [PATCH 2/4] godoc --- run.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/run.go b/run.go index f576294..179ab29 100644 --- a/run.go +++ b/run.go @@ -61,6 +61,9 @@ func Run(ctx context.Context, root *Command, options *RunOptions) error { // os.Exit(1) // } // +// The options parameter may be nil, in which case default values are used. See [RunOptions] for +// more details. +// // For applications that need to perform work between parsing and execution (e.g., initializing // resources based on parsed flags), use [Parse] and [Run] separately. func ParseAndRun(ctx context.Context, root *Command, args []string, options *RunOptions) error { From d01d2c966760e6ea91b1e6a2a56cfde0e67c1aa2 Mon Sep 17 00:00:00 2001 From: Mike Fridman Date: Mon, 16 Feb 2026 14:37:01 -0500 Subject: [PATCH 3/4] lint --- run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run.go b/run.go index 179ab29..4f0d185 100644 --- a/run.go +++ b/run.go @@ -70,7 +70,7 @@ func ParseAndRun(ctx context.Context, root *Command, args []string, options *Run if err := Parse(root, args); err != nil { if errors.Is(err, flag.ErrHelp) { options = checkAndSetRunOptions(options) - fmt.Fprintln(options.Stdout, DefaultUsage(root)) + _, _ = fmt.Fprintln(options.Stdout, DefaultUsage(root)) return nil } return err From 68b2bacc09b4d6a348f1336e4bfecc0a79c619bb Mon Sep 17 00:00:00 2001 From: Mike Fridman Date: Mon, 16 Feb 2026 14:40:55 -0500 Subject: [PATCH 4/4] feat: export ErrHelp so callers don't need to import flag --- command.go | 7 +++++++ parse.go | 2 +- run.go | 3 +-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/command.go b/command.go index 17df7a2..31d1613 100644 --- a/command.go +++ b/command.go @@ -9,6 +9,13 @@ import ( "github.com/pressly/cli/pkg/suggest" ) +// ErrHelp is returned by [Parse] when the -help or -h flag is invoked. It is identical to +// [flag.ErrHelp] but re-exported here so callers using [Parse] and [Run] separately do not need to +// import the flag package solely for error checking. +// +// Note: [ParseAndRun] handles this automatically and never surfaces ErrHelp to the caller. +var ErrHelp = flag.ErrHelp + // Command represents a CLI command or subcommand within the application's command hierarchy. type Command struct { // Name is always a single word representing the command's name. It is used to identify the diff --git a/parse.go b/parse.go index b8efd54..3b35307 100644 --- a/parse.go +++ b/parse.go @@ -50,7 +50,7 @@ func Parse(root *Command, args []string) error { if arg == "-h" || arg == "--h" || arg == "-help" || arg == "--help" { // Combine flags first so the help message includes all inherited flags combineFlags(root.state.path) - return flag.ErrHelp + return ErrHelp } } diff --git a/run.go b/run.go index 4f0d185..c35cf22 100644 --- a/run.go +++ b/run.go @@ -3,7 +3,6 @@ package cli import ( "context" "errors" - "flag" "fmt" "io" "os" @@ -68,7 +67,7 @@ func Run(ctx context.Context, root *Command, options *RunOptions) error { // resources based on parsed flags), use [Parse] and [Run] separately. func ParseAndRun(ctx context.Context, root *Command, args []string, options *RunOptions) error { if err := Parse(root, args); err != nil { - if errors.Is(err, flag.ErrHelp) { + if errors.Is(err, ErrHelp) { options = checkAndSetRunOptions(options) _, _ = fmt.Fprintln(options.Stdout, DefaultUsage(root)) return nil