README

#moonbitlang/core/argparse

Declarative argument parsing for MoonBit.

This package is inspired by clap and keeps a small, predictable feature set.

long defaults to the argument name. Pass long="" to disable long alias. Argument constructors are named FlagArg / OptionArg / PositionArg to avoid shadowing built-in types like Option.

#Quick Start

For tests, snapshotting the failure message is the recommended way to cover both the parse error and the full contextual help text.

///|
test "basic option + positional success snapshot" {
let matches = @argparse.parse(
Command("demo", options=[OptionArg("name")], positionals=[
PositionArg("target"),
]),
argv=["--name", "alice", "file.txt"],
env=Map([]),
)
@debug.debug_inspect(
matches.values,
content=(
#|{ "name": ["alice"], "target": ["file.txt"] }
),
)
}

///|
test "basic option + positional failure snapshot" {
let cmd = @argparse.Command("demo", options=[OptionArg("name")], positionals=[
PositionArg("target"),
])
try cmd.parse(argv=["--bad"], env=Map([])) catch {
err =>
inspect(
err,
content=(
#|error: unexpected argument '--bad' found
#|
#|Usage: demo [options] [target]
#|
#|Arguments:
#| target
#|
#|Options:
#| -h, --help Show help information.
#| --name <name>
#|
),
)
} noraise {
_ => panic()
}
}

#Using it for async CLI

You can call cmd.parse() directly inside async fn main. On parse failure, argparse automatically prints the error message with full contextual help text.

///|
async fn main {
let cmd = @argparse.Command("demo", options=[@argparse.OptionArg("name")], positionals=[
@argparse.PositionArg("target"),
])
let _ = cmd.parse()
}

#Flags And Negation

flags stay as Map[String, Bool], so negated flags preserve explicit false states.

///|
test "negatable flag success snapshot" {
let cmd = @argparse.Command("demo", flags=[FlagArg("cache", negatable=true)])
inspect(
cmd.render_help(),
content=(
#|Usage: demo [options]
#|
#|Options:
#| -h, --help Show help information.
#| --[no-]cache
#|
),
)

let parsed = try! cmd.parse(argv=["--no-cache"], env=Map([]))
@debug.debug_inspect(
parsed.flags,
content=(
#|{ "cache": false }
),
)
}

#Subcommands And Globals

///|
test "global count flag success snapshot" {
let cmd = @argparse.Command(
"demo",
flags=[FlagArg("verbose", short='v', action=Count, global=true)],
subcommands=[Command("run")],
)

let parsed = try! cmd.parse(argv=["-v", "run", "-v"], env=Map([]))
@debug.debug_inspect(
parsed.flag_counts,
content=(
#|{ "verbose": 2 }
),
)
parsed.subcommand is Some(("run", child))
@debug.debug_inspect(
child.flag_counts,
content=(
#|{ "verbose": 2 }
),
)
}

///|
test "subcommand context failure snapshot" {
let cmd = @argparse.Command(
"demo",
flags=[FlagArg("verbose", short='v', action=Count, global=true)],
subcommands=[Command("run")],
)
try cmd.parse(argv=["run", "--oops"], env=Map([])) catch {
err =>
inspect(
err,
content=(
#|error: unexpected argument '--oops' found
#|
#|Usage: demo run [options]
#|
#|Options:
#| -h, --help Show help information.
#| -v, --verbose
#|
),
)
} noraise {
_ => panic()
}
}

#Value Sources (argv > env > default_values)

Value precedence is argv > env > default_values.

///|
test "value source precedence snapshots" {
let cmd = @argparse.Command("demo", options=[
OptionArg("level", env="LEVEL", default_values=["1"]),
])

inspect(
cmd.render_help(),
content=(
#|Usage: demo [options]
#|
#|Options:
#| -h, --help Show help information.
#| --level <level> [env: LEVEL] [default: 1]
#|
),
)

let from_default = try! cmd.parse(argv=[], env=Map([]))
@debug.debug_inspect(
from_default.values,
content=(
#|{ "level": ["1"] }
),
)
@debug.debug_inspect(
from_default.sources,
content=(
#|{ "level": Default }
),
)

let from_env = try! cmd.parse(argv=[], env={ "LEVEL": "2" })
@debug.debug_inspect(
from_env.values,
content=(
#|{ "level": ["2"] }
),
)
@debug.debug_inspect(
from_env.sources,
content=(
#|{ "level": Env }
),
)

let from_argv = try! cmd.parse(argv=["--level", "3"], env={ "LEVEL": "2" })
@debug.debug_inspect(
from_argv.values,
content=(
#|{ "level": ["3"] }
),
)
@debug.debug_inspect(
from_argv.sources,
content=(
#|{ "level": Argv }
),
)
}

#Input Forms

///|
test "option input forms snapshot" {
let cmd = @argparse.Command("demo", options=[OptionArg("count", short='c')])

inspect(
cmd.render_help(),
content=(
#|Usage: demo [options]
#|
#|Options:
#| -h, --help Show help information.
#| -c, --count <count>
#|
),
)

let long_split = try! cmd.parse(argv=["--count", "2"], env=Map([]))
@debug.debug_inspect(
long_split.values,
content=(
#|{ "count": ["2"] }
),
)

let long_inline = try! cmd.parse(argv=["--count=3"], env=Map([]))
@debug.debug_inspect(
long_inline.values,
content=(
#|{ "count": ["3"] }
),
)

let short_split = try! cmd.parse(argv=["-c", "4"], env=Map([]))
@debug.debug_inspect(
short_split.values,
content=(
#|{ "count": ["4"] }
),
)

let short_attached = try! cmd.parse(argv=["-c5"], env=Map([]))
@debug.debug_inspect(
short_attached.values,
content=(
#|{ "count": ["5"] }
),
)
}

///|
test "double-dash separator snapshot" {
let cmd = @argparse.Command("demo", positionals=[
PositionArg("tail", num_args=ValueRange(lower=0), allow_hyphen_values=true),
])
let parsed = try! cmd.parse(argv=["--", "--x", "-y"], env=Map([]))
@debug.debug_inspect(
parsed.values,
content=(
#|{ "tail": ["--x", "-y"] }
),
)
}

#Constraints And Policies

parse raises a single display-ready error string that includes the error and full contextual help.

///|
test "requires relationship success and failure snapshots" {
let cmd = @argparse.Command("demo", options=[
OptionArg("mode", requires=["config"]),
OptionArg("config"),
])

let ok = try! cmd.parse(
argv=["--mode", "fast", "--config", "cfg.toml"],
env=Map([]),
)
@debug.debug_inspect(
ok.values,
content=(
#|{ "mode": ["fast"], "config": ["cfg.toml"] }
),
)

try cmd.parse(argv=["--mode", "fast"], env=Map([])) catch {
err =>
inspect(
err,
content=(
#|error: the following required argument was not provided: 'config' (required by 'mode')
#|
#|Usage: demo [options]
#|
#|Options:
#| -h, --help Show help information.
#| --mode <mode>
#| --config <config>
#|
),
)
} noraise {
_ => panic()
}
}

///|
test "arg group required and exclusive failure snapshot" {
let cmd = @argparse.Command(
"demo",
groups=[
ArgGroup("mode", required=true, multiple=false, args=["fast", "slow"]),
],
flags=[FlagArg("fast"), FlagArg("slow")],
)

try cmd.parse(argv=[], env=Map([])) catch {
err =>
inspect(
err,
content=(
#|error: the following required arguments were not provided:
#| <--fast|--slow>
#|
#|Usage: demo [options]
#|
#|Options:
#| -h, --help Show help information.
#| --fast
#| --slow
#|
#|Groups:
#| mode [required] [exclusive] --fast, --slow
#|
),
)
} noraise {
_ => panic()
}
}

///|
test "subcommand required policy failure snapshot" {
let cmd = @argparse.Command("demo", subcommand_required=true, subcommands=[
Command("echo"),
])

try cmd.parse(argv=[], env=Map([])) catch {
err =>
inspect(
err,
content=(
#|error: the following required argument was not provided: 'subcommand'
#|
#|Usage: demo <command>
#|
#|Commands:
#| echo
#| help Print help for the subcommand(s).
#|
#|Options:
#| -h, --help Show help information.
#|
),
)
} noraise {
_ => panic()
}
}

///|
test "conflicts_with success and failure snapshots" {
let cmd = @argparse.Command("demo", flags=[
FlagArg("verbose", conflicts_with=["quiet"]),
FlagArg("quiet"),
])

let ok = try! cmd.parse(argv=["--verbose"], env=Map([]))
@debug.debug_inspect(
ok.flags,
content=(
#|{ "verbose": true }
),
)

try cmd.parse(argv=["--verbose", "--quiet"], env=Map([])) catch {
err =>
inspect(
err,
content=(
#|error: conflicting arguments: verbose and quiet
#|
#|Usage: demo [options]
#|
#|Options:
#| -h, --help Show help information.
#| --verbose
#| --quiet
#|
),
)
} noraise {
_ => panic()
}
}

#PositionArg Value Ranges

Positionals are parsed in declaration order (no explicit index).

///|
test "bounded non-last positional success snapshot" {
let cmd = @argparse.Command("demo", positionals=[
PositionArg("first", num_args=ValueRange(lower=1, upper=2)),
PositionArg("second", num_args=@argparse.ValueRange::single()),
])

let parsed = try! cmd.parse(argv=["a", "b", "c"], env=Map([]))
@debug.debug_inspect(
parsed.values,
content=(
#|{ "first": ["a", "b"], "second": ["c"] }
),
)
}

///|
test "bounded non-last positional failure snapshot" {
let cmd = @argparse.Command("demo", positionals=[
PositionArg("first", num_args=ValueRange(lower=1, upper=2)),
PositionArg("second", num_args=@argparse.ValueRange::single()),
])
try cmd.parse(argv=["a", "b", "c", "d"], env=Map([])) catch {
err =>
inspect(
err,
content=(
#|error: unexpected value 'd' for '<second>' found; no more were expected
#|
#|Usage: demo <first...> <second>
#|
#|Arguments:
#| first...
#| second
#|
#|Options:
#| -h, --help Show help information.
#|
),
)
} noraise {
_ => panic()
}
}

///|
let cmd : @argparse.Command = Command(
"wrap",
options=[OptionArg("config"), OptionArg("mode")],
positionals=[
PositionArg(
"child_argv",
num_args=ValueRange(lower=0),
allow_hyphen_values=true,
),
],
)

///|
test "positional passthrough keeps child argv after double-dash snapshot" {
let parsed = try! cmd.parse(
argv=[
"--config", "cfg.toml", "--", "child", "--mode", "fast", "--", "--flag",
],
env=Map([]),
)
@debug.debug_inspect(
parsed.values,
content=(
#|{
#| "config": ["cfg.toml"],
#| "child_argv": ["child", "--mode", "fast", "--", "--flag"],
#|}
),
)
}

///|
test "without separator outer parser still consumes its own option names snapshot" {
let parsed = try! cmd.parse(
argv=["--config", "cfg.toml", "child", "--mode", "fast"],
env=Map([]),
)
@debug.debug_inspect(
parsed.values,
content=(
#|{ "config": ["cfg.toml"], "mode": ["fast"], "child_argv": ["child"] }
),
)
}

#
ArgGroup

Declarative argument group constructor.

#
ArgGroup::ArgGroup

#alias(new, deprecated="Use `ArgGroup()` instead")
fn ArgGroup::ArgGroup(name : StringView, required? : Bool, multiple? : Bool, args? : ArrayView[String], requires? : ArrayView[String], conflicts_with? : ArrayView[String]) -> ArgGroup

Create an argument group.

Notes:
  • required=true means at least one member of the group must be present.
  • multiple=false means group members are mutually exclusive.
  • requires and conflicts_with may reference either group names or argument names.

#
Command

pub struct Command {
// private fields
} derive(
Debug
)

Declarative command specification.

#
Command::Command

#alias(new, deprecated="Use `Command()` instead")
fn Command::Command(name : StringView, flags? : ArrayView[FlagArg], options? : ArrayView[OptionArg], positionals? : ArrayView[PositionArg], subcommands? : ArrayView[Command], about? : StringView, version? : StringView, disable_help_flag? : Bool, disable_version_flag? : Bool, disable_help_subcommand? : Bool, arg_required_else_help? : Bool, subcommand_required? : Bool, hidden? : Bool, groups? : ArrayView[ArgGroup], default_subcommand? : StringView) -> Command

Create a declarative command specification.

Notes:
  • flags, options, and positionals declare arguments by kind.
  • groups declares argument-group membership and policies.
  • disable_help_flag / disable_version_flag disable built-in --help / --version.
  • disable_help_subcommand disables built-in help <subcommand> routing.
  • arg_required_else_help=true prints help when no argv tokens are provided.
  • subcommand_required=true requires selecting a subcommand.
  • default_subcommand dispatches missing subcommands to a visible child.
  • hidden=true omits this command from parent command listings.

#
Command::parse

#as_free_fn
fn Command::parse(self : Command, argv? : ArrayView[String], env? : Map[String, String]) -> Matches raise

Parse argv/environment according to this command spec.

Behavior:
  • Help/version requests print output immediately and terminate with exit code 0.
  • Parse failures raise display-ready error text with full contextual help.
  • Command-definition validation failures raise display-ready validation text (without appended help).

Value precedence is argv > env > default_values.

#
Command::render_help

fn Command::render_help(self : Command) -> String

Render help text without parsing.

#
FlagAction

pub(all) enum FlagAction {
SetTrue
SetFalse
Count
Help
Version
} derive(Eq,
Debug
)

Behavior for flag args.

  • SetTrue / SetFalse set a boolean value.
  • Count increments Matches.flag_counts.
  • Help / Version display output and exit successfully when triggered.

#
FlagAction::equal

fn FlagAction::equal(FlagAction, FlagAction) -> Bool

#
FlagAction::to_string

fn FlagAction::to_string(self : FlagAction) -> String

#
FlagArg

pub struct FlagArg {
// private fields
} derive(
Debug
)

Declarative flag constructor wrapper.

#
FlagArg::FlagArg

#alias(new, deprecated="Use `FlagArg()` instead")
fn FlagArg::FlagArg(name : StringView, short? : Char, long? : StringView, about? : StringView, action? : FlagAction, env? : StringView, requires? : ArrayView[String], conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, negatable? : Bool, hidden? : Bool) -> FlagArg

Create a flag argument.

Notes:
  • long defaults to name.
  • Use long="" to disable the long form.
  • At least one of short, long, or env must be available.
  • global=true makes the flag available in subcommands.
  • negatable=true accepts --no-<long> for long flags.
  • If env is set, accepted boolean values are: 1, 0, true, false, yes, no, on, off.

#
Matches

pub struct Matches {
flags : Map[String, Bool]
values : Map[String, Array[String]]
flag_counts : Map[String, Int]
sources : Map[String, ValueSource]
subcommand : (String, Matches)?
// private fields
} derive(
Debug
)

Parse results for declarative commands.

  • flags stores final boolean states for flag args.
  • values stores collected option/positional values.
  • flag_counts stores occurrence counts for FlagAction::Count.
  • sources records the final source (Argv / Env / Default) per arg.
  • subcommand stores nested matches for the selected subcommand.

#
OptionAction

pub(all) enum OptionAction {
Set
Append
} derive(Eq,
Debug
)

Behavior for option args.

  • Set keeps the last provided value.
  • Append keeps all provided values in order.

#
OptionAction::equal

#
OptionAction::to_string

fn OptionAction::to_string(self : OptionAction) -> String

#
OptionArg

pub struct OptionArg {
// private fields
} derive(
Debug
)

Declarative option constructor wrapper. Named OptionArg to avoid shadowing the built-in Option type.

#
OptionArg::OptionArg

#alias(new, deprecated="Use `OptionArg()` instead")
fn OptionArg::OptionArg(name : StringView, short? : Char, long? : StringView, about? : StringView, action? : OptionAction, env? : StringView, default_values? : ArrayView[String], allow_hyphen_values? : Bool, requires? : ArrayView[String], conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, hidden? : Bool) -> OptionArg

Create an option argument.

Notes:
  • long defaults to name.
  • Use long="" to disable the long form.
  • At least one of short, long, or env must be available.
  • Use action=Append to keep repeated occurrences.
  • global=true makes the option available in subcommands.
  • allow_hyphen_values=true allows values like -1 or --raw to be consumed as this option's value when parsing argv.

#
PositionArg

pub struct PositionArg {
// private fields
} derive(
Debug
)

Declarative positional constructor wrapper.

#
PositionArg::PositionArg

#alias(new, deprecated="Use `PositionArg()` instead")
fn PositionArg::PositionArg(name : StringView, about? : StringView, env? : StringView, default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool, requires? : ArrayView[String], conflicts_with? : ArrayView[String], global? : Bool, hidden? : Bool) -> PositionArg

Create a positional argument.

Notes:
  • PositionArg order follows declaration order.
  • num_args controls accepted value count.
  • If num_args is omitted, the default is an optional single value (0..=1).
  • Use num_args=ValueRange::single() for a required single positional.
  • allow_hyphen_values=true allows leading-- tokens to be consumed as positional values (unless they match a declared option).
  • Tokens after -- are always treated as positional values.

#
ValueRange

pub struct ValueRange {
// private fields
} derive(Eq, Show,
Debug
)

Number-of-values constraint for an argument.

#
ValueRange::ValueRange

#alias(new, deprecated="Use `ValueRange()` instead")
fn ValueRange::ValueRange(lower? : Int, upper? : Int) -> ValueRange

Create a value-count range.

Notes:
  • lower defaults to 0.
  • upper omitted means no upper bound.
  • ValueRange(lower=0) means 0...
  • ValueRange(lower=1, upper=3) means 1..=3.

#
ValueRange::equal

fn ValueRange::equal(ValueRange, ValueRange) -> Bool

#
ValueRange::single

fn ValueRange::single() -> ValueRange

Exact single-value range (1..1).

#
ValueRange::to_string

fn ValueRange::to_string(self : ValueRange) -> String

#
ValueSource

pub enum ValueSource {
Argv
Env
Default
} derive(Eq, Show,
Debug
)

Where a value/flag came from.

#
ValueSource::equal

fn ValueSource::equal(ValueSource, ValueSource) -> Bool

#
ValueSource::to_string

fn ValueSource::to_string(self : ValueSource) -> String