Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 1 addition & 9 deletions examples/cmd/echo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
10 changes: 1 addition & 9 deletions examples/cmd/task/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down
38 changes: 33 additions & 5 deletions run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 != "" {
Expand Down