From e368325db6aa32474559c08546934b9ecb296a86 Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Sun, 12 Apr 2026 00:27:50 +0200 Subject: [PATCH 1/5] Auto dispatch + extended mgit example --- README.md | 1 + docs/argmojo_overall_planning.md | 18 +- docs/changelog.md | 1 + docs/user_manual.md | 364 ++++++++++++------- examples/build.sh | 134 +++++++ examples/mgit.mojo | 591 +++++++++++++++++++++++++++++-- pixi.toml | 19 +- src/argmojo/command.mojo | 148 ++++++++ tests/test_dispatch.mojo | 299 ++++++++++++++++ 9 files changed, 1401 insertions(+), 174 deletions(-) create mode 100755 examples/build.sh create mode 100644 tests/test_dispatch.mojo diff --git a/README.md b/README.md index 7f3465f..8a5b960 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ ArgMojo currently supports: - **Partial parsing**: `parse_known_arguments()` collects unrecognised options instead of erroring - **Compile-time validation**: builder parameters validated at `mojo build` time via `comptime assert` - **Registration-time validation**: group constraint typos caught when the program starts, not when the user runs it +- **Auto-dispatch**: `set_run_function(handler)` + `execute()` for Cobra-style automatic subcommand dispatch — no manual `if/elif` chains --- diff --git a/docs/argmojo_overall_planning.md b/docs/argmojo_overall_planning.md index 0572c58..998e4a5 100644 --- a/docs/argmojo_overall_planning.md +++ b/docs/argmojo_overall_planning.md @@ -85,7 +85,8 @@ These features appear across multiple libraries and depend only on string operat | Typed retrieval (`get_int()` etc.) | ✓ | ✓ | ✓ | ✓ | ✓ | | **Done** | | Comptime `StringLiteral` params | — | — | — | ✓ | ✓ | clap derive macros | **Done** | | Registration-time name validation | — | — | — | ✓ | ✓ | clap panic on unknown ID | **Done** | -| Struct-based schema (reflection) | — | — | — | — | ✓ | swift-argument-parser | Phase 7a | +| Struct-based schema (reflection) | — | — | — | — | ✓ | swift-argument-parser | **Done** | +| Auto-dispatch (run functions) | — | — | ✓ | — | ✓ | Cobra / swift | **Done** | | Pre/Post run hooks | — | — | ✓ | — | — | Cobra | Phase 7a | | Derive (macro/decorator-based) | — | — | — | ✓ | — | clap `#[derive(Parser)]` | Phase 7b | | Enum → type mapping (real enums) | — | — | — | ✓ | ✓ | Requires reflection | Phase 7b | @@ -569,7 +570,7 @@ Before adding Phase 5 features, further decompose `parse_arguments()` for readab - [x] **Interactive prompting** — prompt user for missing required args instead of erroring (Click `prompt=True`) (PR #23) - [x] **Password / masked input** — hide typed characters for sensitive values (Click `hide_input=True`) - [x] **Confirmation option** — `confirmation_option()` or `confirmation_option["prompt"]()` auto-registers `--yes`/`-y` flag; prompts user for confirmation after parsing; aborts on decline or non-interactive stdin (Click `confirmation_option`) (PR #26) -- [ ] **Pre/Post run hooks** — callbacks before/after main logic (cobra `PreRun`/`PostRun`) +- [x] **Pre/Post run hooks** — **Auto-dispatch implemented:** `set_run_function(handler)` registers a `def (ParseResult) raises` function pointer on a `Command`; `execute()` parses and walks the subcommand chain to invoke the matching handler; `_execute_with_arguments(args)` provides the same dispatch for testing. **Lifecycle hooks** (PersistentPreRun/PreRun/PostRun/PersistentPostRun) are not yet implemented — they depend on auto-dispatch and will be added in a future release. - [x] **Remainder positional** — `.remainder()` consumes ALL remaining tokens (including `-` prefixed); at most one per command, must be last positional (argparse `nargs=REMAINDER`, clap `trailing_var_arg`) (PR #13) - [x] **Allow hyphen values** — `.allow_hyphen_values()` on positional accepts dash-prefixed tokens as values without `--`; remainder enables this automatically (clap `allow_hyphen_values`) (PR #13) - [ ] **Regex validation** — `.pattern(r"^\d{4}-\d{2}-\d{2}$")` validates value format (no major library has this) @@ -679,10 +680,11 @@ The features below are **not part of the core builder API**. They are split into These features use capabilities already available in Mojo 0.26.2 and can be experimented with immediately. -| Feature | Inspiration | Status | Planning doc | -| ------------------------------ | -------------------------- | ------------- | ---------------------------------------------------------- | -| Declarative / struct-based API | swift-argument-parser | Investigating | [declarative_api_planning.md](declarative_api_planning.md) | -| Pre/Post run hooks | cobra `PreRun` / `PostRun` | Investigating | TBD | +| Feature | Inspiration | Status | Planning doc | +| ------------------------------ | --------------------------- | -------- | ---------------------------------------------------------- | +| Declarative / struct-based API | swift-argument-parser | **Done** | [declarative_api_planning.md](declarative_api_planning.md) | +| Auto-dispatch (run functions) | cobra `Run` / swift `run()` | **Done** | Implemented in command.mojo | +| Pre/Post lifecycle hooks | cobra `PreRun` / `PostRun` | Planned | Depends on auto-dispatch (now available) | **Declarative API summary** (see [full design doc](declarative_api_planning.md)): @@ -691,7 +693,9 @@ These features use capabilities already available in Mojo 0.26.2 and can be expe - **Two innovations beyond Swift**: (1) `to_command()` exposes the underlying `Command` for builder-level tweaks (groups, implications, coloured help); (2) `parse_full()` returns both typed struct + `ParseResult` for hybrid workflows. - **Optional** — Users who prefer the builder API are completely unaffected. Zero change to existing code. -**Pre/Post run hooks** — Straightforward callback mechanism (`def(ParseResult) raises`). No special language features needed; just needs API design and a decision on execution order with subcommands. +**Auto-dispatch** — Implemented via `set_run_function()` + `execute()`. Registers a non-capturing function pointer (`def (ParseResult) raises`) on each `Command`. `execute()` parses `sys.argv()`, walks the subcommand chain, and invokes the leaf handler. `_execute_with_arguments(args)` provides the same dispatch for testing with explicit argument lists. Works with aliases, nested subcommands, and persistent flags. Closures cannot be stored as struct fields in Mojo 0.26.2 (only non-capturing function pointers via the `def (...) raises` type), so handlers must be free functions. + +**Pre/Post lifecycle hooks** — Now unblocked by auto-dispatch. Straightforward extension: add `_pre_run_function` / `_post_run_function` fields with the same function pointer type. Execution order: PersistentPreRun → PreRun → Run → PostRun → PersistentPostRun. Will be implemented in a future release. #### Phase 7b: Blocked on Mojo Language Features diff --git a/docs/changelog.md b/docs/changelog.md index b905920..0e10db3 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -6,6 +6,7 @@ This document tracks all notable changes to ArgMojo, including new features, API Unreleased changes should be commented out from here. This file will be edited just before each release to reflect the final changelog for that version. Otherwise, the users would be confused. - Add `allow_negative_expressions()` on `Command` — treats single-hyphen tokens as positional arguments when they don't conflict with registered short options. Handles mathematical expressions like `-1/3*pi`, `-sin(2)`, `-e^2`. Superset of `allow_negative_numbers()`. +- Add **auto-dispatch** — `set_run_function(handler)` registers a `def (ParseResult) raises` handler on a `Command`; `execute()` parses and auto-dispatches to the matching handler, eliminating manual `if/elif` subcommand chains. `_execute_with_arguments(args)` provides the same dispatch for testing with explicit argument lists. Works with nested subcommands, aliases, and persistent flags. --> ## 20260404 (v0.5.0) diff --git a/docs/user_manual.md b/docs/user_manual.md index dda4f83..1659f12 100644 --- a/docs/user_manual.md +++ b/docs/user_manual.md @@ -8,126 +8,6 @@ All code examples below assume that you have imported the mojo at the top of you from argmojo import Argument, Command ``` -- [Getting Started](#getting-started) - - [Creating a Command](#creating-a-command) - - [Reading Parsed Results](#reading-parsed-results) -- [Defining Arguments](#defining-arguments) - - [Positional Arguments](#positional-arguments) - - [Long Options](#long-options) - - [Short Options](#short-options) - - [Boolean Flags](#boolean-flags) - - [Default Values](#default-values) - - [Required Arguments](#required-arguments) - - [Aliases](#aliases) -- [Builder Method Compatibility](#builder-method-compatibility) - - [ASCII Tree](#ascii-tree) - - [Compatibility Table](#compatibility-table) -- [Short Option Details](#short-option-details) - - [Short Flag Merging](#short-flag-merging) - - [Attached Short Values](#attached-short-values) -- [Flag Variants](#flag-variants) - - [Count Flags](#count-flags) - - [Count Ceiling (`.max[N]()`)](#count-ceiling-maxn) - - [Negatable Flags](#negatable-flags) -- [Collecting Multiple Values](#collecting-multiple-values) - - [Append / Collect Action](#append--collect-action) - - [Value Delimiter](#value-delimiter) - - [Multi-Value Options (nargs)](#multi-value-options-nargs) - - [Key-Value Map Options](#key-value-map-options) -- [Value Validation](#value-validation) - - [Choices Validation](#choices-validation) - - [Positional Argument Count Validation](#positional-argument-count-validation) - - [Numeric Range Validation](#numeric-range-validation) - - [Range Clamping (`.clamp()`)](#range-clamping-clamp) -- [Group Constraints](#group-constraints) - - [Mutually Exclusive Groups](#mutually-exclusive-groups) - - [One-Required Groups](#one-required-groups) - - [Required-Together Groups](#required-together-groups) - - [Conditional Requirements](#conditional-requirements) - - [Mutual Implication](#mutual-implication) -- [Subcommands](#subcommands) - - [Defining Subcommands](#defining-subcommands) - - [Parsing Subcommand Results](#parsing-subcommand-results) - - [Persistent (Global) Flags](#persistent-global-flags) - - [The help Subcommand](#the-help-subcommand) - - [Subcommand Aliases](#subcommand-aliases) - - [Unknown Subcommand Error](#unknown-subcommand-error) - - [Hidden Subcommands](#hidden-subcommands) - - [Mixing Positional Args with Subcommands](#mixing-positional-args-with-subcommands) -- [Help \& Display](#help--display) - - [Value Name](#value-name) - - [Hidden Arguments](#hidden-arguments) - - [Deprecated Arguments](#deprecated-arguments) - - [Default-if-no-value](#default-if-no-value) - - [Require Equals Syntax](#require-equals-syntax) - - [Argument Groups](#argument-groups) - - [Auto-generated Help](#auto-generated-help) - - [Custom Tips](#custom-tips) - - [Version Display](#version-display) - - [CJK-Aware Help Alignment](#cjk-aware-help-alignment) - - [Full-Width → Half-Width Auto-Correction](#full-width--half-width-auto-correction) -- [Parsing Behaviour](#parsing-behaviour) - - [Negative Number Passthrough](#negative-number-passthrough) - - [Long Option Prefix Matching](#long-option-prefix-matching) - - [The `--` Stop Marker](#the----stop-marker) - - [Remainder Positional (`.remainder()`)](#remainder-positional-remainder) - - [Allow Hyphen Values (`.allow_hyphen_values()`)](#allow-hyphen-values-allow_hyphen_values) - - [Partial Parsing (`parse_known_arguments()`)](#partial-parsing-parse_known_arguments) -- [Interactive Prompting](#interactive-prompting) - - [Setup Example](#setup-example) - - [Enabling Prompting](#enabling-prompting) - - [Interactive Session Examples](#interactive-session-examples) - - [All arguments missing — full prompting](#all-arguments-missing--full-prompting) - - [Partial arguments — only missing ones are prompted](#partial-arguments--only-missing-ones-are-prompted) - - [All arguments provided — no prompting at all](#all-arguments-provided--no-prompting-at-all) - - [Empty input with a default — default value is used](#empty-input-with-a-default--default-value-is-used) - - [Flag argument — y/n prompt](#flag-argument--yn-prompt) - - [Argument with choices — choices are shown](#argument-with-choices--choices-are-shown) - - [Prompt Format](#prompt-format) - - [Interaction with Other Features](#interaction-with-other-features) - - [Non-Interactive Use (CI / Piped Input)](#non-interactive-use-ci--piped-input) -- [Argument Parents and Inheritance](#argument-parents-and-inheritance) - - [Defining Shared Arguments](#defining-shared-arguments) - - [What Gets Inherited](#what-gets-inherited) - - [Multiple Parents](#multiple-parents) - - [Using with Subcommands](#using-with-subcommands) - - [Notes](#notes) -- [Password / Masked Input](#password--masked-input) - - [Basic Usage](#basic-usage) - - [Custom Prompt Text](#custom-prompt-text) - - [Restrictions](#restrictions) - - [Non-Interactive Use](#non-interactive-use) -- [Confirmation Option](#confirmation-option) - - [Basic Usage](#basic-usage-1) - - [Custom Prompt Text](#custom-prompt-text-1) - - [Using with Subcommands](#using-with-subcommands-1) - - [Non-Interactive Use](#non-interactive-use-1) -- [Usage Line Customisation](#usage-line-customisation) -- [Shell Completion](#shell-completion) - - [Built-in `--completions` Flag](#built-in---completions-flag) - - [Disabling the Built-in Flag](#disabling-the-built-in-flag) - - [Customising the Trigger Name](#customising-the-trigger-name) - - [Using a Subcommand Instead of an Option](#using-a-subcommand-instead-of-an-option) - - [Generating a Script Programmatically](#generating-a-script-programmatically) - - [Installing Completions](#installing-completions) - - [What Gets Completed](#what-gets-completed) -- [Developer Validation](#developer-validation) - - [Compile-Time Validation](#compile-time-validation) - - [Runtime Registration Validation](#runtime-registration-validation) - - [Recommended Workflow](#recommended-workflow) -- [Declarative API (Struct-Based)](#declarative-api-struct-based) - - [Wrapper Types](#wrapper-types) - - [The `Parsable` Trait](#the-parsable-trait) - - [Pure Declarative — One-Line Parse](#pure-declarative--one-line-parse) - - [Hybrid — Declarative + Builder Customisation](#hybrid--declarative--builder-customisation) - - [Full Parse — Declarative + Extra Builder Fields](#full-parse--declarative--extra-builder-fields) - - [Subcommands](#subcommands-1) - - [Auto-Naming Convention](#auto-naming-convention) - - [API Summary](#api-summary) -- [Cross-Library Method Name Reference](#cross-library-method-name-reference) - - [Argument-Level Builder Methods](#argument-level-builder-methods) - - [Command-Level Constraint Methods](#command-level-constraint-methods) - - [Notes](#notes-1) +- [Getting Started](#getting-started) + - [Creating a Command](#creating-a-command) + - [Reading Parsed Results](#reading-parsed-results) +- [Defining Arguments](#defining-arguments) + - [Positional Arguments](#positional-arguments) + - [Long Options](#long-options) + - [Short Options](#short-options) + - [Boolean Flags](#boolean-flags) + - [Default Values](#default-values) + - [Required Arguments](#required-arguments) + - [Aliases](#aliases) +- [Builder Method Compatibility](#builder-method-compatibility) + - [ASCII Tree](#ascii-tree) + - [Compatibility Table](#compatibility-table) +- [Short Option Details](#short-option-details) + - [Short Flag Merging](#short-flag-merging) + - [Attached Short Values](#attached-short-values) +- [Flag Variants](#flag-variants) + - [Count Flags](#count-flags) + - [Count Ceiling (`.max[N]()`)](#count-ceiling-maxn) + - [Negatable Flags](#negatable-flags) +- [Collecting Multiple Values](#collecting-multiple-values) + - [Append / Collect Action](#append--collect-action) + - [Value Delimiter](#value-delimiter) + - [Multi-Value Options (nargs)](#multi-value-options-nargs) + - [Key-Value Map Options](#key-value-map-options) +- [Value Validation](#value-validation) + - [Choices Validation](#choices-validation) + - [Positional Argument Count Validation](#positional-argument-count-validation) + - [Numeric Range Validation](#numeric-range-validation) + - [Range Clamping (`.clamp()`)](#range-clamping-clamp) +- [Group Constraints](#group-constraints) + - [Mutually Exclusive Groups](#mutually-exclusive-groups) + - [One-Required Groups](#one-required-groups) + - [Required-Together Groups](#required-together-groups) + - [Conditional Requirements](#conditional-requirements) + - [Mutual Implication](#mutual-implication) +- [Subcommands](#subcommands) + - [Defining Subcommands](#defining-subcommands) + - [Parsing Subcommand Results](#parsing-subcommand-results) + - [Auto-Dispatch with `set_run_function` / `execute`](#auto-dispatch-with-set_run_function--execute) + - [Persistent (Global) Flags](#persistent-global-flags) + - [The help Subcommand](#the-help-subcommand) + - [Subcommand Aliases](#subcommand-aliases) + - [Unknown Subcommand Error](#unknown-subcommand-error) + - [Hidden Subcommands](#hidden-subcommands) + - [Mixing Positional Args with Subcommands](#mixing-positional-args-with-subcommands) +- [Help \& Display](#help--display) + - [Value Name](#value-name) + - [Hidden Arguments](#hidden-arguments) + - [Deprecated Arguments](#deprecated-arguments) + - [Default-if-no-value](#default-if-no-value) + - [Require Equals Syntax](#require-equals-syntax) + - [Argument Groups](#argument-groups) + - [Auto-generated Help](#auto-generated-help) + - [Custom Tips](#custom-tips) + - [Version Display](#version-display) + - [CJK-Aware Help Alignment](#cjk-aware-help-alignment) + - [Full-Width → Half-Width Auto-Correction](#full-width--half-width-auto-correction) +- [Parsing Behaviour](#parsing-behaviour) + - [Negative Number Passthrough](#negative-number-passthrough) + - [Long Option Prefix Matching](#long-option-prefix-matching) + - [The `--` Stop Marker](#the----stop-marker) + - [Remainder Positional (`.remainder()`)](#remainder-positional-remainder) + - [Allow Hyphen Values (`.allow_hyphen_values()`)](#allow-hyphen-values-allow_hyphen_values) + - [Partial Parsing (`parse_known_arguments()`)](#partial-parsing-parse_known_arguments) +- [Interactive Prompting](#interactive-prompting) + - [Setup Example](#setup-example) + - [Enabling Prompting](#enabling-prompting) + - [Interactive Session Examples](#interactive-session-examples) + - [Prompt Format](#prompt-format) + - [Interaction with Other Features](#interaction-with-other-features) + - [Non-Interactive Use (CI / Piped Input)](#non-interactive-use-ci--piped-input) +- [Argument Parents and Inheritance](#argument-parents-and-inheritance) + - [Defining Shared Arguments](#defining-shared-arguments) + - [What Gets Inherited](#what-gets-inherited) + - [Multiple Parents](#multiple-parents) + - [Using with Subcommands](#using-with-subcommands) + - [Notes](#notes) +- [Password / Masked Input](#password--masked-input) + - [Basic Usage](#basic-usage) + - [Custom Prompt Text](#custom-prompt-text) + - [Restrictions](#restrictions) + - [Non-Interactive Use](#non-interactive-use) +- [Confirmation Option](#confirmation-option) + - [Basic Usage](#basic-usage-1) + - [Custom Prompt Text](#custom-prompt-text-1) + - [Using with Subcommands](#using-with-subcommands-1) + - [Non-Interactive Use](#non-interactive-use-1) +- [Usage Line Customisation](#usage-line-customisation) +- [Shell Completion](#shell-completion) + - [Built-in `--completions` Flag](#built-in---completions-flag) + - [Disabling the Built-in Flag](#disabling-the-built-in-flag) + - [Customising the Trigger Name](#customising-the-trigger-name) + - [Using a Subcommand Instead of an Option](#using-a-subcommand-instead-of-an-option) + - [Generating a Script Programmatically](#generating-a-script-programmatically) + - [Installing Completions](#installing-completions) + - [What Gets Completed](#what-gets-completed) +- [Developer Validation](#developer-validation) + - [Compile-Time Validation](#compile-time-validation) + - [Runtime Registration Validation](#runtime-registration-validation) + - [Recommended Workflow](#recommended-workflow) +- [Declarative API (Struct-Based)](#declarative-api-struct-based) + - [Wrapper Types](#wrapper-types) + - [The `Parsable` Trait](#the-parsable-trait) + - [Pure Declarative — One-Line Parse](#pure-declarative--one-line-parse) + - [Hybrid — Declarative + Builder Customisation](#hybrid--declarative--builder-customisation) + - [Full Parse — Declarative + Extra Builder Fields](#full-parse--declarative--extra-builder-fields) + - [Subcommands](#subcommands-1) + - [Auto-Naming Convention](#auto-naming-convention) + - [API Summary](#api-summary) +- [Cross-Library Method Name Reference](#cross-library-method-name-reference) + - [Argument-Level Builder Methods](#argument-level-builder-methods) + - [Command-Level Methods](#command-level-methods) + - [Notes](#notes-1) + + ## Getting Started @@ -1635,7 +1744,7 @@ All standard `ParseResult` methods (`get_flag()`, `get_string()`, `get_int()`, ` ### Auto-Dispatch with `set_run_function` / `execute` -#### The problem: manual subcommand routing +#### The problem: manual subcommand routing When a CLI has subcommands (like `git clone`, `git push`, `git remote add`), the `ParseResult` tells you *which* subcommand was selected — but you must route to the right handler yourself: @@ -1656,7 +1765,7 @@ elif result.subcommand == "remote": This is tedious. Every time you add a subcommand, you update the router. With nested subcommands the `if/elif` tree gets deeper. A typo in `"clone"` vs `"cloen"` silently breaks routing. -#### The solution: auto-dispatch +#### The solution: auto-dispatch With auto-dispatch, you register a handler *on the command itself*. ArgMojo then parses the arguments **and** routes to the correct handler in one call: @@ -1671,7 +1780,7 @@ No `if/elif` boilerplate. This is the pattern [Cobra](https://github.com/spf13/c --- -#### Step 1 — Define handler functions +#### Step 1 — Define handler functions Each handler is a free function with signature `def (ParseResult) raises`. It receives the parsed arguments for the command it is attached to. It is a procedure — it does work but returns nothing. @@ -1687,7 +1796,7 @@ def handle_init(result: ParseResult) raises: > **Why free functions?** Mojo does not yet support storing closures (functions that capture variables from their enclosing scope) as struct fields. Handlers must be module-level `def` functions. If you need to pass state to a handler, encode it in the parsed arguments or use module-level constants. -#### Step 2 — Register handlers with `set_run_function()` +#### Step 2 — Register handlers with `set_run_function()` ```mojo def main() raises: @@ -1711,13 +1820,13 @@ Running `myapp init myproject` calls `handle_init` with `name="myproject"`. --- -#### Method reference +#### Method reference -| Method | Description | -| ----------------------------------- | -------------------------------------------------------------------------------- | -| `cmd.set_run_function(handler)` | Registers a `def (ParseResult) raises` handler on `cmd`. | -| `cmd.execute()` | Parses `sys.argv()`, walks the subcommand chain, and calls the matching handler. | -| `cmd._execute_with_arguments(args)` | Testing helper. Same as `execute()` but takes an explicit `List[String]`. | +| Method | Description | +| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `cmd.set_run_function(handler)` | Registers a `def (ParseResult) raises` handler on `cmd`. | +| `cmd.execute()` | Parses `sys.argv()`, walks the subcommand chain, and calls the matching handler. | +| `cmd._execute_with_arguments(args)` | Testing helper. Like `execute()` but takes an explicit `List[String]` and raises on parse errors (instead of exiting). | **`set_run_function(handler)`** — Stores the handler function pointer on the command. You can call it again to replace a previous handler. Think of it as labelling a mailbox: "when mail arrives here, this person handles it." @@ -1729,11 +1838,11 @@ Running `myapp init myproject` calls `handle_init` with `name="myproject"`. If no handler is registered on the resolved command, an `Error` is raised. -**`_execute_with_arguments(args)`** — A **testing helper** (prefixed with `_` to indicate internal use). Identical to `execute()`, but accepts an explicit `List[String]` instead of reading `sys.argv()`. In tests you cannot control the real command line, so you pass arguments directly. Production code should always call `execute()`. +**`_execute_with_arguments(args)`** — A **testing helper** (prefixed with `_` to indicate internal use). Like `execute()`, but accepts an explicit `List[String]` instead of reading `sys.argv()`. Unlike `execute()`, parse errors **raise** instead of calling `exit(2)`, making it suitable for tests that need to catch and inspect errors. Production code should always call `execute()`. --- -#### How dispatch walks the tree +#### How dispatch walks the tree Given `app remote add origin`, the internal dispatch chain is: @@ -1755,7 +1864,7 @@ At each level: --- -#### Nested subcommands +#### Nested subcommands The dispatch recursion handles arbitrary nesting depth: @@ -1784,7 +1893,7 @@ The `remote` command does not need a handler — it exists only to group `add`, --- -#### Testing with `_execute_with_arguments()` +#### Testing with `_execute_with_arguments()` In tests, pass explicit argument lists instead of relying on `sys.argv()`: @@ -1794,7 +1903,7 @@ var args: List[String] = ["app", "search", "hello"] app._execute_with_arguments(args) # Calls handle_search with pattern="hello" ``` -#### When to use auto-dispatch vs manual parsing +#### When to use auto-dispatch vs manual parsing | Scenario | Recommended approach | | ----------------------------------- | ---------------------------------------------- | @@ -3110,7 +3219,7 @@ Argument("token", help="API token").long["token"]().prompt["Enter your API token ### Interactive Session Examples -#### All arguments missing — full prompting +#### All arguments missing — full prompting When none of the prompt-enabled arguments are provided, the user is prompted for each one in order: @@ -3123,7 +3232,7 @@ Server region [us/eu/ap] (us): eu The parsed result contains `user="alice"`, `token="secret-123"`, `region="eu"`. -#### Partial arguments — only missing ones are prompted +#### Partial arguments — only missing ones are prompted When some arguments are already provided on the command line, only the missing ones trigger a prompt: @@ -3135,7 +3244,7 @@ Server region [us/eu/ap] (us): ap `--user` was given on the CLI, so `Username:` is **not** asked. -#### All arguments provided — no prompting at all +#### All arguments provided — no prompting at all ```shell ./login --user alice --token secret-123 --region eu @@ -3143,7 +3252,7 @@ Server region [us/eu/ap] (us): ap No prompts appear. The CLI values are used directly. -#### Empty input with a default — default value is used +#### Empty input with a default — default value is used When the user presses Enter without typing anything and the argument has a `.default[]()`, the default is applied: @@ -3156,7 +3265,7 @@ Server region [us/eu/ap] (us): The user pressed Enter at `Server region`, so `region` gets the default value `"us"`. -#### Flag argument — y/n prompt +#### Flag argument — y/n prompt Flag arguments accept `y`/`n`/`yes`/`no` (case-insensitive): @@ -3174,7 +3283,7 @@ Enable verbose output [y/n]: y Answering `y` or `yes` sets the flag to `True`. Answering `n` or `no` sets it to `False`. -#### Argument with choices — choices are shown +#### Argument with choices — choices are shown When a prompt-enabled argument has `.choice[]()` values, they are displayed in brackets. If a default exists, it is shown in parentheses: @@ -3979,7 +4088,7 @@ The table below maps every ArgMojo builder method / command-level method to its ### Command-Level Methods -#### Registration & Structure +#### Registration & Structure | ArgMojo method | argparse | click | clap (Rust) | cobra / pflag (Go) | | ------------------------------------- | -------------------------------- | ------------------------------------- | ---------------------------------- | -------------------------- | @@ -3991,7 +4100,7 @@ The table below maps every ArgMojo builder method / command-level method to its | `disable_help_subcommand()` | — | — | `.disable_help_subcommand(true)` | — | | `allow_positional_with_subcommands()` | — | — | — | `TraverseChildren` | -#### Group Constraints +#### Group Constraints | ArgMojo method | argparse | click | clap (Rust) | cobra / pflag (Go) | | --------------------------- | -------------------------------- | ------------------------------- | ------------------------------ | ------------------------------- | @@ -4001,7 +4110,7 @@ The table below maps every ArgMojo builder method / command-level method to its | `required_if(target, cond)` | — | — | `.required_if_eq("x","v")` | `MarkFlagRequired…` ¹ | | `implies(trigger, implied)` | — | — | `.requires_if("v","x")` ¹⁰ | — | -#### Parser Behaviour +#### Parser Behaviour | ArgMojo method | argparse | click | clap (Rust) | cobra / pflag (Go) | | ---------------------------------- | --------------------------- | ------------------------------ | ------------------------------- | ----------------------- | @@ -4015,7 +4124,7 @@ The table below maps every ArgMojo builder method / command-level method to its | `disable_fullwidth_correction()` | — | — | — | — | | `disable_punctuation_correction()` | — | — | — | — | -#### Help & Display +#### Help & Display | ArgMojo method | argparse | click | clap (Rust) | cobra / pflag (Go) | | ---------------------- | ------------ | ------------ | ---------------------- | ------------------ | @@ -4026,7 +4135,7 @@ The table below maps every ArgMojo builder method / command-level method to its | `warn_color[name]()` | — | `style()` ¹³ | `Styles::styled()` ¹⁴ | — | | `error_color[name]()` | — | `style()` ¹³ | `Styles::styled()` ¹⁴ | — | -#### Shell Completions +#### Shell Completions | ArgMojo method | argparse | click | clap (Rust) | cobra / pflag (Go) | | ------------------------------- | -------- | ---------------- | ---------------------------------- | -------------------------- | @@ -4035,7 +4144,7 @@ The table below maps every ArgMojo builder method / command-level method to its | `completions_as_subcommand()` | — | — | `clap_complete` subcommand pattern | default (subcommand) | | `generate_completion(shell)` | — | `shell_complete` | `clap_complete::generate()` | `GenBashCompletion()` etc. | -#### Auto-Dispatch +#### Auto-Dispatch | ArgMojo method | argparse | click | clap (Rust) | cobra / pflag (Go) | | ------------------------------- | -------- | ----------------------------- | ----------- | ------------------ | @@ -4043,7 +4152,7 @@ The table below maps every ArgMojo builder method / command-level method to its | `execute()` | — | implicit via `cli()` | — | `cmd.Execute()` | | `_execute_with_arguments(args)` | — | `runner.invoke(cli, args)` ¹⁶ | — | — | -#### Parse +#### Parse | ArgMojo method | argparse | click | clap (Rust) | cobra / pflag (Go) | | ----------------------- | ------------------ | --------------- | ------------------------- | ------------------ | diff --git a/src/argmojo/command.mojo b/src/argmojo/command.mojo index 480a0b4..a8a8f65 100644 --- a/src/argmojo/command.mojo +++ b/src/argmojo/command.mojo @@ -1961,10 +1961,10 @@ struct Command(Copyable, Movable, Writable): """Parses the given argument list and auto-dispatches to the appropriate run function. - This is a **testing helper** — it behaves identically to - ``execute()`` but accepts an explicit argument list instead of - reading from ``sys.argv()``. In tests you cannot control the - real command line, so pass the arguments directly:: + This is a **testing helper** that accepts an explicit argument + list instead of reading from ``sys.argv()``. In tests you + cannot control the real command line, so pass the arguments + directly: var args: List[String] = ["app", "build", "mylib"] app._execute_with_arguments(args) @@ -1972,6 +1972,11 @@ struct Command(Copyable, Movable, Writable): The leading underscore marks this method as internal. Production code should call ``execute()``; only test code should call this. + **Difference from ``execute()``:** ``execute()`` calls ``parse()`` + which catches parse errors and calls ``exit(2)``; this method + calls ``parse_arguments()`` which **raises** on parse errors, + making it suitable for tests that need to catch and inspect errors. + Args: raw_args: The raw argument strings (including program name at index 0). diff --git a/tests/test_dispatch.mojo b/tests/test_dispatch.mojo index 3fa6edc..96f1856 100644 --- a/tests/test_dispatch.mojo +++ b/tests/test_dispatch.mojo @@ -42,6 +42,12 @@ def _handler_assert_target_default(result: ParseResult) raises: assert_equal(result.get_string("target"), "all") +def _handler_assert_target_and_verbose(result: ParseResult) raises: + """Handler that asserts both target positional and verbose flag.""" + assert_equal(result.get_string("target"), "mylib") + assert_true(result.get_flag("verbose"), "Expected verbose=True") + + def _handler_assert_filter(result: ParseResult) raises: """Handler that asserts filter option.""" assert_equal(result.get_string("filter"), "unit_*") @@ -281,7 +287,7 @@ def test_persistent_flags_in_subcommand_dispatch() raises: build.add_argument( Argument("target", help="Target").positional().default["all"]() ) - build.set_run_function(_handler_assert_target) + build.set_run_function(_handler_assert_target_and_verbose) app.add_subcommand(build^) # --verbose before subcommand From 56ab36c817917ab8df3dff7f81531fc468e5ee05 Mon Sep 17 00:00:00 2001 From: ZHU Yuhao Date: Sun, 12 Apr 2026 19:35:39 +0200 Subject: [PATCH 5/5] Update user guide --- docs/argmojo_overall_planning.md | 10 +- docs/user_manual.md | 194 +++++++++++++++---------------- 2 files changed, 102 insertions(+), 102 deletions(-) diff --git a/docs/argmojo_overall_planning.md b/docs/argmojo_overall_planning.md index 7fbe6e0..ba64f5b 100644 --- a/docs/argmojo_overall_planning.md +++ b/docs/argmojo_overall_planning.md @@ -290,14 +290,14 @@ Positional arguments and named options are validated **independently** — a com #### Per-Dimension Behavior -**Positional arguments:** +##### Positional arguments | Command config ↓ \ User input → | Enough positionals provided | Not enough positionals provided | | ------------------------------- | --------------------------- | ------------------------------- | | **Has required positional(s)** | ✓ Proceed | ✗ Error + usage | | **No required positional(s)** | ✓ Proceed | N/A — always "enough" | -**Named options:** +##### Named options | Command config ↓ \ User input → | Enough options provided | Not enough options provided | | ------------------------------- | ----------------------- | --------------------------- | @@ -608,7 +608,7 @@ ArgMojo's differentiating features — no other CLI library addresses CJK-specif --ling 使用宇浩靈明編碼 ← CJK chars each take 2 columns, misaligned ``` -**Implementation:** +##### Implementation (CJK alignment) - [x] Implement `_display_width(s: String) -> Int` in `utils.mojo`, traversing each code point: - CJK Unified Ideographs, CJK Ext-A/B/C/D/E/F/G/H/I/J, fullwidth forms → width 2 @@ -625,7 +625,7 @@ ArgMojo's differentiating features — no other CLI library addresses CJK-specif - `--verbose` instead of `--verbose` - `=` instead of `=` -**Implementation:** +##### Implementation (fullwidth correction) - [x] Implement `_fullwidth_to_halfwidth(token: String) -> String` in `utils.mojo`: - Full-width ASCII range: `U+FF01`–`U+FF5E` → subtract `0xFEE0` to get half-width @@ -654,7 +654,7 @@ Note that the following punctuation characters are already handled by the full-w - `——verbose` (em-dash `U+2014` × 2) instead of `--verbose` - `--key:value` (full-width colon `U+FF1A`) instead of `--key=value` -**Implementation:** +##### Implementation (CJK punctuation) - [x] Integrate with typo suggestion system — when a token fails to match any known option, check for common CJK punctuation patterns before running Levenshtein: - `——` (`U+2014 U+2014`, 破折號) → `--` (note that `U+FF0D` full-width hyphen-minus is already handled by the full-width correction step) diff --git a/docs/user_manual.md b/docs/user_manual.md index 65634bd..9e7e3d9 100644 --- a/docs/user_manual.md +++ b/docs/user_manual.md @@ -93,10 +93,10 @@ from argmojo import Argument, Command - [Restrictions](#restrictions) - [Non-Interactive Use](#non-interactive-use) - [Confirmation Option](#confirmation-option) - - [Basic Usage](#basic-usage-1) - - [Custom Prompt Text](#custom-prompt-text-1) - - [Using with Subcommands](#using-with-subcommands-1) - - [Non-Interactive Use](#non-interactive-use-1) + - [Basic Usage (Confirmation Option)](#basic-usage-confirmation-option) + - [Custom Prompt Text (Confirmation Option)](#custom-prompt-text-confirmation-option) + - [Using with Subcommands (Confirmation Option)](#using-with-subcommands-confirmation-option) + - [Non-Interactive Use (Confirmation Option)](#non-interactive-use-confirmation-option) - [Usage Line Customisation](#usage-line-customisation) - [Shell Completion](#shell-completion) - [Built-in `--completions` Flag](#built-in---completions-flag) @@ -116,13 +116,13 @@ from argmojo import Argument, Command - [Pure Declarative — One-Line Parse](#pure-declarative--one-line-parse) - [Hybrid — Declarative + Builder Customisation](#hybrid--declarative--builder-customisation) - [Full Parse — Declarative + Extra Builder Fields](#full-parse--declarative--extra-builder-fields) - - [Subcommands](#subcommands-1) + - [Subcommands in Declarative API](#subcommands-in-declarative-api) - [Auto-Naming Convention](#auto-naming-convention) - [API Summary](#api-summary) - [Cross-Library Method Name Reference](#cross-library-method-name-reference) - [Argument-Level Builder Methods](#argument-level-builder-methods) - [Command-Level Methods](#command-level-methods) - - [Notes](#notes-1) + - [Notes (Cross-Library Method Name Reference)](#notes-cross-library-method-name-reference) @@ -147,7 +147,7 @@ def main() raises: --- -**`parse()` vs `parse_arguments()`** +#### `parse()` vs `parse_arguments()` - **`command.parse()`** reads the real command-line via `sys.argv()`. - **`command.parse_arguments(args)`** accepts a `List[String]` — useful for testing without a real binary. Note that `args[0]` is expected to be the program name and will be skipped, so the actual arguments should start from index 1. @@ -212,7 +212,7 @@ myapp "hello" ./src Positional arguments are assigned in the order they are registered with `add_argument()`. If fewer values are provided than defined arguments, the remaining ones use their default values (if any). If more are provided, an error is raised (see [Positional Argument Count Validation](#positional-argument-count-validation)). -**Retrieving:** +#### Retrieving: ```mojo var pattern = result.get_string("pattern") # "hello" @@ -283,7 +283,7 @@ myapp -v # verbose = True myapp # verbose = False (default) ``` -**Retrieving:** +#### Retrieving: (Flags) ```mojo var verbose = result.get_flag("verbose") # Bool @@ -573,7 +573,7 @@ myapp -v --verbose # verbose = 2 (short + long both increment) myapp # verbose = 0 (default) ``` -**Retrieving:** +#### Retrieving: (Count) ```mojo var level = result.get_count("verbose") # Int @@ -619,7 +619,7 @@ This replaces the manual pattern of defining two separate flags (`--color` and ` --- -**Defining a negatable flag** +#### Defining a negatable flag ```mojo command.add_argument( @@ -640,7 +640,7 @@ Use `result.has("color")` to distinguish between "user explicitly disabled colou --- -**Help output** +#### Help output Negatable flags are displayed as a paired form: @@ -650,9 +650,9 @@ Negatable flags are displayed as a paired form: --- -**Comparison with manual approach** +#### Comparison with manual approach -**Before (two flags + mutually exclusive):** +#### Before (two flags + mutually exclusive): ```mojo command.add_argument(Argument("color", help="Force colored output").long["color"]().flag()) @@ -661,7 +661,7 @@ var group: List[String] = ["color", "no-color"] command.mutually_exclusive(group^) ``` -**After (single negatable flag):** +#### After (single negatable flag): ```mojo command.add_argument( @@ -693,7 +693,7 @@ This is a common pattern for options like `--include`, `--tag`, or `--define` wh --- -**Defining an append option** +#### Defining an append option ```mojo command.add_argument( @@ -725,7 +725,7 @@ All value syntaxes (space-separated, equals, attached short) work with append op --- -**Retrieving** +#### Retrieving ```mojo var tags = result.get_list("tag") # List[String] @@ -737,7 +737,7 @@ for i in range(len(tags)): --- -**Help output** +#### Help output (Append) Append options show a `...` suffix to indicate they are repeatable: @@ -759,7 +759,7 @@ command.add_argument( --- -**Combining with choices** +#### Combining with choices Choices validation is applied to each individual value: @@ -783,7 +783,7 @@ This is similar to Go cobra's `StringSliceVar` and Rust clap's `value_delimiter` --- -**Defining a delimiter option** +#### Defining a delimiter option ```mojo command.add_argument( @@ -820,7 +820,7 @@ Trailing delimiters are ignored — `--env a,b,` produces `["a", "b"]`, not `["a --- -**Retrieving** +#### Retrieving (Delimiter) ```mojo var envs = result.get_list("env") # List[String] @@ -830,7 +830,7 @@ for i in range(len(envs)): --- -**Combining with choices** +#### Combining with choices (Delimiter) Choices are validated per piece after splitting: @@ -848,7 +848,7 @@ myapp --env dev,local # Error: Invalid value 'local' for argument 'env' --- -**Other delimiters** +#### Other delimiters The allowed delimiters are `,` `;` `:` `|`. When fullwidth correction is enabled (the default), fullwidth equivalents in user input (e.g. `,` `;` `:` `|`) are auto-corrected before splitting: @@ -866,7 +866,7 @@ myapp --path "/usr/lib;/opt/lib;/home/lib" --- -**Combining with append** +#### Combining with append When a delimiter option is used multiple times, all split values accumulate: @@ -891,7 +891,7 @@ This is similar to Python argparse's `nargs=N` and Rust clap's `num_args`. --- -**Defining a multi-value option** +#### Defining a multi-value option Use `.number_of_values[N]()` to specify how many values the option consumes: @@ -915,7 +915,7 @@ myapp --rgb 255 128 0 --- -**Repeated occurrences** +#### Repeated occurrences Each occurrence consumes N more values, all accumulating in the same list: @@ -926,7 +926,7 @@ myapp --point 1 2 --point 3 4 --- -**Short options** +#### Short options nargs works with short options too: @@ -937,7 +937,7 @@ myapp -c 255 128 0 --- -**Retrieving values** +#### Retrieving values ```mojo var result = command.parse() @@ -947,7 +947,7 @@ var coords = result.get_list("point") --- -**Choices validation** +#### Choices validation Choices are validated for **each** value individually: @@ -965,7 +965,7 @@ myapp --route north up # ✗ 'up' is not a valid choice --- -**Help output** +#### Help output (Multi-Value) nargs options show the placeholder repeated N times: @@ -980,7 +980,7 @@ options show exactly N placeholders — making the expected arity clear. --- -**Limitations** +#### Limitations - **Equals syntax is not supported**: `--point=10 20` will raise an error. Use space-separated values: `--point 10 20`. @@ -1061,7 +1061,7 @@ dictionary just like `get_list()` returns an empty list. **Help placeholder** — map options automatically show `` instead of the default `` placeholder: -``` +```console Options: -D, --define ... Define a variable ``` @@ -1091,7 +1091,7 @@ myapp --log-level trace # Error: Invalid value 'trace' for argument 'log-leve --log-level {debug,info,warn,error} Log level ``` -**Combining with short options and attached values:** +#### Combining with short options and attached values: ```shell myapp -ldebug # (if short name is "l") OK @@ -1233,7 +1233,7 @@ This is useful when two options are logically contradictory, such as `--json` vs --- -**Defining a group** +#### Defining a group ```mojo command.add_argument(Argument("json", help="Output as JSON").long["json"]().flag()) @@ -1256,7 +1256,7 @@ myapp --json --csv # Error: Arguments are mutually exclusive: '--json', '--c --- -**Works with value-taking options too** +#### Works with value-taking options too The group members don't have to be flags — they can be any kind of argument: @@ -1276,7 +1276,7 @@ myapp --input data.csv --stdin # Error: mutually exclusive --- -**Multiple groups** +#### Multiple groups You can register more than one exclusive group on the same command: @@ -1309,7 +1309,7 @@ This mirrors Go cobra's `MarkFlagsOneRequired` and Rust clap's `ArgGroup::requir --- -**Defining a one-required group** +#### Defining a one-required group ```mojo command.add_argument(Argument("json", help="Output as JSON").long["json"]().flag()) @@ -1331,7 +1331,7 @@ Note that `one_required` only checks that **at least one** is present. It does n --- -**Exactly-one pattern (one-required + mutually exclusive)** +#### Exactly-one pattern (one-required + mutually exclusive) ```mojo command.add_argument(Argument("json", help="Output as JSON").long["json"]().flag()) @@ -1352,7 +1352,7 @@ myapp --json --yaml # Error: Arguments are mutually exclusive: '--json', --- -**Works with value-taking options** +#### Works with value-taking options ```mojo command.add_argument(Argument("input", help="Input file").long["input"]().short["i"]()) @@ -1368,7 +1368,7 @@ myapp # Error: At least one of the following arguments is r --- -**Multiple one-required groups** +#### Multiple one-required groups You can declare multiple groups. Each is validated independently: @@ -1399,7 +1399,7 @@ This is useful for sets of arguments that only make sense as a group — for exa --- -**Defining a group** +#### Defining a group (Required-Together) ```mojo command.add_argument(Argument("username", help="Auth username").long["username"]().short["u"]()) @@ -1422,7 +1422,7 @@ myapp --password secret # Error: Arguments required together: --- -**Three or more arguments** +#### Three or more arguments Groups can contain any number of arguments: @@ -1442,7 +1442,7 @@ myapp --host localhost # Error: '--port', '--proto' --- -**Combining with mutually exclusive groups** +#### Combining with mutually exclusive groups Required-together and mutually exclusive can coexist on the same command: @@ -1490,7 +1490,7 @@ myapp # OK — neither present --- -**Multiple conditional rules** +#### Multiple conditional rules You can declare multiple conditional requirements on the same command: @@ -1503,7 +1503,7 @@ Each rule is checked independently after parsing. --- -**Error messages** +#### Error messages Error messages use `--long` display names when available: @@ -1547,7 +1547,7 @@ myapp # OK — neither set --- -**Chained implications** +#### Chained implications Implications can be chained. If A implies B and B implies C, then setting A will also set C: @@ -1559,7 +1559,7 @@ command.implies("verbose", "log") --- -**Multiple implications from one trigger** +#### Multiple implications from one trigger A single argument can imply multiple targets: @@ -1571,7 +1571,7 @@ command.implies("debug", "log") --- -**Works with count arguments** +#### Works with count arguments When the implied argument is a count (`.count()`), it is set to 1 if not already present. Explicit counts are preserved: @@ -1584,7 +1584,7 @@ command.implies("debug", "verbose") --- -**Cycle detection** +#### Cycle detection Circular implications are detected at registration time and raise an error: @@ -1597,7 +1597,7 @@ This also catches indirect cycles (A → B → C → A). --- -**Integration with other constraints** +#### Integration with other constraints Implications are applied *after* defaults and *before* validation, so implied arguments participate in all subsequent constraint checks: @@ -1846,7 +1846,7 @@ If no handler is registered on the resolved command, an `Error` is raised. Given `app remote add origin`, the internal dispatch chain is: -``` +```console app._dispatch({subcommand: "remote", ...}) → find child "remote" → remote._dispatch({subcommand: "add", ...}) @@ -1920,7 +1920,7 @@ This is inspired by Go cobra's `PersistentFlags()` and is useful for cross-cutti --- -**Defining persistent flags** +#### Defining persistent flags ```mojo var app = Command("app", "My app") @@ -1945,7 +1945,7 @@ app.add_subcommand(search^) --- -**Both positions work** +#### Both positions work ```shell app --verbose search "fn main" # flag BEFORE subcommand @@ -2033,7 +2033,7 @@ The auto-registered `help` subcommand is excluded from the **Commands** section --- -**Disabling the help subcommand** +#### Disabling the help subcommand If you don't want the auto-registered `help` subcommand (e.g., you want to use `help` as a real subcommand name), call `disable_help_subcommand()`: @@ -2066,7 +2066,7 @@ print(result.subcommand) # always "clone", even if user typed "cl" Aliases appear in help output alongside the primary name: -``` +```console Commands: clone, cl Clone a repository commit, ci Record changes to the repository @@ -2189,14 +2189,14 @@ command.add_argument( ) ``` -**Help output (before):** +#### Help output (before): ```console -o, --output Output file path -d, --max-depth Maximum directory depth ``` -**Help output (after `.value_name()` — wrapped by default):** +#### Help output (after `.value_name()` — wrapped by default): ```console -o, --output Output file path @@ -2234,7 +2234,7 @@ myapp --debug-index # Works — flag is set to True myapp --help # --debug-index does NOT appear in the help text ``` -**Typical use cases:** +#### Typical use cases: - Internal debugging flags that end users shouldn't need. - Features that are experimental or not yet stable. @@ -2302,7 +2302,7 @@ command.add_argument( ) ``` -**Behaviour:** +#### Behaviour: | Syntax | Value | | ------------------ | -------------------------------------------------- | @@ -2312,7 +2312,7 @@ command.add_argument( | `-c` | `"gzip"` (default-if-no-value) | | `-cbzip2` | `"bzip2"` (attached) | -**Combined with `.default()`:** +#### Combined with `.default()`: ```mojo command.add_argument( @@ -2353,7 +2353,7 @@ command.add_argument( ) ``` -**Behaviour:** +#### Behaviour: (Require-Equals) | Syntax | Result | | ------------------- | ------------------------------------------------ | @@ -2394,7 +2394,7 @@ command.add_argument( ) ``` -**Help output:** +#### Help output: ```console Options: @@ -2409,7 +2409,7 @@ Output: -o, --output Output file path ``` -**Key behaviours:** +#### Key behaviours: - **Ungrouped arguments** remain under "Options:". - **Group headings** appear in first-appearance order after "Options:". @@ -2428,7 +2428,7 @@ myapp -h myapp '-?' # quote needed: ? is a shell glob wildcard ``` -**Example output:** +#### Example output: ```console A CJK-aware text search tool @@ -2454,7 +2454,7 @@ Help text columns are **dynamically aligned**: the padding between the option na --- -**Coloured Output** +#### Coloured Output Help output uses **ANSI colour codes** by default to enhance readability. @@ -2475,7 +2475,7 @@ var help_plain = command._generate_help(color=False) # no ANSI codes --- -**Custom Colours** +#### Custom Colours The **header colour**, **argument-name colour**, **deprecation warning colour**, and **parse error colour** are all customisable. Section headers always keep the **bold + underline** style; only the colour changes. @@ -2505,7 +2505,7 @@ An unrecognised colour name is caught at **compile time** — the program will n Padding calculation is always based on the **plain-text width** (without escape codes), so columns remain correctly aligned regardless of whether colour is enabled. -**What controls the output:** +#### What controls the output: | Builder method | Effect on help | | -------------------- | ----------------------------------------------------- | @@ -2519,7 +2519,7 @@ After printing help, the program exits cleanly with exit code 0. --- -**`NO_COLOR` Environment Variable** +#### `NO_COLOR` Environment Variable ArgMojo respects the [`NO_COLOR`](https://no-color.org/) convention. When the `NO_COLOR` environment variable is **set** (any value, including an empty string), all ANSI colour codes are suppressed in: @@ -2537,7 +2537,7 @@ This takes priority over the `color=True` default but does **not** override an e --- -**Show Help When No Arguments Provided** +#### Show Help When No Arguments Provided Use `help_on_no_arguments()` to automatically display help when the user invokes the command with no arguments (like `git`, `docker`, or `cargo`): @@ -2602,7 +2602,7 @@ myapp --version myapp -V ``` -**Output:** +#### Output: ```console myapp 1.0.0 @@ -2624,7 +2624,7 @@ ArgMojo's help formatter uses **display width** (East Asian Width) to compute pa See the [Unicode East Asian Width specification](https://www.unicode.org/reports/tr11/) for details on CJK character ranges and properties. -**Example — mixed ASCII and CJK options:** +#### Example — mixed ASCII and CJK options: ```mojo var command = Command("工具", "一個命令行工具") @@ -2642,7 +2642,7 @@ Options: --編碼 <編碼> 設定編碼 ``` -**Example — CJK subcommands:** +#### Example — CJK subcommands: ```mojo var app = Command("工具", "一個命令行工具") @@ -2674,13 +2674,13 @@ ArgMojo automatically detects and corrects these characters **before parsing**, warning: detected full-width characters in '--verbose', auto-corrected to '--verbose' ``` -**What gets corrected:** +#### What gets corrected: - Fullwidth ASCII characters (`U+FF01`–`U+FF5E`) are converted to their halfwidth equivalents (`U+0021`–`U+007E`) by subtracting `0xFEE0`. - Fullwidth spaces (`U+3000`) are converted to regular spaces (`U+0020`). When a single token contains embedded fullwidth spaces (e.g., `--name\u3000yuhao\u3000--verbose` as one argv token), it is split into multiple arguments. - All tokens containing fullwidth ASCII are normalized (converted to halfwidth). Only tokens that start with `-` after correction are treated as options and trigger a warning. Positional values are also converted but no warning is emitted. -**Example — fullwidth flag:** +#### Example — fullwidth flag: ```mojo var app = Command("myapp", "My CLI") @@ -2690,7 +2690,7 @@ var result = app.parse_arguments(["myapp", "--verbose"]) # stderr: warning: detected full-width characters in '--verbose', auto-corrected to '--verbose' ``` -**Example — fullwidth equals syntax:** +#### Example — fullwidth equals syntax: ```mojo var app = Command("myapp", "My CLI") @@ -2699,7 +2699,7 @@ var result = app.parse_arguments(["myapp", "--output=fil # result.get_string("output") == "file.txt" ``` -**Disabling auto-correction:** +#### Disabling auto-correction: Call `disable_fullwidth_correction()` if you prefer strict parsing: @@ -2709,7 +2709,7 @@ app.disable_fullwidth_correction() # Now: fullwidth characters are NOT corrected ``` -**Whitespace handling:** +#### Whitespace handling: By default, only fullwidth space (`U+3000`) triggers token splitting. Other Unicode whitespace characters (for example, EM SPACE `U+2003`) are treated as regular characters and do **not** cause tokens to be split. @@ -2725,7 +2725,7 @@ ArgMojo provides three complementary approaches to handle this, inspired by Pyth --- -**Approach 1: Auto-detect (zero configuration)** +#### Approach 1: Auto-detect (zero configuration) When no registered short option uses a **digit character** as its name, ArgMojo automatically recognises numeric-looking tokens and treats them as positional arguments instead of options. @@ -2748,7 +2748,7 @@ Recognised patterns: `-N`, `-N.N`, `-.N`, `-NeX`, `-N.NeX`, `-Ne+X`, `-Ne-X` (wh --- -**Approach 2: The `--` separator (always works)** +#### Approach 2: The `--` separator (always works) The `--` stop marker forces everything after it to be treated as positional. This is the most universal approach and works regardless of any configuration. @@ -2765,7 +2765,7 @@ Tip: Use '--' to pass values that start with '-' (e.g., negative numbers): calc --- -**Approach 3: `allow_negative_numbers()` (explicit opt-in)** +#### Approach 3: `allow_negative_numbers()` (explicit opt-in) If you have a registered short option that uses a digit character (e.g., `-3` for `--triple`), the auto-detect is suppressed to avoid ambiguity. In this case, call `allow_negative_numbers()` to force all numeric-looking tokens to be treated as positionals. @@ -2787,7 +2787,7 @@ calc -3 # operand = "-3" (NOT the -3 flag!) --- -**Approach 4: `allow_negative_expressions()` (expressions and arbitrary tokens)** +#### Approach 4: `allow_negative_expressions()` (expressions and arbitrary tokens) When your CLI accepts mathematical expressions that start with `-` (e.g. `-1/3*pi`, `-sin(2)`, `-e^2`), `allow_negative_numbers()` is not enough because those tokens are not pure numeric literals. Call `allow_negative_expressions()` to treat any single-hyphen token as a positional argument, provided it doesn't conflict with a registered short option. @@ -2816,7 +2816,7 @@ Rules: --- -**When to use which approach** +#### When to use which approach | Scenario | Recommended approach | | ------------------------------------------------------------------------- | ---------------------------------------- | @@ -2829,7 +2829,7 @@ Rules: --- -**`allow_negative_expressions()` vs `allow_hyphen_values()` — how do they relate?** +#### `allow_negative_expressions()` vs `allow_hyphen_values()` — how do they relate? These two features partially overlap, especially when the command has a single positional argument. Here is a quick comparison: @@ -2856,7 +2856,7 @@ Choose `allow_hyphen_values()` when: --- -**What is NOT a number** +#### What is NOT a number Tokens like `-1abc`, `-e5`, or `-1-2` are not valid numeric patterns. Without `allow_negative_expressions()`, they will still be parsed as short-option strings and may raise "Unknown option" errors if unregistered. With `allow_negative_expressions()`, they are consumed as positional arguments. @@ -2881,7 +2881,7 @@ myapp --out=file.txt # resolves to --output=file.txt --- -**Ambiguous prefixes** +#### Ambiguous prefixes If the prefix matches more than one option, an error is raised: @@ -2897,7 +2897,7 @@ myapp --ver --- -**Exact match always wins** +#### Exact match always wins If the user's input is an **exact match** for one option, it is chosen even if it is also a prefix of another option: @@ -2913,7 +2913,7 @@ myapp --col # ambiguous → error --- -**Works with negatable flags** +#### Works with negatable flags Prefix matching also applies to `--no-X` negation: @@ -2989,7 +2989,7 @@ runner myapp --verbose -x --output=foo.txt The remainder positional automatically implies `.positional()` and `.append()`. In help output, it is displayed as `args...` (with trailing ellipsis). -**Rules:** +#### Rules: - `.remainder()` must not have `.long()` or `.short()` — it is positional-only. - At most **one** remainder positional is allowed per command. @@ -3523,7 +3523,7 @@ To bypass the prompt entirely, provide the value on the command line: Some commands are destructive or irreversible — dropping databases, deleting files, deploying to production. The **confirmation option** adds a built-in `--yes` / `-y` flag that lets users skip an interactive confirmation prompt. This is equivalent to Click's `confirmation_option` decorator. -### Basic Usage +### Basic Usage (Confirmation Option) ```mojo from argmojo import Command, Argument @@ -3556,7 +3556,7 @@ $ ./drop mydb --yes Dropping database: mydb ``` -### Custom Prompt Text +### Custom Prompt Text (Confirmation Option) Use the compile-time parameter overload to set a custom prompt: @@ -3570,7 +3570,7 @@ This changes the prompt to: Drop the database? This cannot be undone. [y/N]: ``` -### Using with Subcommands +### Using with Subcommands (Confirmation Option) Confirmation works naturally with subcommands. The `--yes` flag is registered on the command that calls `confirmation_option()`: @@ -3586,7 +3586,7 @@ var result = app.parse() # app --yes deploy prod → skips confirmation ``` -### Non-Interactive Use +### Non-Interactive Use (Confirmation Option) When stdin is not available (piped input, CI environments, `/dev/null`), the confirmation prompt cannot be displayed. In this case, the command **aborts with an error** unless `--yes` is passed. This ensures that destructive commands never run silently without explicit opt-in: @@ -3739,7 +3739,7 @@ After generating a script, users `source` it or place it in a shell-specific dir --- -**Bash:** +#### Bash: ```bash # One-shot (current session only) @@ -3752,7 +3752,7 @@ myapp --completions bash > ~/.bash_completion.d/myapp --- -**Zsh:** +#### Zsh: ```zsh # Place in your fpath (file must be named _myapp) @@ -3765,7 +3765,7 @@ myapp --completions zsh > ~/.zsh/completions/_myapp --- -**Fish:** +#### Fish: ```shell # Fish auto-loads from this directory @@ -3969,7 +3969,7 @@ def main() raises: See [`examples/declarative/convert.mojo`](../examples/declarative/convert.mojo) for a complete example. -### Subcommands +### Subcommands in Declarative API Every level in the command tree is a `Parsable` struct. Register children via the `subcommands()` hook: @@ -4159,7 +4159,7 @@ The table below maps every ArgMojo builder method / command-level method to its | `parse()` | `parse_args()` | implicit | `.get_matches()` | implicit | | `parse_arguments(args)` | `parse_args(args)` | `.main(args=…)` | `.get_matches_from(args)` | — | -### Notes +### Notes (Cross-Library Method Name Reference) 1. Cobra / pflag uses imperative `cmd.MarkFlag…()` calls on the command, not builder-chaining on the flag definition. 2. clap positional args are defined by `.index(1)`, `.index(2)`, etc., or by omitting `.long()` / `.short()`.