Skip to content

Latest commit

 

History

History
3021 lines (2201 loc) · 104 KB

File metadata and controls

3021 lines (2201 loc) · 104 KB

ArgMojo — User Manual

Comprehensive guide to every feature of the ArgMojo command-line argument parser.

All code examples below assume that you have imported the mojo at the top of your mojo file:

from argmojo import Argument, Command

Getting Started

Creating a Command

A Command is the top-level object that holds argument definitions and runs the parser.

fn main() raises:
    var command = Command("myapp", "A short description of the program", version="1.0.0")
    # ... add arguments ...
    var result = command.parse()
Parameter Type Required Description
name String Yes Program name, shown in help text and usage line.
description String No One-line description shown at the top of help.
version String No Version string printed by --version.

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.

Reading Parsed Results

After calling command.parse() or command.parse_arguments(), you get a ParseResult with these typed accessors:

Method Returns Description
result.get_flag("name") Bool Returns True if the flag was set, else False.
result.get_string("name") String Returns the string value. Raises if not found.
result.get_int("name") Int Parses the value as an integer. Raises on error.
result.get_count("name") Int Returns the count (0 if never provided).
result.get_list("name") List[String] Returns collected values (empty list if none).
result.has("name") Bool Returns True if the argument was provided.

get_string() works for both named options and positional arguments — positional values are looked up by the name given in Argument("name", ...).

var result = command.parse()

# Flags
if result.get_flag("verbose"):
    print("Verbose mode on")

# String values
var output = result.get_string("output")

# Integer values
var depth = result.get_int("max-depth")

# Count flags
var verbosity = result.get_count("verbose")

# List (append) values
var tags = result.get_list("tag")  # List[String]

# Check presence
if result.has("output"):
    print("Output was specified:", result.get_string("output"))

Defining Arguments

Positional Arguments

Positional arguments are matched by order, not by name. They do not start with - or --.

command.add_argument(Argument("pattern", help="Search pattern").positional().required())
command.add_argument(Argument("path",    help="Search path").positional().default("."))
myapp "hello" ./src
#       ↑        ↑
#     pattern   path

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).

Retrieving:

var pattern = result.get_string("pattern")  # "hello"
var path    = result.get_string("path")     # "./src"

Long Options

Long options start with -- and can receive a value in two ways:

Syntax Example Description
--key value --output result.txt Space-separated value.
--key=value --output=result.txt Equals-separated value.
command.add_argument(Argument("output", help="Output file").long("output"))
myapp --output result.txt
myapp --output=result.txt

Both forms produce the same result:

result.get_string("output")  # "result.txt"

Short Options

Short options use a single dash followed by a single character.

command.add_argument(
    Argument("output", help="Output file").long("output").short("o")
)
myapp -o result.txt          # space-separated
myapp -oresult.txt           # attached value (see §9)

A short name is typically defined alongside a long name, but can also be used alone.

Boolean Flags

A flag is a boolean option that takes no value. It is False by default and becomes True when present.

command.add_argument(
    Argument("verbose", help="Enable verbose output")
    .long("verbose").short("v").flag()
)
myapp --verbose    # verbose = True
myapp -v           # verbose = True
myapp              # verbose = False (default)

Retrieving:

var verbose = result.get_flag("verbose")  # Bool

Default Values

When an argument is not provided on the command line, its default value (if any) is used.

command.add_argument(
    Argument("format", help="Output format")
    .long("format").short("f").default("table")
)
command.add_argument(
    Argument("path", help="Search path").positional().default(".")
)
myapp "hello"                # format = "table", path = "."
myapp "hello" --format csv   # format = "csv",   path = "."
myapp "hello" ./src          # format = "table", path = "./src"

Works for both named options and positional arguments.

Required Arguments

Mark an argument as required to make parsing fail when it is absent.

command.add_argument(
    Argument("pattern", help="Search pattern").positional().required()
)
myapp "hello"   # OK
myapp           # Error: Required argument 'pattern' was not provided

Typically used for positional arguments. Named options can also be marked required.

Aliases

Register alternative long names for an argument with .aliases(). Any alias resolves to the same argument during parsing.

var alias_list: List[String] = ["color"]
command.add_argument(
    Argument("colour", help="Colour theme")
        .long("colour")
        .aliases(alias_list^)
)
myapp --colour red     # OK — colour = "red"
myapp --color  red     # OK — resolved via alias, colour = "red"

Prefix matching applies to aliases as well. When both the primary name and an alias share a prefix, _find_by_long deduplicates so the match is never ambiguous within a single argument.

Multiple aliases are supported:

var alias_list: List[String] = ["out", "fmt"]
command.add_argument(
    Argument("output", help="Output format")
        .long("output")
        .aliases(alias_list^)
)

Short Option Details

Short Flag Merging

When multiple short options are boolean flags, they can be combined into a single - token.

command.add_argument(Argument("all",       help="Show all").long("all").short("a").flag())
command.add_argument(Argument("brief",     help="Brief mode").long("brief").short("b").flag())
command.add_argument(Argument("colorize",  help="Colorize").long("colorize").short("c").flag())
myapp -abc
# Expands to: -a -b -c
# all = True, brief = True, colorize = True

Mixing flags with a value-taking option: The last character in a merged group can take a value (the rest of the token or the next argument):

command.add_argument(Argument("output", help="Output file").long("output").short("o"))
myapp -abofile.txt
# Expands to: -a -b -o file.txt
# all = True, brief = True, output = "file.txt"

Attached Short Values

A short option that takes a value can have its value attached directly — no space needed.

command.add_argument(Argument("output", help="Output file").long("output").short("o"))
myapp -ofile.txt          # output = "file.txt"
myapp -o file.txt         # output = "file.txt"  (same result)

This is the same behaviour as GCC's -O2, tar's -xzf archive.tar.gz, and similar UNIX traditions.

Flag Variants

Count Flags

A count flag increments a counter every time it appears. This is a common pattern for verbosity levels.

command.add_argument(
    Argument("verbose", help="Increase verbosity (-v, -vv, -vvv)")
    .long("verbose").short("v").count()
)
myapp -v             # verbose = 1
myapp -vv            # verbose = 2
myapp -vvv           # verbose = 3
myapp --verbose      # verbose = 1
myapp -v --verbose   # verbose = 2  (short + long both increment)
myapp                # verbose = 0  (default)

Retrieving:

var level = result.get_count("verbose")  # Int
if level >= 2:
    print("Debug-level output enabled")

Count flags are a special kind of boolean flag — calling .count() automatically sets .flag() as well, so they don't expect a value.

Merged short flags work seamlessly: -vvv is three occurrences of -v.

Count Ceiling (.max[N]())

You can cap a count flag at a maximum value with .max[n](). The ceiling value n is a compile-time parameter (must be ≥ 1); invalid values are caught at build time. Any occurrences beyond the ceiling are clamped to the maximum and a warning is printed to stderr informing the user of the adjustment.

command.add_argument(
    Argument("verbose", help="Increase verbosity (capped at 3)")
    .long("verbose").short("v").count().max[3]()
)
myapp -vvv           # verbose = 3
myapp -vvvvv         # verbose = 3  (capped, warning printed)
myapp -vvvvvvvvvv    # verbose = 3  (capped, warning printed)
myapp -vv            # verbose = 2  (below ceiling, not affected)

The warning looks like:

warning: '--verbose' count 5 exceeds maximum 3, capped to 3

This is useful when verbosity levels above a certain threshold have no additional effect, or to prevent accidental over-counting. From users' perspective, they get a clear warning rather than a hard error, which is friendlier than using the count option without a ceiling and silently ignoring extra occurrences.

Negatable Flags

A negatable flag automatically creates a --no-X counterpart. When the user passes --X, the flag is set to True; when they pass --no-X, it is explicitly set to False.

This replaces the manual pattern of defining two separate flags (--color and --no-color) and a mutually exclusive group.


Defining a negatable flag

command.add_argument(
    Argument("color", help="Enable colored output")
    .long("color").flag().negatable()
)

myapp --color       # color = True,  has("color") = True
myapp --no-color    # color = False, has("color") = True
myapp               # color = False, has("color") = False  (default)

Use result.has("color") to distinguish between "user explicitly disabled colour" (--no-color) and "user didn't mention colour at all".


Help output

Negatable flags are displayed as a paired form:

      --color / --no-color    Enable colored output

Comparison with manual approach

Before (two flags + mutually exclusive):

command.add_argument(Argument("color", help="Force colored output").long("color").flag())
command.add_argument(Argument("no-color", help="Disable colored output").long("no-color").flag())
var group: List[String] = ["color", "no-color"]
command.mutually_exclusive(group^)

After (single negatable flag):

command.add_argument(
    Argument("color", help="Enable colored output")
    .long("color").flag().negatable()
)

The negatable approach is simpler and uses only one entry in ParseResult.


Scenario Example
Colour control --color / --no-color
Feature toggle --cache / --no-cache
Header inclusion --headers / --no-headers
Interactive prompt --interactive / --no-interactive

Note: Only flags (.flag()) can be made negatable. Calling .negatable() on a non-flag argument has no effect on parsing.

Collecting Multiple Values

Append / Collect Action

An append option collects repeated occurrences into a list. Each time the option appears, its value is added to the list rather than overwriting the previous value.

This is a common pattern for options like --include, --tag, or --define where more than one value is expected.


Defining an append option

command.add_argument(
    Argument("tag", help="Add a tag (repeatable)")
    .long("tag").short("t").append()
)

myapp --tag alpha --tag beta --tag gamma
# tags = ["alpha", "beta", "gamma"]

myapp -t alpha -t beta
# tags = ["alpha", "beta"]

myapp --tag=alpha --tag=beta
# tags = ["alpha", "beta"]

myapp -talpha -tbeta
# tags = ["alpha", "beta"]

myapp
# tags = []  (empty list, not provided)

All value syntaxes (space-separated, equals, attached short) work with append options.


Retrieving

var tags = result.get_list("tag")  # List[String]
for i in range(len(tags)):
    print("tag:", tags[i])

get_list() returns an empty List[String] when the option was never provided.


Help output

Append options show a ... suffix to indicate they are repeatable:

  -t, --tag <tag>...            Add a tag (repeatable)

If a value name is set, it replaces the default placeholder:

command.add_argument(
    Argument("include", help="Include path").long("include").short("I").value_name("DIR").append()
)
  -I, --include DIR...          Include path

Combining with choices

Choices validation is applied to each individual value:

var envs: List[String] = ["dev", "staging", "prod"]
command.add_argument(
    Argument("env", help="Target environment")
    .long("env").choices(envs^).append()
)
myapp --env dev --env prod       # OK
myapp --env dev --env local      # Error: Invalid value 'local' for argument 'env'

Value Delimiter

A value delimiter lets users supply multiple values in a single argument token by splitting on a delimiter character. For example, --env dev,staging,prod is equivalent to --env dev --env staging --env prod.

This is similar to Go cobra's StringSliceVar and Rust clap's value_delimiter.


Defining a delimiter option

command.add_argument(
    Argument("env", help="Target environments")
    .long("env").short("e").delimiter(",")
)

Calling .delimiter(",") automatically implies .append() — you do not need to call both.


myapp --env dev,staging,prod
# envs = ["dev", "staging", "prod"]

myapp --env=dev,staging
# envs = ["dev", "staging"]

myapp -e dev,prod
# envs = ["dev", "prod"]

myapp --env dev,staging --env prod
# envs = ["dev", "staging", "prod"]   (values accumulate across uses)

myapp --env single
# envs = ["single"]                   (no delimiter → one-element list)

myapp
# envs = []                           (not provided → empty list)

Trailing delimiters are ignored — --env a,b, produces ["a", "b"], not ["a", "b", ""].


Retrieving

var envs = result.get_list("env")  # List[String]
for i in range(len(envs)):
    print("env:", envs[i])

Combining with choices

Choices are validated per piece after splitting:

var envs: List[String] = ["dev", "staging", "prod"]
command.add_argument(
    Argument("env", help="Target environments")
    .long("env").choices(envs^).delimiter(",")
)
myapp --env dev,prod       # OK
myapp --env dev,local      # Error: Invalid value 'local' for argument 'env'

Custom delimiter

Any string can be used as the delimiter:

command.add_argument(
    Argument("path", help="Search paths")
    .long("path").delimiter(";")
)
myapp --path "/usr/lib;/opt/lib;/home/lib"
# paths = ["/usr/lib", "/opt/lib", "/home/lib"]

Combining with append

When a delimiter option is used multiple times, all split values accumulate:

command.add_argument(
    Argument("tag", help="Tags").long("tag").short("t").append().delimiter(",")
)
myapp --tag a,b --tag c -t d,e
# tags = ["a", "b", "c", "d", "e"]

Multi-Value Options (nargs)

Some options need to consume multiple consecutive values per occurrence. For example, a 2D point needs two values (--point 10 20), and an RGB colour needs three (--rgb 255 128 0).

This is similar to Python argparse's nargs=N and Rust clap's num_args.


Defining a multi-value option

Use .number_of_values[N]() to specify how many values the option consumes:

command.add_argument(Argument("point", help="X Y coordinates").long("point").number_of_values[2]())
command.add_argument(Argument("rgb", help="RGB colour").long("rgb").short("c").number_of_values[3]())

.number_of_values[N]() automatically implies .append() — values are retrieved with get_list().


myapp --point 10 20
# point = ["10", "20"]

myapp --rgb 255 128 0
# rgb = ["255", "128", "0"]

Repeated occurrences

Each occurrence consumes N more values, all accumulating in the same list:

myapp --point 1 2 --point 3 4
# point = ["1", "2", "3", "4"]

Short options

nargs works with short options too:

myapp -c 255 128 0
# rgb = ["255", "128", "0"]

Retrieving values

var result = command.parse()
var coords = result.get_list("point")
# coords[0] = "10", coords[1] = "20"

Choices validation

Choices are validated for each value individually:

var dirs: List[String] = ["north", "south", "east", "west"]
command.add_argument(
    Argument("route", help="Start and end").long("route").number_of_values[2]().choices(dirs^)
)
myapp --route north east    # ✓ both valid
myapp --route north up      # ✗ 'up' is not a valid choice

Help output

nargs options show the placeholder repeated N times:

Options:
  --point <point> <point>    X Y coordinates
  --rgb N N N                RGB colour        (with .value_name("N"))

Regular append options show ... to indicate repeatability, while nargs options show exactly N placeholders — making the expected arity clear.


Limitations

  • Equals syntax is not supported: --point=10 20 will raise an error. Use space-separated values: --point 10 20.
  • Insufficient values: if fewer than N values remain on the command line, an error is raised with a clear message.

Key-Value Map Options

.map_option() collects key=value pairs into a dictionary. The option is implicitly repeatable (implies .append()), and each value is stored in both a Dict[String, String] map and the list.

command.add_argument(
    Argument("define", help="Define a variable")
        .long("define")
        .short("D")
        .map_option()
)
myapp --define CC=gcc -D CXX=g++
# result.get_map("define") → {"CC": "gcc", "CXX": "g++"}

Equals syntax is supported — --define=CC=gcc works. The first = is consumed by argmojo's --long=value splitting; the remaining CC=gcc is treated as the raw value and split at the next = to produce key CC and value gcc.


Value with embedded = — everything after the first = in the raw value is the value part:

myapp --define PATH=/usr/bin:/bin
# key = "PATH", value = "/usr/bin:/bin"

Delimiter — combine with .delimiter(",") to pass multiple key-value pairs in a single token:

command.add_argument(
    Argument("define", help="Define vars")
        .long("define")
        .map_option()
        .delimiter(",")
)
myapp --define CC=gcc,CXX=g++
# result.get_map("define") → {"CC": "gcc", "CXX": "g++"}

Retrieving values — use result.get_map(name) to get a Dict[String, String] copy of all collected pairs:

var m = result.get_map("define")
# Access individual keys:  m["CC"]

If the argument was not provided, get_map() returns an empty dictionary just like get_list() returns an empty list.


Help placeholder — map options automatically show <key=value> instead of the default <name> placeholder:

Options:
  -D, --define <key=value>...    Define a variable

Value Validation

Choices Validation

Restrict an option's value to a fixed set of allowed strings. If the user provides a value not in the set, parsing fails with a clear error message.

var levels: List[String] = ["debug", "info", "warn", "error"]
command.add_argument(
    Argument("log-level", help="Log level")
    .long("log-level").choices(levels^).default("info")
)
myapp --log-level debug    # OK
myapp --log-level trace    # Error: Invalid value 'trace' for argument 'log-level'
                           #        (choose from 'debug', 'info', 'warn', 'error')

In help text, choices are shown automatically:

  --log-level {debug,info,warn,error}  Log level

Combining with short options and attached values:

myapp -ldebug              # (if short name is "l") OK
myapp -l trace             # Error, same as above

Note: You need to pass the List[String] with ^ (ownership transfer) or .copy() (a new copy) because List[String] is not implicitly copyable.

Positional Argument Count Validation

ArgMojo ensures that the user does not provide more positional arguments than defined. Extra positional values trigger an error.

command.add_argument(Argument("pattern", help="Search pattern").positional().required())
# Only 1 positional arg is defined.
myapp "hello"                  # OK
myapp "hello" extra1 extra2    # Error: Too many positional arguments: expected 1, got 3

With two positional args defined:

command.add_argument(Argument("pattern", help="Search pattern").positional().required())
command.add_argument(Argument("path",    help="Search path").positional().default("."))
myapp "hello" ./src            # OK — pattern = "hello", path = "./src"
myapp "hello" ./src /tmp       # Error: Too many positional arguments: expected 2, got 3

Numeric Range Validation

Constrain a numeric argument to an inclusive [min, max] range with .range[min, max](). The validation is applied after parsing, so the value is still stored as a string; atol() is used internally to convert and compare.

command.add_argument(
    Argument("port", help="Listening port")
        .long("port")
        .range[1, 65535]()
)
myapp --port 8080    # OK
myapp --port 0       # Error: Value 0 for '--port' is out of range [1, 65535]
myapp --port 70000   # Error: Value 70000 for '--port' is out of range [1, 65535]

Boundary values — both min and max are inclusive:

myapp --port 1       # OK
myapp --port 65535   # OK

Append / list values — when combined with .append() or .delimiter(","), every collected value is validated individually:

command.add_argument(
    Argument("port", help="Ports").long("port").append().range[1, 100]()
)
myapp --port 50 --port 101
# Error: Value 101 for '--port' is out of range [1, 100]

Range Clamping (.clamp())

By default, an out-of-range value causes a hard error. If you prefer a gentler approach, chain .clamp() after .range[min, max]() to adjust the value to the nearest boundary and print a warning instead of failing.

command.add_argument(
    Argument("level", help="Compression level (0–9)")
        .long("level")
        .range[0, 9]()
        .clamp()
)
myapp --level 5      # OK — level = 5
myapp --level 20     # Warning, level = 9  (clamped to max)
myapp --level -3     # Warning, level = 0  (clamped to min)

The warning looks like:

warning: '--level' value 20 is out of range [0, 9], clamped to 9

With append mode — each collected value is clamped individually:

command.add_argument(
    Argument("port", help="Ports").long("port").append().range[1, 100]().clamp()
)
myapp --port 50 --port 200 --port 0
# warning: '--port' value 200 is out of range [1, 100], clamped to 100
# warning: '--port' value 0 is out of range [1, 100], clamped to 1
# Result: ports = [50, 100, 1]

Without .clamp() — the existing behaviour is unchanged; an out-of-range value raises an error:

myapp --port 200
# Error: Value 200 for '--port' is out of range [1, 100]

Builder Method Compatibility

The Argument builder has ~20 chainable methods, but not all combinations make sense. The diagrams below show which methods can be used together at a glance.

ASCII Tree

Argument("name", help="...")
║
╠══ Named option ═══════════════════════════════════════════════════════════════
║   .long("x") ─── .short("x")             ← pick one or both
║   │
║   ├── [value mode] (default)             ← takes a string value
║   │   ├── .required()
║   │   ├── .default("val")
║   │   ├── .choices(["a","b","c"])
║   │   ├── .range[1, 100]() ─── .clamp()
║   │   ├── .append()
║   │   │   ├── .delimiter(",")
║   │   │   └── .number_of_values[2]()
║   │   └── .map_option()
║   │
║   ├── .flag()                            ← boolean, no value
║   │   └── .negatable()                     adds --no-X form
║   │
║   └── .count()                           ← counter: -vvv → 3
║       └── .max[3]()                        cap the counter
║
╠══ Positional ═════════════════════════════════════════════════════════════════
║   .positional()                          ← matched by position
║   ├── .required()
║   ├── .default("val")
║   ├── .choices(["a","b","c"])
║   ├── .allow_hyphen_values()               accept -x as a value, not option
║   └── .remainder()                         consume ALL remaining tokens
║       └── (implies .allow_hyphen_values())
║
╠══ Decorators (combine with any path above) ═══════════════════════════════════
║   .value_name("FILE")          display name in help      (value / positional)
║   .hidden()                    hide from --help          (any)
║   .aliases(["alt"])            alternative --names       (named only)
║   .deprecated("msg")           deprecation warning       (any)
║   .persistent()                inherit to subcommands    (named only)
║   .default_if_no_value("val")  default-if-no-value       (value only)
║   .require_equals()            force --key=value syntax  (named value only)
║
╠══ Command-level constraints (called on Command, not Argument) ════════════════
║   command.mutually_exclusive(["a","b"])  at most one from the group
║   command.one_required(["a","b"])        at least one from the group
║   command.required_together(["a","b"])   all or none from the group
║   command.required_if("target","cond")   target required when cond is set
║   command.implies("trigger","implied")   auto-set implied when trigger is set
╚═══════════════════════════════════════════════════════════════════════════════

Reading guide: Indentation shows "goes after" — e.g. .clamp() is indented under .range[min,max]() because it requires range. The three main paths (value / flag / count) under Named option are mutually exclusive — pick exactly one mode per argument.

Mermaid Diagram

flowchart LR
    ARG["Argument(name, help)"]

    ARG --> NAMED[".long() / .short()"]
    ARG --> POS[".positional()"]

    NAMED --> VAL["Value mode\n(default)"]
    NAMED --> FLAG[".flag()"]
    NAMED --> COUNT[".count()"]

    VAL --> req1[".required()"]
    VAL --> def1[".default()"]
    VAL --> cho1[".choices()"]
    VAL --> rng[".range[min,max]()"]
    rng --> clp[".clamp()"]
    VAL --> app[".append()"]
    app --> delim[".delimiter()"]
    app --> nvals[".number_of_values[N]()"]
    VAL --> mapopt[".map_option()"]

    FLAG --> neg[".negatable()"]
    COUNT --> maxn[".max[N]()"]

    POS --> req2[".required()"]
    POS --> def2[".default()"]
    POS --> cho2[".choices()"]
    POS --> ahv[".allow_hyphen_values()"]
    POS --> rem[".remainder()"]
    rem -.-> ahv

    ARG -.-> DEC["Decorators"]
    DEC --> meta[".value_name()"]
    DEC --> hid[".hidden()"]
    DEC --> ali[".aliases()"]
    DEC --> dep[".deprecated()"]
    DEC --> per[".persistent()"]
    DEC --> dinv[".default_if_no_value()"]
    DEC --> reqeq[".require_equals()"]

    ARG -.-> CMDCON["Command-level\nConstraints"]
    CMDCON --> mutex["command.mutually_exclusive()"]
    CMDCON --> onereq["command.one_required()"]
    CMDCON --> reqtog["command.required_together()"]
    CMDCON --> reqif["command.required_if()"]
    CMDCON --> imp["command.implies()"]

    style ARG fill:#e8f4fd,stroke:#333
    style NAMED fill:#d4edda,stroke:#333
    style POS fill:#d4edda,stroke:#333
    style FLAG fill:#fff3cd,stroke:#333
    style COUNT fill:#fff3cd,stroke:#333
    style VAL fill:#fff3cd,stroke:#333
    style DEC fill:#f0f0f0,stroke:#999,stroke-dasharray: 5 5
    style CMDCON fill:#fce4ec,stroke:#999,stroke-dasharray: 5 5
Loading

Compatibility Table

The table below shows which builder methods can be used with each argument mode. = compatible, = not applicable.

Method Named value .flag() .count() .positional()
.long("x")
.short("x")
.required()
.default("val")
.choices(["a","b"])
.range[min,max]()
.clamp() ✓ ¹
.append()
.delimiter(",") ✓ ²
.number_of_values[N]() ✓ ²
.map_option()
.negatable()
.max[N]()
.value_name("FILE")
.hidden()
.aliases(["alt"])
.deprecated("msg")
.persistent()
.default_if_no_value("val")
.allow_hyphen_values()
.remainder()
.require_equals()
command.mutually_exclusive() ³
command.one_required() ³
command.required_together() ³
command.required_if() ³
command.implies() ³

¹ Requires .range[min,max]() first. ² Implies .append() automatically. ³ Command-level method — takes argument names as strings, not chained on Argument.

Group Constraints

Mutually Exclusive Groups

Mutually exclusive means "at most one of these arguments may be provided". If the user supplies two or more arguments from the same group, parsing fails.

This is useful when two options are logically contradictory, such as --json vs --yaml (you can only pick one output format), or --color vs --no-color.


Defining a group

command.add_argument(Argument("json", help="Output as JSON").long("json").flag())
command.add_argument(Argument("yaml", help="Output as YAML").long("yaml").flag())
command.add_argument(Argument("csv",  help="Output as CSV").long("csv").flag())

var group: List[String] = ["json", "yaml", "csv"]
command.mutually_exclusive(group^)

myapp --json           # OK — only one from the group
myapp --yaml           # OK
myapp                  # OK — none from the group is also fine
myapp --json --yaml    # Error: Arguments are mutually exclusive: '--json', '--yaml'
myapp --json --csv     # Error: Arguments are mutually exclusive: '--json', '--csv'

Works with value-taking options too

The group members don't have to be flags — they can be any kind of argument:

command.add_argument(Argument("input", help="Read from file").long("input"))
command.add_argument(Argument("stdin", help="Read from stdin").long("stdin").flag())

var io_group: List[String] = ["input", "stdin"]
command.mutually_exclusive(io_group^)
myapp --input data.csv         # OK
myapp --stdin                  # OK
myapp --input data.csv --stdin # Error: mutually exclusive

Multiple groups

You can register more than one exclusive group on the same command:

var format_group: List[String] = ["json", "yaml", "csv"]
command.mutually_exclusive(format_group^)

var color_group: List[String] = ["color", "no-color"]
command.mutually_exclusive(color_group^)

Each group is validated independently — using --json and --no-color together is fine, because they belong to different groups.


Scenario Example
Conflicting output formats --json / --yaml / --csv
Boolean toggle pair --color / --no-color
Exclusive input sources --file <path> / --stdin
Verbose vs quiet --verbose / --quiet

Note: Pass the List[String] with ^ (ownership transfer).

One-Required Groups

A one-required group declares that at least one argument from the group must be provided. Parsing fails if none are present. This is useful for ensuring the user specifies a mandatory choice — for example, an output format or an input source.

This mirrors Go cobra's MarkFlagsOneRequired and Rust clap's ArgGroup::required.


Defining a one-required group

command.add_argument(Argument("json", help="Output as JSON").long("json").flag())
command.add_argument(Argument("yaml", help="Output as YAML").long("yaml").flag())
var format_group: List[String] = ["json", "yaml"]
command.one_required(format_group^)

myapp --json               # OK (one provided)
myapp --yaml               # OK (one provided)
myapp                      # Error: At least one of the following arguments is required: '--json', '--yaml'
myapp --json --yaml        # OK (at least one is satisfied — both is fine for one_required alone)

Note that one_required only checks that at least one is present. It does not prevent multiple from being used. To enforce exactly one, combine it with mutually_exclusive:


Exactly-one pattern (one-required + mutually exclusive)

command.add_argument(Argument("json", help="Output as JSON").long("json").flag())
command.add_argument(Argument("yaml", help="Output as YAML").long("yaml").flag())

var excl: List[String] = ["json", "yaml"]
var req: List[String] = ["json", "yaml"]
command.mutually_exclusive(excl^)
command.one_required(req^)
myapp --json               # OK
myapp --yaml               # OK
myapp                      # Error: At least one of the following arguments is required: '--json', '--yaml'
myapp --json --yaml        # Error: Arguments are mutually exclusive: '--json', '--yaml'

Works with value-taking options

command.add_argument(Argument("input", help="Input file").long("input").short("i"))
command.add_argument(Argument("stdin", help="Read from stdin").long("stdin").flag())
var source: List[String] = ["input", "stdin"]
command.one_required(source^)
myapp --input data.txt     # OK
myapp --stdin              # OK
myapp                      # Error: At least one of the following arguments is required: '--input', '--stdin'

Multiple one-required groups

You can declare multiple groups. Each is validated independently:

var format_group: List[String] = ["json", "yaml"]
var source_group: List[String] = ["input", "stdin"]
command.one_required(format_group^)
command.one_required(source_group^)
myapp --json --input f.txt   # OK (both groups satisfied)
myapp --json                 # Error (source group unsatisfied)

Scenario Example
Tags / labels --tag release --tag stable
Include paths -I /usr/lib -I /opt/lib
Target environments --env dev --env staging
Compiler defines --define DEBUG --define VERSION=2

Required-Together Groups

Required together means "if any one of these arguments is provided, all the others must be provided too". If only some are given, parsing fails.

This is useful for sets of arguments that only make sense as a group — for example, authentication credentials (--username and --password), or network settings (--host, --port, --protocol).


Defining a group

command.add_argument(Argument("username", help="Auth username").long("username").short("u"))
command.add_argument(Argument("password", help="Auth password").long("password").short("p"))

var group: List[String] = ["username", "password"]
command.required_together(group^)

myapp --username admin --password secret   # OK — both provided
myapp                                      # OK — neither provided
myapp --username admin                     # Error: Arguments required together:
                                           #        '--password' required when '--username' is provided
myapp --password secret                    # Error: Arguments required together:
                                           #        '--username' required when '--password' is provided

Three or more arguments

Groups can contain any number of arguments:

command.add_argument(Argument("host",  help="Host").long("host"))
command.add_argument(Argument("port",  help="Port").long("port"))
command.add_argument(Argument("proto", help="Protocol").long("proto"))

var net_group: List[String] = ["host", "port", "proto"]
command.required_together(net_group^)
myapp --host localhost --port 8080 --proto https   # OK
myapp --host localhost                             # Error: '--port', '--proto' required when '--host' is provided

Combining with mutually exclusive groups

Required-together and mutually exclusive can coexist on the same command:

# These two must appear together
var auth: List[String] = ["username", "password"]
command.required_together(auth^)

# These two cannot appear together
var excl: List[String] = ["json", "yaml"]
command.mutually_exclusive(excl^)

Scenario Example
Authentication pair --username + --password
Network connection --host + --port + --protocol
TLS settings --cert + --key
Database connection --db-host + --db-user + --db-pass

Note: Pass the List[String] with ^ (ownership transfer).

Conditional Requirements

Sometimes an argument should only be required when another argument is present. For example, --output might only make sense when --save is also provided.


command.add_argument(Argument("save", help="Save results").long("save").flag())
command.add_argument(Argument("output", help="Output file").long("output").short("o"))
command.required_if("output", "save")

This means: if --save is provided, then --output must also be provided.

myapp --save --output out.txt   # OK — both present
myapp --save                    # Error: '--output' is required when '--save' is provided
myapp --output file.txt         # OK — condition not triggered
myapp                           # OK — neither present

Multiple conditional rules

You can declare multiple conditional requirements on the same command:

command.required_if("output", "save")       # --output required when --save
command.required_if("format", "compress")   # --format required when --compress

Each rule is checked independently after parsing.


Error messages

Error messages use --long display names when available:

Error: Argument '--output' is required when '--save' is provided

Scenario Example
Save to file --output required when --save
Compression settings --format required when --compress
Custom export configuration --template required when --export

Difference from required_together(): required_together() is symmetric — if any argument from the group appears, all must appear. required_if() is one-directional — only the target is required when the condition is present, not vice versa.

Mutual Implication

Use implies() to declare that setting one argument automatically sets another. This is useful when one mode logically entails another — for example, debug mode should always enable verbose output.


command.add_argument(Argument("debug", help="Debug mode").long("debug").flag())
command.add_argument(Argument("verbose", help="Verbose output").long("verbose").short("v").flag())
command.implies("debug", "verbose")

This means: if --debug is provided, --verbose is automatically set too.

myapp --debug          # OK — --verbose is auto-set
myapp --debug -v       # OK — --verbose already set, no conflict
myapp --verbose        # OK — --debug is NOT set (one-directional)
myapp                  # OK — neither set

Chained implications

Implications can be chained. If A implies B and B implies C, then setting A will also set C:

command.implies("debug", "verbose")
command.implies("verbose", "log")
# --debug → --verbose → --log (all three are set)

Multiple implications from one trigger

A single argument can imply multiple targets:

command.implies("debug", "verbose")
command.implies("debug", "log")
# --debug sets both --verbose and --log

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:

command.add_argument(Argument("verbose", help="Verbosity").long("verbose").short("v").count())
command.implies("debug", "verbose")
# --debug        → verbose count = 1
# --debug -vvv   → verbose count = 3 (explicit value kept)

Cycle detection

Circular implications are detected at registration time and raise an error:

command.implies("a", "b")
command.implies("b", "a")   # Error: cycle detected

This also catches indirect cycles (A → B → C → A).


Integration with other constraints

Implications are applied after defaults and before validation, so implied arguments participate in all subsequent constraint checks:

command.implies("debug", "verbose")
command.required_if("output", "verbose")
# --debug implies --verbose, which triggers the conditional requirement for --output
command.implies("debug", "verbose")
var excl: List[String] = ["verbose", "quiet"]
command.mutually_exclusive(excl^)
# --debug --quiet fails: --debug implies --verbose, which conflicts with --quiet

Scenario Example
Debug enables verbose implies("debug", "verbose")
Verbose enables logging implies("verbose", "log")
Strict enables all checks implies("strict", "lint") + implies("strict", "typecheck")

Difference from required_if(): required_if() requires the user to provide the target argument — parsing fails if they don't. implies() automatically sets the target — no user action needed.

Subcommands

Subcommands (app <subcommand> [args]) let you group related functionality under a single binary — similar to git commit, docker run, or cargo build. In ArgMojo, a subcommand is simply another Command instance registered on the parent.

Defining Subcommands

Register subcommands with add_subcommand(). Each subcommand has its own set of arguments, help text, and validation rules.

var app = Command("app", "My CLI tool", version="1.0.0")
app.add_argument(Argument("verbose", help="Verbose output").long("verbose").short("v").flag())

var search = Command("search", "Search for patterns")
search.add_argument(Argument("pattern", help="Search pattern").positional().required())
search.add_argument(Argument("max-depth", help="Max depth").long("max-depth").short("d").value_name("N"))

var init = Command("init", "Initialise a new project")
init.add_argument(Argument("name", help="Project name").positional().required())

app.add_subcommand(search^)
app.add_subcommand(init^)

var result = app.parse()
app search "fn main" --max-depth 3
app init my-project

Root-level flags before the subcommand token are parsed as part of the root command:

app --verbose search "fn main"
# verbose = True (root flag), subcommand = "search"

Help output — when subcommands are registered, the root help automatically includes a Commands section and the usage line shows <COMMAND>:

My CLI tool

Usage: app <COMMAND> [OPTIONS]

Options:
  -v, --verbose    Verbose output
  -h, --help       Show this help message
  -V, --version    Show version

Commands:
  search    Search for patterns
  init      Initialise a new project

Child help shows the full command path in the usage line:

app search --help
Search for patterns

Usage: app search <pattern> [OPTIONS]

Arguments:
  pattern    Search pattern

Options:
  -d, --max-depth N    Max depth
  -h, --help           Show this help message
  -V, --version        Show version

The -- stop marker prevents subcommand dispatch. After --, all tokens become positional arguments for the root command:

app -- search
# "search" is a root positional, NOT a subcommand dispatch

Parsing Subcommand Results

After parsing, check result.subcommand to see which subcommand was selected, and use result.get_subcommand_result() to access the child's parsed values.

var result = app.parse()

if result.subcommand == "search":
    var sub = result.get_subcommand_result()
    var pattern = sub.get_string("pattern")
    var depth = sub.get_int("max-depth") if sub.has("max-depth") else 10
    print("Searching for:", pattern)

elif result.subcommand == "init":
    var sub = result.get_subcommand_result()
    var name = sub.get_string("name")
    print("Initialising project:", name)
Method / Field Returns Description
result.subcommand String Name of selected subcommand (empty if none).
result.has_subcommand_result() Bool True if a subcommand was dispatched.
result.get_subcommand_result() ParseResult The child command's parsed result.

All standard ParseResult methods (get_flag(), get_string(), get_int(), get_list(), get_map(), get_count(), has()) work on the subcommand result.

Persistent (Global) Flags

A persistent flag is declared on the parent command but is automatically available in every subcommand. The user can place it either before or after the subcommand token — both work identically.

This is inspired by Go cobra's PersistentFlags() and is useful for cross-cutting concerns like verbosity, output format, or colour control.


Defining persistent flags

var app = Command("app", "My app")

# These are available everywhere
app.add_argument(
    Argument("verbose", help="Verbose output")
    .long("verbose").short("v").flag().persistent()
)
app.add_argument(
    Argument("output", help="Output format")
    .long("output").short("o")
    .choices(["json", "text", "yaml"])
    .default("text")
    .persistent()
)

var search = Command("search", "Search for patterns")
search.add_argument(Argument("pattern", help="Pattern").positional().required())
app.add_subcommand(search^)

Both positions work

app --verbose search "fn main"     # flag BEFORE subcommand
app search --verbose "fn main"     # flag AFTER subcommand  (same result)
app -v search -o json "fn main"    # short forms work too

Bidirectional sync — persistent flag values are synchronised between root and child results, regardless of where the user places them:

var result = app.parse()
var sub = result.get_subcommand_result()

# Both see the same value, no matter where the flag was placed
print(result.get_flag("verbose"))   # True
print(sub.get_flag("verbose"))      # True

Help output — persistent flags appear under a separate Global Options heading in both root and child help:

# Root help (app --help)
Options:
  -h, --help       Show this help message
  -V, --version    Show version

Global Options:
  -v, --verbose               Verbose output
  -o, --output {json,text,yaml}    Output format

# Child help (app search --help)
Options:
  -h, --help    Show this help message
  -V, --version Show version

Global Options:
  -v, --verbose               Verbose output
  -o, --output {json,text,yaml}    Output format

Conflict detection — if a persistent flag on the parent has the same long or short name as a local flag on a child, add_subcommand() raises an error at registration time:

var app = Command("app", "My app")
app.add_argument(Argument("verbose", help="Verbose").long("verbose").short("v").flag().persistent())

var sub = Command("sub", "A child")
sub.add_argument(Argument("verbose", help="Also verbose").long("verbose").flag())  # conflict!

app.add_subcommand(sub^)  # raises: Persistent flag '--verbose' on 'app'
                           #         conflicts with '--verbose' on subcommand 'sub'

Non-persistent root flags with the same name as child flags do not conflict — they are independent and scoped to their own command.


All argument types can be made persistent — flags, count flags, value options, choices, etc.:

app.add_argument(
    Argument("log-level", help="Log level")
    .long("log-level").choices(["debug", "info", "warn", "error"])
    .default("info").persistent()
)

The help Subcommand

When you call add_subcommand() for the first time, ArgMojo automatically registers a help subcommand. This mirrors the behaviour of git help, cargo help, and kubectl help.

app help search    # equivalent to: app search --help
app help init      # equivalent to: app init --help
app help           # shows root help (same as: app --help)

The auto-registered help subcommand is excluded from the Commands section in help output to avoid clutter.


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():

app.disable_help_subcommand()

This can be called before or after add_subcommand(). If called after, the auto-added help entry is removed.

Subcommand Aliases

You can register short aliases for subcommands with command_aliases(). When the user types an alias, ArgMojo dispatches to the canonical subcommand and stores the canonical name (not the alias) in result.subcommand.

var clone = Command("clone", "Clone a repository")
var aliases: List[String] = ["cl"]
clone.command_aliases(aliases^)
app.add_subcommand(clone^)
app cl https://example.com/repo.git   # dispatches to "clone"
app clone https://example.com/repo.git # still works
var result = app.parse()
print(result.subcommand)  # always "clone", even if user typed "cl"

Aliases appear in help output alongside the primary name:

Commands:
  clone, cl    Clone a repository
  commit, ci   Record changes to the repository

Aliases are also included in shell-completion scripts and typo suggestions.

Unknown Subcommand Error

When the root command has subcommands registered and allow_positional_with_subcommands() has not been called, an unrecognised token triggers an error listing available commands:

app foobar
# error: app: Unknown command 'foobar'. Available commands: search, init

The error message excludes the auto-registered help subcommand and hidden subcommands from the list.

If the command has opted in via allow_positional_with_subcommands(), unknown tokens are treated as positionals rather than triggering this error.

Hidden Subcommands

A hidden subcommand is fully functional but excluded from user-facing surfaces:

  • --help output (the Commands: section and usage line)
  • Shell completion scripts (bash, zsh, fish)
  • "Available commands" error messages
  • Typo suggestions

The subcommand remains dispatchable by its exact name or alias. This is useful for internal, experimental, or deprecated commands.

var app = Command("myapp", "My application")

var debug = Command("debug", "Internal diagnostics")
debug.hidden()                          # mark as hidden
app.add_subcommand(debug^)

var search = Command("search", "Search for items")
app.add_subcommand(search^)

# 'debug' won't appear in --help or completions, but:
#   myapp debug ...   still works

Hidden subcommand aliases also remain functional:

var debug = Command("debug", "Internal diagnostics")
debug.hidden()
var aliases: List[String] = ["dbg"]
debug.command_aliases(aliases^)
app.add_subcommand(debug^)
# myapp dbg   still dispatches to debug

Mixing Positional Args with Subcommands

By default, ArgMojo prevents mixing positional arguments and subcommands on the same command. This follows the convention of major CLI frameworks (cobra, clap, Click) — mixing the two creates ambiguity about whether an unknown token is a misspelt subcommand or a positional value.

var app = Command("app", "My app")
app.add_subcommand(Command("search", "Search"))
app.add_argument(Argument("query", help="Query").positional())  # raises!

The same guard triggers if you add a subcommand to a command that already has positional arguments:

var app = Command("app", "My app")
app.add_argument(Argument("file", help="File").positional())
app.add_subcommand(Command("init", "Init"))  # raises!

If you genuinely need both (e.g., -- stopping dispatch so the subcommand name becomes a positional), call allow_positional_with_subcommands() before adding either:

var app = Command("app", "My app")
app.allow_positional_with_subcommands()
app.add_subcommand(Command("search", "Search"))
app.add_argument(Argument("fallback", help="Fallback").positional())

# "foo" doesn't match any subcommand → treated as positional
var args: List[String] = ["app", "foo"]
var result = app.parse_arguments(args)
print(result.get_string("fallback"))  # "foo"

Please seriously think twice before doing this — it's usually better to design your CLI with a clear separation between subcommands and positionals. Allowing both on the same command can lead to confusing user experiences and error messages.


Error path prefix — errors inside child parsing include the full command path for clarity:

app search --unknown-flag
# error: app search: Unknown option '--unknown-flag'

This makes it immediately clear which subcommand triggered the error, especially in deeply nested command trees.

Help & Display

Value Name

Value name overrides the placeholder text shown for a value in help output. Without it, the argument's internal name is shown in angle brackets (e.g., <output>).

Libraries with similar support: argparse (metavar), clap (value_name), cobra (metavar), Click (metavar).

command.add_argument(
    Argument("output", help="Output file path")
    .long("output").short("o").value_name("FILE")
)
command.add_argument(
    Argument("max-depth", help="Maximum directory depth")
    .long("max-depth").short("d").value_name("N")
)

Help output (before):

  -o, --output <output>       Output file path
  -d, --max-depth <max-depth> Maximum directory depth

Help output (after .value_name()):

  -o, --output FILE           Output file path
  -d, --max-depth N           Maximum directory depth

Value name is purely cosmetic — it has no effect on parsing.

Hidden Arguments

A hidden argument is fully functional but excluded from the --help output. Useful for internal, deprecated, or debug-only options.

command.add_argument(
    Argument("debug-index", help="Dump internal search index")
    .long("debug-index").flag().hidden()
)
myapp --debug-index    # Works — flag is set to True
myapp --help           # --debug-index does NOT appear in the help text

Typical use cases:

  • Internal debugging flags that end users shouldn't need.
  • Features that are experimental or not yet stable.
  • Backward-compatible aliases you don't want to advertise.

Deprecated Arguments

Mark an argument as deprecated with .deprecated("message"). The argument still works normally, but a warning is printed to stderr when the user provides it.

command.add_argument(
    Argument("format_old", help="Legacy output format")
        .long("format-old")
        .deprecated("Use --format instead")
)
myapp --format-old csv
# stderr: Warning: '--format-old' is deprecated: Use --format instead
# parsing continues: format_old = "csv"

Short options also trigger the warning:

command.add_argument(
    Argument("compat", help="Compat mode")
        .long("compat").short("C").flag()
        .deprecated("Will be removed in 2.0")
)
myapp -C
# stderr: Warning: '-C' is deprecated: Will be removed in 2.0

Help display — deprecated arguments show the deprecation message in the help text:

Options:
  --format-old <format_old>    Legacy output format [deprecated: Use --format instead]

Default-if-no-value

Use .default_if_no_value("value") to make an option's value optional. When the option is present without an explicit value, the default-if-no-value is used. When an explicit value is provided (via = for long options, or attached for short options), that value is used instead.

.default_if_no_value() automatically implies .require_equals() for long options in the sense that = is required to attach an explicit value. A bare --key is still accepted and uses the default-if-no-value; --key value (space-separated) does not treat value as the argument to --key but leaves it to be parsed as a positional argument or another option. To supply an explicit value to the option itself, the user must write --key=value.

command.add_argument(
    Argument("compress", help="Compression algorithm")
    .long("compress")
    .short("c")
    .default_if_no_value("gzip")
)

Behaviour:

Syntax Value
(omitted) not set (or default, if .default() is also used)
--compress "gzip" (default-if-no-value)
--compress=bzip2 "bzip2" (explicit)
-c "gzip" (default-if-no-value)
-cbzip2 "bzip2" (attached)

Combined with .default():

command.add_argument(
    Argument("compress", help="Compression algorithm")
    .long("compress")
    .default_if_no_value("gzip")
    .default("none")
)
# Not provided  → "none"  (default)
# --compress    → "gzip"  (default-if-no-value)
# --compress=xz → "xz"    (explicit)

Help display — the optional value is shown in brackets:

Options:
      --compress[=<compress>]    Compression algorithm

With .value_name("ALGO"):

Options:
      --compress[=ALGO]          Compression algorithm

Require Equals Syntax

Use .require_equals() to force --key=value syntax. Space-separated --key value is rejected, which avoids ambiguity when values might start with -.

command.add_argument(
    Argument("output", help="Output file")
    .long("output")
    .short("o")
    .require_equals()
)

Behaviour:

Syntax Result
--output=file.txt "file.txt" (OK)
--output file.txt error
--output error
-o file.txt "file.txt" (OK — short options are unaffected)

Help display — the = is shown in the help:

Options:
  -o, --output=<output>    Output file

Combined with .default_if_no_value() — see Default-if-no-value above. When both are set, --key uses the default-if-no-value while --key=val uses the explicit value.

Auto-generated Help

Every command automatically supports --help (or -h or -?). The help text is generated from the registered argument definitions.

myapp --help
myapp -h
myapp '-?'     # quote needed: ? is a shell glob wildcard

Example output:

A CJK-aware text search tool

Usage: myapp <pattern> [path] [OPTIONS]

Arguments:
  pattern    Search pattern
  path       Search path

Options:
  -l, --ling                        Use Lingming IME for encoding
  -i, --ignore-case                 Case-insensitive search
  -v, --verbose                     Increase verbosity (-v, -vv, -vvv)
  -d, --max-depth N                 Maximum directory depth
  -f, --format {json,csv,table}     Output format
      --color / --no-color          Enable colored output
  -?, -h, --help                    Show this help message
  -V, --version                     Show version

Help text columns are dynamically aligned: the padding between the option names and the description text adjusts automatically based on the longest option line, so everything stays neatly aligned regardless of option length.


Coloured Output

Help output uses ANSI colour codes by default to enhance readability.

Element Default style ANSI code
Section headers bold + underline + bright yellow \x1b[1;4;93m
Option / argument names bright magenta \x1b[95m
Deprecation warnings orange (dark yellow) \x1b[33m
Parse errors bright red \x1b[91m
Description text default terminal colour

The _generate_help() method accepts an optional color parameter:

var help_colored = command._generate_help()              # color=True (default)
var help_plain   = command._generate_help(color=False)   # no ANSI codes

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.

var command = Command("myapp", "My app")
command.header_color("BLUE")     # section headers in bright blue
command.arg_color("GREEN")       # option/argument names in bright green
command.warn_color("YELLOW")     # deprecation warnings (default: orange)
command.error_color("MAGENTA")   # parse errors (default: red)

Available colour names (case-insensitive):

Name ANSI code Preview
RED 91 bright red
GREEN 92 bright green
YELLOW 93 bright yellow
BLUE 94 bright blue
MAGENTA 95 bright magenta
PINK 95 alias for MAGENTA
CYAN 96 bright cyan
WHITE 97 bright white
ORANGE 33 orange/dark yellow

An unrecognised colour name raises an Error at runtime.

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:

Builder method Effect on help
.help("...") Sets the description text for the option.
.value_name("X") Replaces the default placeholder (e.g., N, FILE).
.choices() Shows {a,b,c} in the placeholder.
.hidden() Completely excludes the option from help.
.required() Positional args show as <name> instead of [name].

After printing help, the program exits cleanly with exit code 0.


NO_COLOR Environment Variable

ArgMojo respects the NO_COLOR convention. When the NO_COLOR environment variable is set (any value, including an empty string), all ANSI colour codes are suppressed in:

  • Help output (_generate_help())
  • Warning messages (_warn())
  • Error messages (_error() and _error_with_usage())
NO_COLOR=1 myapp --help    # plain-text help, no colours
NO_COLOR= myapp --help     # also suppressed (empty string counts as "set")
myapp --help               # coloured output (NO_COLOR is unset)

This takes priority over the color=True default but does not override an explicit _generate_help(color=False) call (which already produces plain output regardless).


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):

var command = Command("myapp", "My application")
command.add_argument(Argument("file", help="Input file").long("file").required())
command.help_on_no_arguments()
var result = command.parse()
myapp          # prints help and exits
myapp --file x # normal parsing

This is particularly useful for commands that require arguments — instead of showing an obscure "missing required argument" error, the user sees the full help text.

Custom Tips

Add custom tip lines to the bottom of your help output with add_tip(). This is useful for documenting common patterns, gotchas, or examples.

var command = Command("calc", "A calculator")
command.add_argument(Argument("expr", help="Expression").positional().required())
command.add_tip("Expressions starting with `-` are accepted.")
command.add_tip("Use quotes if you use spaces in expressions.")
A calculator

Usage: calc <expr> [OPTIONS]

Arguments:
  expr    Expression

Options:
  -h, --help       Show this help message
  -V, --version    Show version

Tip: Use '--' to pass values starting with '-' as positionals:  calc -- -10.18
Tip: Expressions starting with `-` are accepted.
Tip: Use quotes if you use spaces in expressions.

Smart default tip — when positional arguments are defined, ArgMojo automatically adds a built-in tip explaining the -- separator. The example in this default tip adapts based on whether negative numbers are auto-detected: if they are, it uses -my-value; otherwise, it uses -10.18.

User-defined tips appear below the built-in tip.


Multiple tips can be added; each is displayed on its own line prefixed with Tip:.

Version Display

Every command automatically supports --version (or -V).

myapp --version
myapp -V

Output:

myapp 1.0.0

The version string is set when creating the Command:

var command = Command("myapp", "Description", version="1.0.0")

After printing the version, the program exits cleanly with exit code 0.

CJK-Aware Help Alignment

ArgMojo automatically handles CJK (Chinese, Japanese, Korean) characters in help output. CJK ideographs and fullwidth characters occupy two terminal columns instead of one, so naïve byte- or codepoint-based padding would cause misaligned help columns.

ArgMojo's help formatter uses display width (East Asian Width) to compute padding, so help descriptions stay aligned even when option names, positional names, subcommand names, or help text contain CJK characters.

See Unicode v17.0 for details on CJK character ranges and properties.

Example — mixed ASCII and CJK options:

var command = Command("工具", "一個命令行工具")
command.add_argument(
    Argument("output", help="Output path").long("output").short("o")
)
command.add_argument(
    Argument("編碼", help="設定編碼").long("編碼")
)
Options:
  -o, --output <output>    Output path
      --編碼 <編碼>        設定編碼

Example — CJK subcommands:

var app = Command("工具", "一個命令行工具")
var init_cmd = Command("初始化", "建立新項目")
app.add_subcommand(init_cmd^)
var build_cmd = Command("構建", "編譯項目")
app.add_subcommand(build_cmd^)
Commands:
  初始化    建立新項目
  構建      編譯項目

No configuration is needed — CJK-aware alignment is always active.

Parsing Behaviour

Negative Number Passthrough

By default, tokens starting with - are interpreted as options. This creates a problem when you need to pass negative numbers (like -10.18, -3.14, -1.5e10) as positional values.

ArgMojo provides three complementary approaches to handle this, inspired by Python's argparse.


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.

var command = Command("calc", "Calculator")
command.add_argument(Argument("operand", help="A number").positional().required())
calc -9876543        # operand = "-9876543" (auto-detected as a number)
calc -3.14           # operand = "-3.14"
calc -.5             # operand = "-.5"
calc -1.5e10         # operand = "-1.5e10"
calc -2.0e-3         # operand = "-2.0e-3"

This works because -9, -3, etc. do not match any registered short option. The parser sees a numeric pattern and skips the option-dispatch path.

Recognised patterns: -N, -N.N, -.N, -NeX, -N.NeX, -Ne+X, -Ne-X (where N and X are digit sequences).


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.

calc -- -10.18         # operand = "-10.18"
calc -- -3e4         # operand = "-3e4"

See The -- Stop Marker for details. When positional arguments are registered, ArgMojo's help output includes a Tip line reminding users about this:

Tip: Use '--' to pass values that start with '-' (e.g., negative numbers):  calc -- -10.18

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.

var command = Command("calc", "Calculator")
command.allow_negative_numbers()   # Explicit opt-in
command.add_argument(
    Argument("triple", help="Triple mode").long("triple").short("3").flag()
)
command.add_argument(Argument("operand", help="A number").positional().required())
calc --triple -3.14   # triple = True, operand = "-3.14"
calc -3               # operand = "-3" (NOT the -3 flag!)

Warning: When allow_negative_numbers() is active, even a bare -3 that exactly matches a registered short option will be consumed as a positional number. Use the long form (--triple) to set the flag.


When to use which approach

Scenario Recommended approach
No digit short options registered Auto-detect (nothing to configure)
You have digit short options (-3, -5, etc.) and need negative numbers allow_negative_numbers()
You need to pass arbitrary dash-prefixed strings (not just numbers) -- separator
Legacy or defensive: works in all cases -- separator

What is NOT a number

Tokens like -1abc, -e5, or -1-2 are not valid numeric patterns. They will still be parsed as short-option strings and may raise "Unknown option" errors if unregistered.

Long Option Prefix Matching

ArgMojo supports prefix matching (also known as abbreviation) for long options. If a user types a prefix of a long option name that unambiguously matches exactly one registered option, it is automatically resolved.

This mirrors Python argparse's allow_abbrev behaviour.


command.add_argument(Argument("verbose", help="Verbose output").long("verbose").short("v").flag())
command.add_argument(Argument("output",  help="Output file").long("output").short("o"))
myapp --verb               # resolves to --verbose
myapp --out file.txt       # resolves to --output file.txt
myapp --out=file.txt       # resolves to --output=file.txt

Ambiguous prefixes

If the prefix matches more than one option, an error is raised:

command.add_argument(Argument("verbose",      help="Verbose").long("verbose").flag())
command.add_argument(Argument("version-info", help="Version info").long("version-info").flag())
myapp --ver
# Error: Ambiguous option '--ver' could match: '--verbose', '--version-info'

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:

command.add_argument(Argument("color",    help="Color mode").long("color").flag())
command.add_argument(Argument("colorize", help="Colorize output").long("colorize").flag())
myapp --color       # exact match → color (not ambiguous with colorize)
myapp --col         # ambiguous → error

Works with negatable flags

Prefix matching also applies to --no-X negation:

myapp --no-col      # resolves to --no-color (if color is the only negatable match)

This feature is always enabled — no configuration needed. It is most useful for long option names where typing the full name is cumbersome:

myapp --max 5       # instead of --max-depth 5
myapp --ig          # instead of --ignore-case

Tip: Exact long option names are always accepted. Prefix matching is a convenience that does not change the behaviour of exact matches.

The -- Stop Marker

A bare -- tells the parser to stop interpreting options. Everything after -- is treated as a positional argument, even if it looks like an option.

command.add_argument(Argument("ling", help="Use Lingming encoding").long("ling").flag())
command.add_argument(Argument("pattern", help="Search pattern").positional().required())
myapp -- --ling
# ling = False  (the -- stopped option parsing)
# pattern = "--ling"  (treated as a positional value)

This is especially useful for patterns or file paths that look like options:

myapp --ling -- "-v is not a flag here" ./src
# ling = True  (parsed before --)
# pattern = "-v is not a flag here"
# path = "./src"

A common use-case is passing negative numbers as positional arguments:

myapp -- -10.18
# pattern = "-10.18"

Tip: ArgMojo's Auto-detect can handle most negative-number cases without --. Use -- only when auto-detect is insufficient (e.g., a digit short option is registered without allow_negative_numbers()).

Remainder Positional (.remainder())

A remainder positional consumes all remaining command-line tokens once it starts matching, including tokens that look like options (e.g., --foo, -x, --some-flag). This is useful for wrapper CLIs that forward arguments to another program.

Libraries with similar support: argparse (nargs=argparse.REMAINDER), clap (trailing_var_arg), cobra (TraverseChildren + ArbitraryArgs).

var command = Command("runner", "Run a program with arguments")
command.add_argument(
    Argument("program", help="Program to run").positional().required()
)
command.add_argument(
    Argument("args", help="Arguments to pass through").remainder()
)
runner myapp --verbose -x --output=foo.txt
# program = "myapp"
# args    = ["--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:

  • .remainder() must not have .long() or .short() — it is positional-only.
  • At most one remainder positional is allowed per command.
  • The remainder positional must be the last positional argument.
  • When no trailing tokens are present, the remainder list is empty (not an error).

Allow Hyphen Values (.allow_hyphen_values())

By default, tokens starting with - are interpreted as options. The .allow_hyphen_values() builder method tells the parser that a specific positional argument may accept tokens starting with - as regular values without requiring -- beforehand. This covers both the bare - (Unix stdin/stdout convention) and any other dash-prefixed literal.

A common use case is accepting - as a conventional shorthand for stdin/stdout:

var command = Command("cat", "Concatenate files")
command.add_argument(
    Argument("file", help="Input file (use - for stdin)")
    .positional()
    .required()
    .allow_hyphen_values()
)
cat -        # file = "-"  (stdin convention)
cat input.txt  # file = "input.txt"

Note: .remainder() automatically enables .allow_hyphen_values() — no need to set it separately on remainder positionals.

Partial Parsing (parse_known_arguments())

parse_known_arguments() works like parse_arguments() but does not raise an error for unrecognised options. Instead, unknown tokens are collected and can be retrieved from the result.

Libraries with similar support: argparse (parse_known_args()), clap (not built-in; use allow_external_subcommands), cobra (FParseErrWhitelist).

var command = Command("wrapper", "Wrapper that forwards unknown flags")
command.add_argument(
    Argument("verbose", help="Verbose output").long("verbose").flag()
)
command.add_argument(
    Argument("file", help="Input file").positional().required()
)

var args: List[String] = ["wrapper", "input.txt", "--verbose", "--color", "-x"]
var result = command.parse_known_arguments(args)

# Known arguments are accessed normally:
var verbose = result.get_flag("verbose")
var file = result.get_string("file")

# Unknown arguments are collected separately:
var unknown = result.get_unknown_args()
# e.g., ["--color", "-x", "--threads=4"]
wrapper input.txt --verbose --color -x --threads=4
# verbose = True
# file    = "input.txt"
# unknown = ["--color", "-x", "--threads=4"]

All other validation (required arguments, choices, range) still applies. Only the "Unknown option" error is suppressed.

Note: Unknown options using = syntax (e.g., --color=auto) are captured as a single token. For space-separated syntax (--color auto), only --color is recorded as unknown; auto flows to positional arguments because the parser cannot tell whether the unknown option takes a value. Use = syntax when forwarding unknown options reliably.

Shell Completion

ArgMojo can generate shell completion scripts for Bash, Zsh, and Fish. These scripts enable tab-completion for your CLI's options, flags, subcommands, and choice values — with zero runtime overhead.

The generated scripts are static: they are produced once from your command tree and sourced by the user's shell. No runtime hook or callback mechanism is needed.

Built-in --completions Flag

Every Command automatically responds to --completions <shell> — just like --help and --version. No extra code is required.

var app = Command("myapp", "My application", version="1.0.0")
app.add_argument(Argument("verbose", help="Verbose output").long("verbose").short("v").flag())
app.add_argument(Argument("output", help="Output file").long("output").short("o").value_name("FILE"))
var formats: List[String] = ["json", "csv", "table"]
app.add_argument(Argument("format", help="Output format").long("format").choices(formats^))

var sub = Command("serve", "Start a server")
sub.add_argument(Argument("port", help="Port number").long("port").short("p"))
app.add_subcommand(sub^)

# parse() handles --completions automatically — no extra code needed
var result = app.parse()

Users run:

myapp --completions bash   # prints Bash completion script and exits
myapp --completions zsh    # prints Zsh completion script and exits
myapp --completions fish   # prints Fish completion script and exits

The --completions option is shown in the help output alongside --help and --version:

Options:
  --help                  Show this help message and exit
  --version               Show the version and exit
  --completions {bash,zsh,fish}
                          Generate shell completion script

Disabling the Built-in Flag

If you want to use completions as a regular argument name — or handle completion triggering entirely on your own — call disable_default_completions():

var app = Command("myapp", "My CLI")
app.disable_default_completions()   # --completions is now an unknown option

disable_default_completions() removes --completions from the parse loop, help output, and all generated completion scripts. The generate_completion() method remains available for programmatic use.

Customising the Trigger Name

By default the option is called --completions. Use completions_name() to rename it:

var app = Command("myapp", "My CLI")
app.completions_name("autocomp")    # → --autocomp bash/zsh/fish

Help output, parse loop, and generated scripts all reflect the new name.

Using a Subcommand Instead of an Option

To expose completion generation as a subcommand rather than a -- option, call completions_as_subcommand():

var app = Command("myapp", "My CLI")
app.completions_as_subcommand()     # → myapp completions bash

The trigger moves from Options: to Commands: in help output. This can be combined with completions_name():

app.completions_name("comp")
app.completions_as_subcommand()     # → myapp comp bash

Generating a Script Programmatically

You can also call generate_completion(shell) directly to get a completion script as a String:

# Generate for any supported shell
var script = app.generate_completion("bash")   # or "zsh" or "fish"
print(script)

The shell argument is case-insensitive ("Bash", "BASH", "bash" all work). An error is raised for unrecognised shell names.

Installing Completions

After generating a script, users source it or place it in a shell-specific directory.


Bash:

# One-shot (current session only)
eval "$(myapp --completions bash)"

# Persistent
myapp --completions bash > ~/.bash_completion.d/myapp
# Then add to ~/.bashrc:  source ~/.bash_completion.d/myapp

Zsh:

# Place in your fpath (file must be named _myapp)
myapp --completions zsh > ~/.zsh/completions/_myapp

# Make sure ~/.zsh/completions is in fpath (add to ~/.zshrc):
#   fpath=(~/.zsh/completions $fpath)
#   autoload -Uz compinit && compinit

Fish:

# Fish auto-loads from this directory
myapp --completions fish > ~/.config/fish/completions/myapp.fish

What Gets Completed

The generated scripts cover the full command tree:

Element Completed? Notes
Long options (--verbose) Yes With description text from help
Short options (-v) Yes Paired with long option when both exist
Boolean flags Yes Marked as no-argument (no file/value completion after the flag)
Count flags (-vvv) Yes Treated like boolean flags (no value expected)
Choices (--format json) Yes Tab-completes the allowed values (json, csv, table)
Subcommands Yes Listed with descriptions; scoped completions for each subcommand
Built-in --help / --version Yes Automatically included
Built-in --completions {bash,…} Yes Automatically included; disable with disable_default_completions()
Hidden arguments No (intentional) .hidden() arguments are excluded from completion
Positional arguments No (by design) Positionals use default shell completion (file paths, etc.)
Persistent (global) flags Yes (root level) Inherited flags appear in the root command's completions

Note: Negatable flags (--color / --no-color) — the --no-X form is not separately listed in completions. The base --color flag is completed; users type --no- manually. This matches the behaviour of other CLI frameworks.

Cross-Library Method Name Reference

The table below maps every ArgMojo builder method / command-level method to its equivalent in four popular CLI libraries. An empty cell means the name is identical (or near-identical) to ArgMojo's. A filled cell shows the other library's name or approach. means the library has no built-in equivalent.

Libraries compared: argparse (Python stdlib), click (Python CLI framework), clap (Rust, derive & builder API), cobra / pflag (Go).

Argument-Level Builder Methods

ArgMojo method argparse click clap (Rust) cobra / pflag (Go)
Argument("name", help="…") add_argument("name", help="…") @click.option("--name", help="…") Arg::new("name").help("…") cmd.Flags().StringP(…)
.long("x") prefix --x in name string prefix --x in decorator .long("x") implicit from flag name
.short("x") prefix -x in name string implicit or combined with long .short('x') StringP → second arg
.flag() action="store_true" is_flag=True action(ArgAction::SetTrue) BoolP / BoolVarP
.required() required=True .required(true) MarkFlagRequired() ¹
.positional() no prefix (positional by default) @click.argument() .index(N) ² cmd.Args ³
.takes_value() (default for non-flag) (default for options) .action(ArgAction::Set) (default for non-bool)
.default("val") default="val" .default_value("val") flag definition arg
.choices(["a","b"]) choices=["a","b"] type=click.Choice(…) .value_parser(["a","b"]) — ⁴
.value_name("FILE") metavar="FILE" metavar="FILE" .value_name("FILE")
.hidden() help=argparse.SUPPRESS .hide(true) MarkHidden() ¹
.count() action="count" count=True .action(ArgAction::Count) CountP / CountVarP
.max[N]()
.negatable() BooleanOptionalAction flag_value / is_flag + secondary --no-x pattern ⁶
.append() action="append" multiple=True .action(ArgAction::Append) StringSliceP
.delimiter(",") type + split .value_delimiter(',') StringSliceP (comma default)
.number_of_values[N]() nargs=N nargs=N .num_args(N)
.range[min,max]() type + manual check type=IntRange(…) .value_parser(RangedI64…) — ⁴
.clamp() clamp=True (on IntRange)
.map_option()
.aliases(["alt"]) — (use multiple names) .visible_alias("alt")
.deprecated("msg") deprecated (3.13+) deprecated=True .hide(true) + manual ShorthandDeprecated() ¹
.persistent() — ⁷ .global(true) PersistentFlags()
.default_if_no_value("val") const="val" + nargs="?" — ⁸ .default_missing_value("val") NoOptDefVal field
.require_equals() .require_equals(true)
.remainder() nargs=argparse.REMAINDER .trailing_var_arg(true) ¹¹ TraverseChildren ¹²
.allow_hyphen_values() .allow_hyphen_values(true)

Command-Level Constraint Methods

ArgMojo method argparse click clap (Rust) cobra / pflag (Go)
mutually_exclusive(…) add_mutually_exclusive_group() cls=MutuallyExclusiveOption .conflicts_with("x") per arg — ⁴
one_required(…) group + required=True .group("G").required(true) — ⁴
required_together(…) .requires("x") per arg MarkFlagsRequiredTogether() ¹
required_if(target, cond) .required_if_eq("x","v") MarkFlagRequired… ¹
implies(trigger, implied) .requires_if("v","x") ¹⁰
parse_known_arguments() parse_known_args() — ¹¹ FParseErrWhitelist ¹²
response_file_prefix() fromfile_prefix_chars="@"

Notes

  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().
  3. Cobra uses cobra.ExactArgs(n), cobra.MinimumNArgs(n), etc. — a completely different approach.
  4. No built-in support; typically implemented with custom validation logic.
  5. click supports --flag/--no-flag via is_flag=True, flag_value=… or the secondary parameter.
  6. Cobra / pflag has no first-class negatable flag; users manually add a --no-x flag.
  7. argparse has parents= for sharing argument definitions, but not inheritable persistent flags in a subcommand tree.
  8. click's closest equivalent is is_eager combined with a custom callback; there is no direct const equivalent for options.
  9. click has no built-in MutuallyExclusiveOption; it is typically implemented via a custom cls or callback.
  10. clap's .requires_if("val", "other_arg") means "if this arg has value val, then other_arg is also required", which is a superset of ArgMojo's implies.
  11. clap uses .trailing_var_arg(true) on the command (not the argument) for remainder-like behaviour. For parse_known_arguments, clap has no direct equivalent; use allow_external_subcommands.
  12. Cobra uses TraverseChildren for remainder-like behaviour. For partial parsing, Cobra's FParseErrWhitelist{UnknownFlags: true} ignores unknown flags.