Skip to content

Latest commit

 

History

History
429 lines (297 loc) · 15.9 KB

File metadata and controls

429 lines (297 loc) · 15.9 KB

Rad Args Library Specification

Overview

Rad Args is a Go library for CLI argument parsing that provides flexible positional and flag-based argument handling with support for subcommands, constraints, and various data types. It is designed to be user-friendly by default, with clear help messages and exit-on-error behavior.

Core Architecture

Command Structure

  • Cmd: Central structure representing a command or subcommand. It holds flag definitions and subcommands.
  • Flags: Named arguments that can be passed positionally or as flags.
  • Subcommands: Nested commands with their own argument sets.
  • Global Flags: Flags inherited by all subcommands.
  • ParseOpt: A functional option for configuring parsing behavior.

Flag Types

Non-Slice Types

  • BoolFlag: Boolean values (true/false).
  • StringFlag: String values with optional enum/regex constraints.
  • IntFlag: Integer values with optional min/max constraints.
  • Int64Flag: Int64 values with optional min/max constraints.
  • Float64Flag: Float values with optional min/max constraints.

Slice Types

  • BoolSliceFlag: Array of bools.
  • StringSliceFlag: Array of strings.
  • IntSliceFlag: Array of integers.
  • Int64SliceFlag: Array of int64s.
  • Float64SliceFlag: Array of float64s.

Flag Configuration

Common Properties

All flags support:

  • Name: Primary identifier (required).
  • Short: Single character short flag (optional).
  • Usage: Help text description.
  • Default: Default value when not specified.
  • Optional: Whether the flag is required (default: false).
  • Hidden: When true, the flag is omitted from all help output (default: false).
  • HiddenInShortHelp: When true, the flag is omitted from short help (-h) but still shown in long help (--help) (default: false).
  • PositionalOnly: Flag can only be passed positionally.
  • FlagOnly: Flag can only be passed as a named flag.

Relational Constraints

  • Excludes: Flags that cannot be used together with this flag. Works one-way like Requires - only the flag declaring the exclusion needs to specify the relationship.
  • Requires: Flags that must be present when this flag is used.

Exclusion and Required Flag Interaction

When a flag excludes another flag, and both flags are required, the exclusion takes precedence over the requirement. This means:

  • If flag A excludes flag B, and both are required, providing flag A makes flag B no longer required.
  • If neither flag is provided, both flags will be reported as missing required arguments.
  • If both flags are provided, an exclusion error will be raised.

Example:

// Both flags are required and mutually exclusive
fileFlag, _ := NewString("file").SetExcludes([]string{"url"}).Register(cmd)
urlFlag, _ := NewString("url").SetExcludes([]string{"file"}).Register(cmd)

// Valid: Only file provided (url is no longer required due to exclusion)
cmd.ParseOrError([]string{"--file", "input.txt"})

// Valid: Only url provided (file is no longer required due to exclusion)  
cmd.ParseOrError([]string{"--url", "https://example.com"})

// Invalid: Both provided (exclusion violation)
cmd.ParseOrError([]string{"--file", "input.txt", "--url", "https://example.com"})

// Invalid: Neither provided (both missing required arguments)
cmd.ParseOrError([]string{})

Value Constraints

  • EnumConstraint (string): Restricts value to a specific set.
  • RegexConstraint (string): Restricts value to match a regex pattern.
  • Min/Max (numeric): Restricts value to a minimum or maximum.

Slice Flag Options

  • Separator: Character to split a single argument into multiple values.
  • Variadic: Consume multiple consecutive arguments until the next flag.

Argument Parsing

The library provides two primary methods for parsing arguments:

// Parses args, printing usage and exiting on error.
func (c *Cmd) ParseOrExit(args []string, opts ...ParseOpt)

// Parses args, returning a ParseError on failure.
func (c *Cmd) ParseOrError(args []string, opts ...ParseOpt) *ParseError

Parse Options

Parsing behavior can be customized using functional options (ParseOpt):

  • WithIgnoreUnknown(bool): If true, unknown flags and arguments are collected (retrievable via GetUnknownArgs()) instead of causing a parsing error.

Positional Arguments

  • All flags can be passed positionally unless marked as FlagOnly.
  • Unless explicitly marked as PositionalOnly, all flags are also available as named flags (e.g., --flag-name).
  • Positional assignment is left-to-right based on registration order.
  • If a flag is set via a named flag, it's skipped in positional assignment.

Flag Precedence

  • Named flags override positional values.
  • Later occurrences override earlier ones.

Number Shorts Mode

  • Activated when any IntFlag has a short name.
  • Applies per-command (including inherited global flags).
  • In this mode, standalone negative numbers are treated as short flags.
  • To pass negative integers, use --flag=-5 syntax.

Slice Flag Parsing

  • Multiple occurrences: --flag value1 --flag value2["value1", "value2"]
  • Separator: --flag "value1,value2" with separator "," → ["value1", "value2"]
  • Variadic: --flag value1 value2["value1", "value2"] (stops at next flag)
  • Combined: Variadic + separator processes both mechanisms.

Bool Flag Clustering

  • Multiple bool shorts can be clustered: -abc is equivalent to -a -b -c.
  • A non-bool flag can terminate a cluster: -abc value (where c is non-bool).

Commands and Subcommands

Command Registration

cmd := NewCmd("mycmd")
subCmd := NewCmd("subcmd")
invoked, err := cmd.RegisterCmd(subCmd)

Subcommand Parsing

  • Subcommands must appear immediately after the parent command.
  • The first positional argument matching a subcommand name invokes that subcommand.
  • If no match is found, parsing continues with the parent command's arguments.

Global Flags

  • Registered with WithGlobal(true) option.
  • Automatically inherited by all subcommands.
  • Global flags preserve their state across subcommand parsing.

Configuration Options

Command Options

  • SetDescription(string): Sets a description for the command, shown at the top of the help text.
  • SetCustomUsage(func(isLongHelp bool)): Overrides the entire usage generation logic.
  • SetHelpEnabled(bool): Disables the automatic registration of -h/--help flags if set to false.
  • SetHidden(bool): When true, the command is omitted from the parent's help output entirely (both -h and --help) and from shell completion. The command remains fully invocable by name.
  • SetHiddenInShortHelp(bool): When true, the command is omitted from short help (-h) but still listed in long help (--help). It remains available in shell completion.
  • SetAutoHelpOnNoArgs(bool): When enabled, automatically shows help (equivalent to -h) if no arguments are provided and the command has required flags. This provides a user-friendly experience when users run a command without arguments to see what options are available.

Auto-Help on No Arguments

When SetAutoHelpOnNoArgs(true) is enabled, the following behavior applies:

  • Trigger condition: No arguments provided AND command has required flags
  • Action: Show short help (-h equivalent) and exit with code 0
  • Scope: Works for both main commands and subcommands
  • Precedence: Takes precedence over "missing required arguments" errors

Example:

cmd := NewCmd("deploy")
cmd.SetAutoHelpOnNoArgs(true)

// Required flags
NewString("environment").Register(cmd)
NewString("version").Register(cmd)

// Optional flags  
NewBool("force").SetOptional(true).Register(cmd)

// Running with no args shows help instead of error
cmd.ParseOrError([]string{}) // Shows help, exits 0

// Running with some args works normally
cmd.ParseOrError([]string{"--environment", "prod", "--version", "1.2.3"}) // Parses normally

Error Handling

The library provides two parsing methods with different error handling behaviors:

  • ParseOrExit: Prints errors and usage to stderr, then exits with appropriate code (0 for help, 1 for errors)
  • ParseOrError: Returns errors for programmatic handling, never calls exit()

For testability, the library uses interface-based dependency injection for exit functionality and stderr writing, allowing for clean test mocking without race conditions.

Error Types

The library distinguishes between two categories of errors with different presentation:

Programming Errors

These are bugs in the code using the Ra library, not user input mistakes. They show only the error message (no usage):

  • Constraint validation errors: Constraints referencing undefined flags (e.g., SetRequires([]string{"nonexistent"}))
  • Unsupported flag type errors: Internal programming bugs where a new flag type wasn't fully implemented

Programming errors return a *ProgrammingError type that can be detected with errors.As().

User Input Errors

These are mistakes in command-line input by end users. They show error message + usage:

  • Unknown flag
  • Missing required flag value
  • Type conversion errors
  • Constraint violations (enum, regex, min/max)
  • Missing required arguments
  • Relational constraint violations (requires/excludes logic)

Help Invoked Error

A special exported error constant returned when help/usage is displayed:

var HelpInvokedErr = errors.New("help invoked")

When returned:

  • -h or --help flags are used
  • Auto-help triggers due to no arguments + required flags

Usage:

err := cmd.ParseOrError(args)
if err == ra.HelpInvokedErr {
    // Help was shown, handle appropriately
    return
} else if err != nil {
    // Handle actual parsing error
    log.Fatal(err)
}

Behavior:

  • ParseOrError returns HelpInvokedErr without exiting
  • ParseOrExit detects help invocation, outputs help text, and exits with code 0

This provides clean separation between help invocation and actual parsing errors.

State Management

Parsing State

  • Configured(name): Returns true only if the user explicitly provided the flag.
  • GetUnknownArgs(): Returns unrecognized arguments when WithIgnoreUnknown(true) is used.
  • used: A per-command boolean indicating if a subcommand was invoked.

Default Behavior

  • If a flag has no default and is not marked Optional, parsing errors if it's not provided.
  • If a default is set, Configured(name) returns false when the default value is used.
  • Optional flags without defaults get the zero value of their type.

Registration and Type Safety

Flag Registration

// Register with a returned pointer
flagPtr, err := NewString("name").Register(cmd)

// Register with an existing pointer
err := NewString("name").RegisterWithPtr(cmd, existingPtr)

// Register as a global flag
err := NewString("name").RegisterWithPtr(cmd, ptr, WithGlobal(true))

Validation During Registration

  • Flag names must be unique within a command.
  • Default values must satisfy any defined constraints (enum, regex, min/max).
  • Constraint violations return an error during registration.
  • Global flag registration propagates to subcommands.

Type Safety

  • Generic Flag[T] and SliceFlag[T] structures ensure type safety.
  • Registration validates that flag names are unique.

Usage Generation

Help Flags

When helpEnabled is true (the default), two help flags are automatically registered:

  • -h: Triggers the "short help" output.
  • --help: Triggers the "long help" output.

The only difference between short and long help is that flags and commands marked HiddenInShortHelp are excluded from the short help output but included in the long help output.

Flag Ordering in Usage

Flags appear in usage output in the order they were registered, not alphabetically. This preserves the logical ordering that developers choose when defining their CLI interface.

Default Usage Format

The generated usage string follows a structured format:

<description>

Usage:
  <synopsis>

Script args:
  <flags...>

Global options:
  <global flags...>

Example Output:

A rad-powered recreation of 'um', with the help of 'tldr'.
Allows you to check the tldr for commands, but then also
add your own notes and customize the notes in their own
entries.

Usage:
  hm <task> [OPTIONS]

Script args:
      --task str
  -e, --edit
  -l, --list        Lists stored entries. Exits after.
      --reconfigure Enable to reconfigure hm.

Global options:
  -d, --debug       Enables debug output. Intended for Rad script developers.
      --color str   Control output colorization. Valid values: [auto, always, never]. (default auto)
  -q, --quiet       Suppresses some output.
      --confirm-shell Confirm all shell commands before running them.
  -h, --help        Print usage string.

Generating Usage Manually

The following methods can be used to manually generate usage strings:

  • GenerateShortUsage() string
  • GenerateLongUsage() string

Individual Usage Chunks

Ra also provides methods to generate individual sections of the usage output, allowing for custom composition:

Description Section:

  • GenerateDescription() string - Returns the command description with trailing newlines (empty string if no description)

Synopsis Section:

  • GenerateSynopsis(isLongHelp bool) string - Returns the command synopsis line
  • GenerateShortSynopsis() string - Convenience method equivalent to GenerateSynopsis(false)
  • GenerateLongSynopsis() string - Convenience method equivalent to GenerateSynopsis(true)

Commands Section:

  • GenerateCommandsSection(isLongHelp bool) string - Returns the subcommands section with header (empty string if no subcommands)
  • GenerateShortCommandsSection() string - Convenience method equivalent to GenerateCommandsSection(false)
  • GenerateLongCommandsSection() string - Convenience method equivalent to GenerateCommandsSection(true)

Arguments Section:

  • GenerateArgumentsSection(isLongHelp bool) string - Returns the script-level arguments section with header (empty string if no visible arguments)
  • GenerateShortArgumentsSection() string - Convenience method equivalent to GenerateArgumentsSection(false)
  • GenerateLongArgumentsSection() string - Convenience method equivalent to GenerateArgumentsSection(true)

Global Options Section:

  • GenerateGlobalOptionsSection(isLongHelp bool) string - Returns the global options section with header (empty string if no visible global options)
  • GenerateShortGlobalOptionsSection() string - Convenience method equivalent to GenerateGlobalOptionsSection(false)
  • GenerateLongGlobalOptionsSection() string - Convenience method equivalent to GenerateGlobalOptionsSection(true)

Example Usage:

// Generate only the synopsis
synopsis := cmd.GenerateShortSynopsis()
fmt.Println(synopsis) // Output: myapp <input> [OPTIONS]

// Create custom usage by combining sections
var customUsage strings.Builder
customUsage.WriteString("My Custom Usage:\n")
customUsage.WriteString(cmd.GenerateShortSynopsis())
customUsage.WriteString("\n")
customUsage.WriteString(cmd.GenerateShortArgumentsSection())

Note: Individual chunk methods respect the same visibility rules as the full usage generation (hidden flags, short vs. long help, etc.).

Custom Usage

  • Override via SetCustomUsage(func(isLongHelp bool)). The boolean parameter indicates whether long help (--help) was requested.
  • Inside the custom function, you can call GenerateShortUsage/GenerateLongUsage to build upon the default output. The *Cmd instance must be captured in a closure by the user if it's needed.

Thread Safety and Re-parsing

Limitations

  • Not designed for concurrent access to same Cmd instance.
  • Not designed for repeated parsing of same Cmd instance.

Best Practices

  • Create a new Cmd instance for each parse operation.
  • Use separate Cmd instances for concurrent parsing.

Misc

  • The library must behave deterministically.