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/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/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 5f3cc38..c35cf22 100644 --- a/run.go +++ b/run.go @@ -49,6 +49,34 @@ 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) +// } +// +// 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 { + if err := Parse(root, args); err != nil { + if errors.Is(err, 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 +149,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 != "" {