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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions argparse/README.mbt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# moonbitlang/core/argparse

Declarative argument parsing for MoonBit.

This package is inspired by [`clap`](https://github.com/clap-rs/clap) and intentionally implements a small,
predictable subset of its behavior.

## Positional Semantics

Positional behavior is deterministic and intentionally strict:

- `index` is zero-based.
- Indexed positionals are ordered by ascending `index`.
- Unindexed positionals are appended after indexed ones in declaration order.
- For indexed positionals that are not last, `num_args` must be omitted or exactly
`ValueRange::single()` (`1..1`).
- If a positional has `num_args.lower > 0` and no value is provided, parsing raises
`ArgParseError::TooFewValues`.

## Argument Shape Rule

`FlagArg` and `OptionArg` must provide at least one of `short` or `long`.
Arguments without both are positional-only and should be declared with
`PositionalArg`.

```mbt check
///|
test "name-only option is rejected" {
let cmd = @argparse.Command("demo", args=[@argparse.OptionArg("input")])
try cmd.parse(argv=["file.txt"], env={}) catch {
@argparse.ArgBuildError::Unsupported(msg) =>
inspect(msg, content="flag/option args require short/long")
_ => panic()
} noraise {
_ => panic()
}
}
```

## Core Patterns

```mbt check
///|
test "flag option positional" {
let cmd = @argparse.Command("demo", args=[
@argparse.FlagArg("verbose", short='v', long="verbose"),
@argparse.OptionArg("count", long="count"),
@argparse.PositionalArg("name", index=0),
])
let matches = cmd.parse(argv=["-v", "--count", "2", "alice"], env={}) catch {
_ => panic()
}
assert_true(matches.flags is { "verbose": true, .. })
assert_true(matches.values is { "count": ["2"], "name": ["alice"], .. })
}

///|
test "subcommand with global flag" {
let echo = @argparse.Command("echo", args=[
@argparse.PositionalArg("msg", index=0),
])
let cmd = @argparse.Command(
"demo",
args=[@argparse.FlagArg("verbose", short='v', long="verbose", global=true)],
subcommands=[echo],
)
let matches = cmd.parse(argv=["--verbose", "echo", "hi"], env={}) catch {
_ => panic()
}
assert_true(matches.flags is { "verbose": true, .. })
assert_true(
matches.subcommand is Some(("echo", sub)) &&
sub.flags is { "verbose": true, .. } &&
sub.values is { "msg": ["hi"], .. },
)
}
```

## Help and Version Snapshots

`parse` raises display events instead of exiting. Snapshot tests work well for
help text:

```mbt check
///|
test "help snapshot" {
let cmd = @argparse.Command("demo", about="demo app", version="1.0.0", args=[
@argparse.FlagArg(
"verbose",
short='v',
long="verbose",
about="verbose mode",
),
@argparse.OptionArg("count", long="count", about="repeat count"),
])
try cmd.parse(argv=["--help"], env={}) catch {
@argparse.DisplayHelp::Message(text) =>
inspect(
text,
content=(
#|Usage: demo [options]
#|
#|demo app
#|
#|Options:
#| -h, --help Show help information.
#| -V, --version Show version information.
#| -v, --verbose verbose mode
#| --count <count> repeat count
#|
),
)
_ => panic()
} noraise {
_ => panic()
}
}

///|
test "custom version option overrides built-in version flag" {
let cmd = @argparse.Command("demo", version="1.0.0", args=[
@argparse.FlagArg(
"custom_version",
short='V',
long="version",
about="custom version flag",
),
])
let matches = cmd.parse(argv=["--version"], env={}) catch { _ => panic() }
assert_true(matches.flags is { "custom_version": true, .. })
inspect(
cmd.render_help(),
content=(
#|Usage: demo [options]
#|
#|Options:
#| -h, --help Show help information.
#| -V, --version custom version flag
#|
),
)
}
```
81 changes: 81 additions & 0 deletions argparse/arg_action.mbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Parser-internal action model used for control flow.
priv enum ArgAction {
Set
SetTrue
SetFalse
Count
Append
Help
Version
} derive(Eq)

///|
fn arg_takes_value(arg : Arg) -> Bool {
!arg.is_flag
}

///|
fn arg_action(arg : Arg) -> ArgAction {
if arg.is_flag {
match arg.flag_action {
FlagAction::SetTrue => ArgAction::SetTrue
FlagAction::SetFalse => ArgAction::SetFalse
FlagAction::Count => ArgAction::Count
FlagAction::Help => ArgAction::Help
FlagAction::Version => ArgAction::Version
}
} else {
match arg.option_action {
OptionAction::Set => ArgAction::Set
OptionAction::Append => ArgAction::Append
}
}
}

///|
fn arg_min_max_for_validate(arg : Arg) -> (Int, Int?) raise ArgBuildError {
if arg.num_args is Some(range) {
if range.lower < 0 {
raise ArgBuildError::Unsupported("min values must be >= 0")
}
if range.upper is Some(max_value) {
if max_value < 0 {
raise ArgBuildError::Unsupported("max values must be >= 0")
}
if max_value < range.lower {
raise ArgBuildError::Unsupported("max values must be >= min values")
}
if range.lower == 0 && max_value == 0 {
raise ArgBuildError::Unsupported(
"empty value range (0..0) is unsupported",
)
}
}
(range.lower, range.upper)
} else {
(0, None)
}
}

///|
fn arg_min_max(arg : Arg) -> (Int, Int?) {
match arg.num_args {
Some(range) => (range.lower, range.upper)
None => (0, None)
}
}
59 changes: 59 additions & 0 deletions argparse/arg_group.mbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Declarative argument group constructor.
pub struct ArgGroup {
priv name : String
priv required : Bool
priv multiple : Bool
priv args : Array[String]
priv requires : Array[String]
priv conflicts_with : Array[String]

/// Create an argument group.
fn new(
name : String,
required? : Bool,
multiple? : Bool,
args? : Array[String],
requires? : Array[String],
conflicts_with? : Array[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` can reference either group names or arg names.
pub fn ArgGroup::new(
name : String,
required? : Bool = false,
multiple? : Bool = true,
args? : Array[String] = [],
requires? : Array[String] = [],
conflicts_with? : Array[String] = [],
) -> ArgGroup {
ArgGroup::{
name,
required,
multiple,
args: args.copy(),
requires: requires.copy(),
conflicts_with: conflicts_with.copy(),
}
}
Loading