From 7d504b54a37ae94354093c4e49e17a07e6a54519 Mon Sep 17 00:00:00 2001 From: zihang Date: Fri, 6 Feb 2026 17:37:33 +0800 Subject: [PATCH 01/40] feat: argparse --- argparse/README.mbt.md | 125 ++ argparse/arg_action.mbt | 88 ++ argparse/arg_group.mbt | 61 + argparse/arg_spec.mbt | 349 ++++++ argparse/argparse_blackbox_test.mbt | 1345 ++++++++++++++++++++ argparse/argparse_test.mbt | 444 +++++++ argparse/command.mbt | 285 +++++ argparse/error.mbt | 85 ++ argparse/help_render.mbt | 423 +++++++ argparse/matches.mbt | 91 ++ argparse/moon.pkg | 5 + argparse/parser.mbt | 1767 +++++++++++++++++++++++++++ argparse/pkg.generated.mbti | 131 ++ argparse/value_range.mbt | 49 + 14 files changed, 5248 insertions(+) create mode 100644 argparse/README.mbt.md create mode 100644 argparse/arg_action.mbt create mode 100644 argparse/arg_group.mbt create mode 100644 argparse/arg_spec.mbt create mode 100644 argparse/argparse_blackbox_test.mbt create mode 100644 argparse/argparse_test.mbt create mode 100644 argparse/command.mbt create mode 100644 argparse/error.mbt create mode 100644 argparse/help_render.mbt create mode 100644 argparse/matches.mbt create mode 100644 argparse/moon.pkg create mode 100644 argparse/parser.mbt create mode 100644 argparse/pkg.generated.mbti create mode 100644 argparse/value_range.mbt diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md new file mode 100644 index 000000000..57502db5a --- /dev/null +++ b/argparse/README.mbt.md @@ -0,0 +1,125 @@ +# moonbitlang/core/argparse + +Declarative argument parsing for MoonBit. + +## Argument Shape Rule + +If an argument has neither `short` nor `long`, it is parsed as a positional +argument. + +This applies even for `OptionArg("name")`. For readability, prefer +`PositionalArg("name", index=...)` when you mean positional input. + +```mbt check +///| +test "name-only option behaves as positional" { + let cmd = @argparse.Command("demo", args=[@argparse.OptionArg("input")]) + let matches = cmd.parse(argv=["file.txt"], env={}) catch { _ => panic() } + assert_true(matches.values is { "input": ["file.txt"], .. }) +} +``` + +## 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 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 + #| + ), + ) +} +``` diff --git a/argparse/arg_action.mbt b/argparse/arg_action.mbt new file mode 100644 index 000000000..f70bbae67 --- /dev/null +++ b/argparse/arg_action.mbt @@ -0,0 +1,88 @@ +// 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 resolve_value_range(range : ValueRange) -> (Int, Int?) { + let min = match range.lower { + Some(value) => if range.lower_inclusive { value } else { value + 1 } + None => 0 + } + let max = match range.upper { + Some(value) => Some(if range.upper_inclusive { value } else { value - 1 }) + None => None + } + (min, max) +} + +///| +fn validate_value_range(range : ValueRange) -> (Int, Int?) raise ArgBuildError { + let (min, max) = resolve_value_range(range) + match max { + Some(max_value) if max_value < min => + raise ArgBuildError::Unsupported("max values must be >= min values") + _ => () + } + (min, max) +} + +///| +fn arg_min_max_for_validate(arg : Arg) -> (Int, Int?) raise ArgBuildError { + match arg.num_args { + Some(range) => validate_value_range(range) + None => (0, None) + } +} + +///| +fn arg_min_max(arg : Arg) -> (Int, Int?) { + match arg.num_args { + Some(range) => resolve_value_range(range) + None => (0, None) + } +} diff --git a/argparse/arg_group.mbt b/argparse/arg_group.mbt new file mode 100644 index 000000000..6ab3d87fa --- /dev/null +++ b/argparse/arg_group.mbt @@ -0,0 +1,61 @@ +// 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] + + fn new( + name : String, + required? : Bool, + multiple? : Bool, + args? : Array[String], + requires? : Array[String], + conflicts_with? : Array[String], + ) -> ArgGroup +} + +///| +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: clone_array_group_decl(args), + requires: clone_array_group_decl(requires), + conflicts_with: clone_array_group_decl(conflicts_with), + } +} + +///| +fn[T] clone_array_group_decl(arr : Array[T]) -> Array[T] { + let out = Array::new(capacity=arr.length()) + for value in arr { + out.push(value) + } + out +} diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt new file mode 100644 index 000000000..1cbe26d49 --- /dev/null +++ b/argparse/arg_spec.mbt @@ -0,0 +1,349 @@ +// 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. + +///| +/// Behavior for flag args. +pub(all) enum FlagAction { + SetTrue + SetFalse + Count + Help + Version +} derive(Eq, Show) + +///| +/// Behavior for option args. +pub(all) enum OptionAction { + Set + Append +} derive(Eq, Show) + +///| +/// Unified argument model used by the parser internals. +priv struct Arg { + name : String + short : Char? + long : String? + index : Int? + about : String? + is_flag : Bool + is_positional : Bool + flag_action : FlagAction + option_action : OptionAction + env : String? + default_values : Array[String]? + num_args : ValueRange? + multiple : Bool + allow_hyphen_values : Bool + last : Bool + requires : Array[String] + conflicts_with : Array[String] + group : String? + required : Bool + global : Bool + negatable : Bool + hidden : Bool +} + +///| +/// Trait for declarative arg constructors. +trait ArgLike { + to_arg(Self) -> Arg +} + +///| +/// Declarative flag constructor wrapper. +pub struct FlagArg { + priv arg : Arg + + fn new( + name : String, + short? : Char, + long? : String, + about? : String, + action? : FlagAction, + env? : String, + requires? : Array[String], + conflicts_with? : Array[String], + group? : String, + required? : Bool, + global? : Bool, + negatable? : Bool, + hidden? : Bool, + ) -> FlagArg +} + +///| +pub impl ArgLike for FlagArg with to_arg(self : FlagArg) { + self.arg +} + +///| +pub fn FlagArg::new( + name : String, + short? : Char, + long? : String, + about? : String, + action? : FlagAction = FlagAction::SetTrue, + env? : String, + requires? : Array[String] = [], + conflicts_with? : Array[String] = [], + group? : String, + required? : Bool = false, + global? : Bool = false, + negatable? : Bool = false, + hidden? : Bool = false, +) -> FlagArg { + FlagArg::{ + arg: Arg::{ + name, + short, + long, + index: None, + about, + is_flag: true, + is_positional: false, + flag_action: action, + option_action: OptionAction::Set, + env, + default_values: None, + num_args: None, + multiple: false, + allow_hyphen_values: false, + last: false, + requires: clone_array_spec(requires), + conflicts_with: clone_array_spec(conflicts_with), + group, + required, + global, + negatable, + hidden, + }, + } +} + +///| +/// Declarative option constructor wrapper. +pub struct OptionArg { + priv arg : Arg + + fn new( + name : String, + short? : Char, + long? : String, + about? : String, + action? : OptionAction, + env? : String, + default_values? : Array[String], + num_args? : ValueRange, + allow_hyphen_values? : Bool, + last? : Bool, + requires? : Array[String], + conflicts_with? : Array[String], + group? : String, + required? : Bool, + global? : Bool, + hidden? : Bool, + ) -> OptionArg +} + +///| +pub impl ArgLike for OptionArg with to_arg(self : OptionArg) { + self.arg +} + +///| +pub fn OptionArg::new( + name : String, + short? : Char, + long? : String, + about? : String, + action? : OptionAction = OptionAction::Set, + env? : String, + default_values? : Array[String], + num_args? : ValueRange, + allow_hyphen_values? : Bool = false, + last? : Bool = false, + requires? : Array[String] = [], + conflicts_with? : Array[String] = [], + group? : String, + required? : Bool = false, + global? : Bool = false, + hidden? : Bool = false, +) -> OptionArg { + OptionArg::{ + arg: Arg::{ + name, + short, + long, + index: None, + about, + is_flag: false, + is_positional: false, + flag_action: FlagAction::SetTrue, + option_action: action, + env, + default_values: clone_optional_array_string(default_values), + num_args, + multiple: allows_multiple_values(num_args, action), + allow_hyphen_values, + last, + requires: clone_array_spec(requires), + conflicts_with: clone_array_spec(conflicts_with), + group, + required, + global, + negatable: false, + hidden, + }, + } +} + +///| +/// Declarative positional constructor wrapper. +pub struct PositionalArg { + priv arg : Arg + + fn new( + name : String, + index? : Int, + about? : String, + env? : String, + default_values? : Array[String], + num_args? : ValueRange, + allow_hyphen_values? : Bool, + last? : Bool, + requires? : Array[String], + conflicts_with? : Array[String], + group? : String, + required? : Bool, + global? : Bool, + hidden? : Bool, + ) -> PositionalArg +} + +///| +pub impl ArgLike for PositionalArg with to_arg(self : PositionalArg) { + self.arg +} + +///| +pub fn PositionalArg::new( + name : String, + index? : Int, + about? : String, + env? : String, + default_values? : Array[String], + num_args? : ValueRange, + allow_hyphen_values? : Bool = false, + last? : Bool = false, + requires? : Array[String] = [], + conflicts_with? : Array[String] = [], + group? : String, + required? : Bool = false, + global? : Bool = false, + hidden? : Bool = false, +) -> PositionalArg { + PositionalArg::{ + arg: Arg::{ + name, + short: None, + long: None, + index, + about, + is_flag: false, + is_positional: true, + flag_action: FlagAction::SetTrue, + option_action: OptionAction::Set, + env, + default_values: clone_optional_array_string(default_values), + num_args, + multiple: range_allows_multiple(num_args), + allow_hyphen_values, + last, + requires: clone_array_spec(requires), + conflicts_with: clone_array_spec(conflicts_with), + group, + required, + global, + negatable: false, + hidden, + }, + } +} + +///| +fn arg_name(arg : Arg) -> String { + arg.name +} + +///| +fn is_flag_spec(arg : Arg) -> Bool { + arg.is_flag +} + +///| +fn is_count_flag_spec(arg : Arg) -> Bool { + arg.is_flag && arg.flag_action == FlagAction::Count +} + +///| +fn allows_multiple_values( + num_args : ValueRange?, + action : OptionAction, +) -> Bool { + action == OptionAction::Append || range_allows_multiple(num_args) +} + +///| +fn range_allows_multiple(range : ValueRange?) -> Bool { + match range { + Some(r) => { + let min = match r.lower { + Some(value) => if r.lower_inclusive { value } else { value + 1 } + None => 0 + } + let max = match r.upper { + Some(value) => Some(if r.upper_inclusive { value } else { value - 1 }) + None => None + } + if min > 1 { + true + } else { + match max { + Some(value) => value > 1 + None => true + } + } + } + None => false + } +} + +///| +fn[T] clone_array_spec(arr : Array[T]) -> Array[T] { + let out = Array::new(capacity=arr.length()) + for value in arr { + out.push(value) + } + out +} + +///| +fn clone_optional_array_string(values : Array[String]?) -> Array[String]? { + match values { + Some(arr) => Some(clone_array_spec(arr)) + None => None + } +} diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt new file mode 100644 index 000000000..65526ff14 --- /dev/null +++ b/argparse/argparse_blackbox_test.mbt @@ -0,0 +1,1345 @@ +// 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. + +///| +struct DecodeName { + name : String +} + +///| +impl @argparse.FromMatches for DecodeName with from_matches( + matches : @argparse.Matches, +) { + match matches.values.get("name") { + Some(values) if values.length() > 0 => DecodeName::{ name: values[0] } + _ => raise @argparse.ArgParseError::MissingRequired("name") + } +} + +///| +test "render help snapshot with groups and hidden entries" { + let cmd = @argparse.Command( + "render", + groups=[ + @argparse.ArgGroup("mode", required=true, multiple=false, args=[ + "fast", "path", + ]), + ], + subcommands=[ + @argparse.Command("run", about="run"), + @argparse.Command("hidden", about="hidden", hidden=true), + ], + args=[ + @argparse.FlagArg("fast", short='f', long="fast", group="mode"), + @argparse.FlagArg("slow", long="slow", group="mode", hidden=true), + @argparse.FlagArg("cache", long="cache", negatable=true, about="cache"), + @argparse.OptionArg( + "path", + short='p', + long="path", + env="PATH_ENV", + default_values=["a", "b"], + required=true, + group="mode", + ), + @argparse.PositionalArg("target", index=0, required=true), + @argparse.PositionalArg( + "rest", + index=1, + num_args=@argparse.ValueRange(lower=0), + ), + @argparse.PositionalArg("secret", index=2, hidden=true), + ], + ) + inspect( + cmd.render_help(), + content=( + #|Usage: render [options] [rest...] + #| + #|Commands: + #| run run + #| help Print help for the subcommand(s). + #| + #|Arguments: + #| target required + #| rest... + #| + #|Options: + #| -h, --help Show help information. + #| -f, --fast + #| --[no-]cache cache + #| -p, --path env: PATH_ENV, defaults: a, b, required + #| + #|Groups: + #| mode (required, exclusive) -f, --fast, -p, --path + #| + ), + ) +} + +///| +test "render help conversion coverage snapshot" { + let cmd = @argparse.Command( + "shape", + groups=[@argparse.ArgGroup("grp", args=["f", "opt", "pos"])], + args=[ + @argparse.FlagArg( + "f", + short='f', + about="f", + env="F_ENV", + requires=["opt"], + required=true, + global=true, + hidden=true, + group="grp", + ), + @argparse.OptionArg( + "opt", + short='o', + about="opt", + default_values=["x", "y"], + env="OPT_ENV", + allow_hyphen_values=true, + last=true, + required=true, + global=true, + hidden=true, + conflicts_with=["pos"], + group="grp", + ), + @argparse.PositionalArg( + "pos", + about="pos", + env="POS_ENV", + default_values=["p1", "p2"], + num_args=@argparse.ValueRange(lower=0, upper=2), + allow_hyphen_values=true, + last=true, + requires=["opt"], + conflicts_with=["f"], + group="grp", + required=true, + global=true, + hidden=true, + ), + ], + ) + inspect( + cmd.render_help(), + content=( + #|Usage: shape + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) +} + +///| +test "count flags and sources with pattern matching" { + let cmd = @argparse.Command("demo", args=[ + @argparse.FlagArg( + "verbose", + short='v', + long="verbose", + action=@argparse.FlagAction::Count, + ), + ]) + let matches = cmd.parse(argv=["-v", "-v", "-v"], env=empty_env()) catch { + _ => panic() + } + assert_true(matches.flags is { "verbose": true, .. }) + assert_true(matches.flag_counts is { "verbose": 3, .. }) + assert_true(matches.sources is { "verbose": @argparse.ValueSource::Argv, .. }) +} + +///| +test "global option merges parent and child values" { + let child = @argparse.Command("run") + let cmd = @argparse.Command( + "demo", + args=[ + @argparse.OptionArg( + "profile", + short='p', + long="profile", + action=@argparse.OptionAction::Append, + global=true, + ), + ], + subcommands=[child], + ) + + let matches = cmd.parse( + argv=["--profile", "parent", "run", "--profile", "child"], + env=empty_env(), + ) catch { + _ => panic() + } + assert_true(matches.values is { "profile": ["parent", "child"], .. }) + assert_true(matches.sources is { "profile": @argparse.ValueSource::Argv, .. }) + assert_true( + matches.subcommand is Some(("run", sub)) && + sub.values is { "profile": ["parent", "child"], .. }, + ) +} + +///| +test "global append keeps parent argv over child env/default" { + let child = @argparse.Command("run") + let cmd = @argparse.Command( + "demo", + args=[ + @argparse.OptionArg( + "profile", + long="profile", + action=@argparse.OptionAction::Append, + env="PROFILE", + default_values=["def"], + global=true, + ), + ], + subcommands=[child], + ) + + let matches = cmd.parse(argv=["--profile", "parent", "run"], env={ + "PROFILE": "env", + }) catch { + _ => panic() + } + assert_true(matches.values is { "profile": ["parent"], .. }) + assert_true(matches.sources is { "profile": @argparse.ValueSource::Argv, .. }) + assert_true( + matches.subcommand is Some(("run", sub)) && + sub.values is { "profile": ["parent"], .. } && + sub.sources is { "profile": @argparse.ValueSource::Argv, .. }, + ) +} + +///| +test "global scalar keeps parent argv over child env/default" { + let child = @argparse.Command("run") + let cmd = @argparse.Command( + "demo", + args=[ + @argparse.OptionArg( + "profile", + long="profile", + env="PROFILE", + default_values=["def"], + global=true, + ), + ], + subcommands=[child], + ) + + let matches = cmd.parse(argv=["--profile", "parent", "run"], env={ + "PROFILE": "env", + }) catch { + _ => panic() + } + assert_true(matches.values is { "profile": ["parent"], .. }) + assert_true(matches.sources is { "profile": @argparse.ValueSource::Argv, .. }) + assert_true( + matches.subcommand is Some(("run", sub)) && + sub.values is { "profile": ["parent"], .. } && + sub.sources is { "profile": @argparse.ValueSource::Argv, .. }, + ) +} + +///| +test "global count merges parent and child occurrences" { + let child = @argparse.Command("run") + let cmd = @argparse.Command( + "demo", + args=[ + @argparse.FlagArg( + "verbose", + short='v', + long="verbose", + action=@argparse.FlagAction::Count, + global=true, + ), + ], + subcommands=[child], + ) + + let matches = cmd.parse(argv=["-v", "run", "-v", "-v"], env=empty_env()) catch { + _ => panic() + } + assert_true(matches.flag_counts is { "verbose": 3, .. }) + assert_true( + matches.subcommand is Some(("run", sub)) && + sub.flag_counts is { "verbose": 3, .. }, + ) +} + +///| +test "global count keeps parent argv over child env fallback" { + let child = @argparse.Command("run") + let cmd = @argparse.Command( + "demo", + args=[ + @argparse.FlagArg( + "verbose", + short='v', + long="verbose", + action=@argparse.FlagAction::Count, + env="VERBOSE", + global=true, + ), + ], + subcommands=[child], + ) + + let matches = cmd.parse(argv=["-v", "run"], env={ "VERBOSE": "1" }) catch { + _ => panic() + } + assert_true(matches.flag_counts is { "verbose": 1, .. }) + assert_true(matches.sources is { "verbose": @argparse.ValueSource::Argv, .. }) + assert_true( + matches.subcommand is Some(("run", sub)) && + sub.flag_counts is { "verbose": 1, .. } && + sub.sources is { "verbose": @argparse.ValueSource::Argv, .. }, + ) +} + +///| +test "global flag keeps parent argv over child env fallback" { + let child = @argparse.Command("run") + let cmd = @argparse.Command( + "demo", + args=[ + @argparse.FlagArg("verbose", long="verbose", env="VERBOSE", global=true), + ], + subcommands=[child], + ) + + let matches = cmd.parse(argv=["--verbose", "run"], env={ "VERBOSE": "0" }) catch { + _ => panic() + } + assert_true(matches.flags is { "verbose": true, .. }) + assert_true(matches.sources is { "verbose": @argparse.ValueSource::Argv, .. }) + assert_true( + matches.subcommand is Some(("run", sub)) && + sub.flags is { "verbose": true, .. } && + sub.sources is { "verbose": @argparse.ValueSource::Argv, .. }, + ) +} + +///| +test "global count source keeps env across subcommand merge" { + let child = @argparse.Command("run") + let cmd = @argparse.Command( + "demo", + args=[ + @argparse.FlagArg( + "verbose", + short='v', + long="verbose", + action=@argparse.FlagAction::Count, + env="VERBOSE", + global=true, + ), + ], + subcommands=[child], + ) + + let matches = cmd.parse(argv=["run"], env={ "VERBOSE": "1" }) catch { + _ => panic() + } + assert_true(matches.flags is { "verbose": true, .. }) + assert_true(matches.flag_counts is { "verbose": 1, .. }) + assert_true(matches.sources is { "verbose": @argparse.ValueSource::Env, .. }) + assert_true( + matches.subcommand is Some(("run", sub)) && + sub.flag_counts is { "verbose": 1, .. } && + sub.sources is { "verbose": @argparse.ValueSource::Env, .. }, + ) +} + +///| +test "help subcommand styles and errors" { + let leaf = @argparse.Command("echo", about="echo") + let cmd = @argparse.Command("demo", subcommands=[leaf]) + + try cmd.parse(argv=["help", "echo", "-h"], env=empty_env()) catch { + @argparse.DisplayHelp::Message(text) => + inspect( + text, + content=( + #|Usage: echo + #| + #|echo + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=["help", "echo", "--help"], env=empty_env()) catch { + @argparse.DisplayHelp::Message(text) => + inspect( + text, + content=( + #|Usage: echo + #| + #|echo + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=["help"], env=empty_env()) catch { + @argparse.DisplayHelp::Message(text) => + inspect( + text, + content=( + #|Usage: demo + #| + #|Commands: + #| echo echo + #| help Print help for the subcommand(s). + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=["help", "--bad"], env=empty_env()) catch { + @argparse.ArgParseError::InvalidArgument(msg) => + inspect( + msg, + content=( + #|unexpected help argument: --bad + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=["help", "missing"], env=empty_env()) catch { + @argparse.ArgParseError::InvalidArgument(msg) => + inspect( + msg, + content=( + #|unknown subcommand: missing + ), + ) + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "subcommand help includes inherited global options" { + let leaf = @argparse.Command("echo", about="echo") + let cmd = @argparse.Command( + "demo", + args=[ + @argparse.FlagArg( + "verbose", + short='v', + long="verbose", + about="Enable verbose mode", + global=true, + ), + ], + subcommands=[leaf], + ) + + try cmd.parse(argv=["echo", "-h"], env=empty_env()) catch { + @argparse.DisplayHelp::Message(text) => { + assert_true(text.contains("Usage: echo [options]")) + assert_true(text.contains("-v, --verbose")) + } + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=["help", "echo"], env=empty_env()) catch { + @argparse.DisplayHelp::Message(text) => { + assert_true(text.contains("Usage: echo [options]")) + assert_true(text.contains("-v, --verbose")) + } + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "unknown argument suggestions are exposed" { + let cmd = @argparse.Command("demo", args=[ + @argparse.FlagArg("verbose", short='v', long="verbose"), + ]) + + try cmd.parse(argv=["--verbse"], env=empty_env()) catch { + @argparse.ArgParseError::UnknownArgument(arg, hint) => { + assert_true(arg == "--verbse") + assert_true(hint is Some("--verbose")) + } + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=["-x"], env=empty_env()) catch { + @argparse.ArgParseError::UnknownArgument(arg, hint) => { + assert_true(arg == "-x") + assert_true(hint is Some("-v")) + } + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=["--zzzzzzzzzz"], env=empty_env()) catch { + @argparse.ArgParseError::UnknownArgument(arg, hint) => { + assert_true(arg == "--zzzzzzzzzz") + assert_true(hint is None) + } + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "long and short value parsing branches" { + let cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg("count", short='c', long="count"), + ]) + + let long_inline = cmd.parse(argv=["--count=2"], env=empty_env()) catch { + _ => panic() + } + assert_true(long_inline.values is { "count": ["2"], .. }) + + let short_inline = cmd.parse(argv=["-c=3"], env=empty_env()) catch { + _ => panic() + } + assert_true(short_inline.values is { "count": ["3"], .. }) + + let short_attached = cmd.parse(argv=["-c4"], env=empty_env()) catch { + _ => panic() + } + assert_true(short_attached.values is { "count": ["4"], .. }) + + try cmd.parse(argv=["--count"], env=empty_env()) catch { + @argparse.ArgParseError::MissingValue(name) => + assert_true(name == "--count") + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=["-c"], env=empty_env()) catch { + @argparse.ArgParseError::MissingValue(name) => assert_true(name == "-c") + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "append option action is publicly selectable" { + let cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg( + "tag", + long="tag", + action=@argparse.OptionAction::Append, + ), + ]) + let appended = cmd.parse(argv=["--tag", "a", "--tag", "b"], env=empty_env()) catch { + _ => panic() + } + assert_true(appended.values is { "tag": ["a", "b"], .. }) + assert_true(appended.sources is { "tag": @argparse.ValueSource::Argv, .. }) +} + +///| +test "negation parsing and invalid negation forms" { + let cmd = @argparse.Command("demo", args=[ + @argparse.FlagArg("cache", long="cache", negatable=true), + @argparse.OptionArg("path", long="path"), + ]) + + let off = cmd.parse(argv=["--no-cache"], env=empty_env()) catch { + _ => panic() + } + assert_true(off.flags is { "cache": false, .. }) + assert_true(off.sources is { "cache": @argparse.ValueSource::Argv, .. }) + + try cmd.parse(argv=["--no-path"], env=empty_env()) catch { + @argparse.ArgParseError::UnknownArgument(arg, _) => + assert_true(arg == "--no-path") + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=["--no-missing"], env=empty_env()) catch { + @argparse.ArgParseError::UnknownArgument(arg, _) => + assert_true(arg == "--no-missing") + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=["--no-cache=1"], env=empty_env()) catch { + @argparse.ArgParseError::InvalidArgument(arg) => + assert_true(arg == "--no-cache=1") + _ => panic() + } noraise { + _ => panic() + } + + let count_cmd = @argparse.Command("demo", args=[ + @argparse.FlagArg( + "verbose", + long="verbose", + action=@argparse.FlagAction::Count, + negatable=true, + ), + ]) + let reset = count_cmd.parse( + argv=["--verbose", "--no-verbose"], + env=empty_env(), + ) catch { + _ => panic() + } + assert_true(reset.flags is { "verbose": false, .. }) + assert_true(reset.flag_counts is { "verbose"? : None, .. }) + assert_true(reset.sources is { "verbose": @argparse.ValueSource::Argv, .. }) +} + +///| +test "positionals force mode and dash handling" { + let force_cmd = @argparse.Command("demo", args=[ + @argparse.PositionalArg( + "tail", + index=0, + num_args=@argparse.ValueRange(lower=0), + last=true, + allow_hyphen_values=true, + ), + ]) + let forced = force_cmd.parse(argv=["a", "--x", "-y"], env=empty_env()) catch { + _ => panic() + } + assert_true(forced.values is { "tail": ["a", "--x", "-y"], .. }) + + let dashed = force_cmd.parse(argv=["--", "p", "q"], env=empty_env()) catch { + _ => panic() + } + assert_true(dashed.values is { "tail": ["p", "q"], .. }) + + let negative_cmd = @argparse.Command("demo", args=[ + @argparse.PositionalArg("n", index=0), + ]) + let negative = negative_cmd.parse(argv=["-9"], env=empty_env()) catch { + _ => panic() + } + assert_true(negative.values is { "n": ["-9"], .. }) + + try negative_cmd.parse(argv=["x", "y"], env=empty_env()) catch { + @argparse.ArgParseError::TooManyPositionals => () + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "bounded positional does not greedily consume later required values" { + let cmd = @argparse.Command("demo", args=[ + @argparse.PositionalArg( + "first", + index=0, + num_args=@argparse.ValueRange(lower=1, upper=2), + ), + @argparse.PositionalArg("second", index=1, required=true), + ]) + + let two = cmd.parse(argv=["a", "b"], env=empty_env()) catch { _ => panic() } + assert_true(two.values is { "first": ["a"], "second": ["b"], .. }) + + let three = cmd.parse(argv=["a", "b", "c"], env=empty_env()) catch { + _ => panic() + } + assert_true(three.values is { "first": ["a", "b"], "second": ["c"], .. }) +} + +///| +test "env parsing for settrue setfalse count and invalid values" { + let cmd = @argparse.Command("demo", args=[ + @argparse.FlagArg( + "on", + long="on", + action=@argparse.FlagAction::SetTrue, + env="ON", + ), + @argparse.FlagArg( + "off", + long="off", + action=@argparse.FlagAction::SetFalse, + env="OFF", + ), + @argparse.FlagArg( + "v", + long="v", + action=@argparse.FlagAction::Count, + env="V", + ), + ]) + + let parsed = cmd.parse(argv=[], env={ "ON": "true", "OFF": "true", "V": "3" }) catch { + _ => panic() + } + assert_true(parsed.flags is { "on": true, "off": false, "v": true, .. }) + assert_true(parsed.flag_counts is { "v": 3, .. }) + assert_true( + parsed.sources + is { + "on": @argparse.ValueSource::Env, + "off": @argparse.ValueSource::Env, + "v": @argparse.ValueSource::Env, + .. + }, + ) + + try cmd.parse(argv=[], env={ "ON": "bad" }) catch { + @argparse.ArgParseError::InvalidValue(msg) => + inspect( + msg, + content=( + #|invalid value 'bad' for boolean flag; expected one of: 1, 0, true, false, yes, no, on, off + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=[], env={ "OFF": "bad" }) catch { + @argparse.ArgParseError::InvalidValue(msg) => + inspect( + msg, + content=( + #|invalid value 'bad' for boolean flag; expected one of: 1, 0, true, false, yes, no, on, off + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=[], env={ "V": "bad" }) catch { + @argparse.ArgParseError::InvalidValue(msg) => + inspect( + msg, + content=( + #|invalid value 'bad' for count; expected a non-negative integer + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=[], env={ "V": "-1" }) catch { + @argparse.ArgParseError::InvalidValue(msg) => + inspect( + msg, + content=( + #|invalid value '-1' for count; expected a non-negative integer + ), + ) + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "defaults and value range helpers through public API" { + let defaults = @argparse.Command("demo", args=[ + @argparse.OptionArg( + "mode", + long="mode", + default_values=["a", "b"], + num_args=@argparse.ValueRange(lower=0), + ), + @argparse.OptionArg("one", long="one", default_values=["x"]), + ]) + let by_default = defaults.parse(argv=[], env=empty_env()) catch { + _ => panic() + } + assert_true(by_default.values is { "mode": ["a", "b"], "one": ["x"], .. }) + assert_true( + by_default.sources + is { + "mode": @argparse.ValueSource::Default, + "one": @argparse.ValueSource::Default, + .. + }, + ) + + let upper_only = @argparse.Command("demo", args=[ + @argparse.OptionArg( + "tag", + long="tag", + num_args=@argparse.ValueRange(upper=2), + ), + ]) + try + upper_only.parse( + argv=["--tag", "a", "--tag", "b", "--tag", "c"], + env=empty_env(), + ) + catch { + @argparse.ArgParseError::TooManyValues(name, got, max) => { + assert_true(name == "tag") + assert_true(got == 3) + assert_true(max == 2) + } + _ => panic() + } noraise { + _ => panic() + } + + let lower_only = @argparse.Command("demo", args=[ + @argparse.OptionArg( + "tag", + long="tag", + num_args=@argparse.ValueRange(lower=1), + ), + ]) + try lower_only.parse(argv=[], env=empty_env()) catch { + @argparse.ArgParseError::TooFewValues(name, got, min) => { + assert_true(name == "tag") + assert_true(got == 0) + assert_true(min == 1) + } + _ => panic() + } noraise { + _ => panic() + } + + let empty_range = @argparse.ValueRange::empty() + let single_range = @argparse.ValueRange::single() + inspect( + (empty_range, single_range), + content=( + #|({lower: Some(0), upper: Some(0), lower_inclusive: true, upper_inclusive: true}, {lower: Some(1), upper: Some(1), lower_inclusive: true, upper_inclusive: true}) + ), + ) +} + +///| +test "num_args options consume argv values in one occurrence" { + let cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg( + "tag", + long="tag", + num_args=@argparse.ValueRange(lower=2, upper=2), + ), + ]) + let parsed = cmd.parse(argv=["--tag", "a", "b"], env=empty_env()) catch { + _ => panic() + } + assert_true(parsed.values is { "tag": ["a", "b"], .. }) + assert_true(parsed.sources is { "tag": @argparse.ValueSource::Argv, .. }) +} + +///| +test "from_matches uses public decoding hook" { + let cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg("name", long="name"), + ]) + let matches = cmd.parse(argv=["--name", "alice"], env=empty_env()) catch { + _ => panic() + } + let decoded : DecodeName = @argparse.from_matches(matches) catch { + _ => panic() + } + assert_true(decoded.name == "alice") +} + +///| +test "default argv path is reachable" { + let cmd = @argparse.Command("demo", args=[ + @argparse.PositionalArg( + "rest", + num_args=@argparse.ValueRange(lower=0), + allow_hyphen_values=true, + ), + ]) + let _ = cmd.parse(env=empty_env()) catch { _ => panic() } +} + +///| +test "validation branches exposed through parse" { + try + @argparse.Command("demo", args=[ + @argparse.OptionArg("x", long="x", last=true), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|positional-only settings require no short/long + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", args=[ + @argparse.FlagArg("f", action=@argparse.FlagAction::Help), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|help/version actions require short/long option + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", args=[ + @argparse.FlagArg( + "f", + long="f", + action=@argparse.FlagAction::Help, + negatable=true, + ), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|help/version actions do not support negatable + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", args=[ + @argparse.FlagArg( + "f", + long="f", + action=@argparse.FlagAction::Help, + env="F", + ), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|help/version actions do not support env/defaults + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + let ranged = @argparse.Command("demo", args=[ + @argparse.OptionArg( + "x", + long="x", + num_args=@argparse.ValueRange(lower=2, upper=2), + ), + ]).parse(argv=["--x", "a", "--x", "b"], env=empty_env()) catch { + _ => panic() + } + assert_true(ranged.values is { "x": ["a", "b"], .. }) + + try + @argparse.Command("demo", args=[ + @argparse.OptionArg("x", long="x", default_values=["a", "b"]), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|default_values require action=Append or num_args allowing >1 + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", args=[ + @argparse.OptionArg( + "x", + long="x", + num_args=@argparse.ValueRange(lower=3, upper=2), + ), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|max values must be >= min values + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", groups=[ + @argparse.ArgGroup("g"), + @argparse.ArgGroup("g"), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|duplicate group: g + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", groups=[@argparse.ArgGroup("g", requires=["g"])]).parse( + argv=[], + env=empty_env(), + ) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|group cannot require itself: g + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", groups=[ + @argparse.ArgGroup("g", conflicts_with=["g"]), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|group cannot conflict with itself: g + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", groups=[@argparse.ArgGroup("g", args=["missing"])]).parse( + argv=[], + env=empty_env(), + ) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|unknown group arg: g -> missing + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", args=[ + @argparse.OptionArg("x", long="x"), + @argparse.OptionArg("x", long="y"), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|duplicate arg name: x + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", args=[ + @argparse.OptionArg("x", long="same"), + @argparse.OptionArg("y", long="same"), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|duplicate long option: --same + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", args=[ + @argparse.FlagArg("hello", long="hello", negatable=true), + @argparse.FlagArg("x", long="no-hello"), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|duplicate long option: --no-hello + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", args=[ + @argparse.OptionArg("x", short='s'), + @argparse.OptionArg("y", short='s'), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|duplicate short option: -s + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", args=[ + @argparse.FlagArg("x", long="x", requires=["x"]), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|arg cannot require itself: x + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", args=[ + @argparse.FlagArg("x", long="x", conflicts_with=["x"]), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|arg cannot conflict with itself: x + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", subcommands=[ + @argparse.Command("x"), + @argparse.Command("x"), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|duplicate subcommand: x + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", subcommand_required=true).parse( + argv=[], + env=empty_env(), + ) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|subcommand_required requires at least one subcommand + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", subcommands=[@argparse.Command("help")]).parse( + argv=[], + env=empty_env(), + ) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|subcommand name reserved for built-in help: help (disable with disable_help_subcommand) + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + let custom_help = @argparse.Command("demo", args=[ + @argparse.FlagArg( + "custom_help", + short='h', + long="help", + about="custom help", + ), + ]) + let help_short = custom_help.parse(argv=["-h"], env=empty_env()) catch { + _ => panic() + } + let help_long = custom_help.parse(argv=["--help"], env=empty_env()) catch { + _ => panic() + } + assert_true(help_short.flags is { "custom_help": true, .. }) + assert_true(help_long.flags is { "custom_help": true, .. }) + inspect( + custom_help.render_help(), + content=( + #|Usage: demo [options] + #| + #|Options: + #| -h, --help custom help + #| + ), + ) + + let custom_version = @argparse.Command("demo", version="1.0", args=[ + @argparse.FlagArg( + "custom_version", + short='V', + long="version", + about="custom version", + ), + ]) + let version_short = custom_version.parse(argv=["-V"], env=empty_env()) catch { + _ => panic() + } + let version_long = custom_version.parse(argv=["--version"], env=empty_env()) catch { + _ => panic() + } + assert_true(version_short.flags is { "custom_version": true, .. }) + assert_true(version_long.flags is { "custom_version": true, .. }) + inspect( + custom_version.render_help(), + content=( + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| -V, --version custom version + #| + ), + ) + + try + @argparse.Command("demo", args=[ + @argparse.FlagArg("v", long="v", action=@argparse.FlagAction::Version), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|version action requires command version text + ), + ) + _ => panic() + } noraise { + _ => panic() + } +} diff --git a/argparse/argparse_test.mbt b/argparse/argparse_test.mbt new file mode 100644 index 000000000..4f01465c4 --- /dev/null +++ b/argparse/argparse_test.mbt @@ -0,0 +1,444 @@ +// 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. + +///| +fn empty_env() -> Map[String, String] { + {} +} + +///| +test "declarative parse basics" { + let cmd = @argparse.Command("demo", args=[ + @argparse.FlagArg("verbose", short='v', long="verbose"), + @argparse.OptionArg("count", long="count", env="COUNT"), + @argparse.PositionalArg("name", index=0), + ]) + let matches = cmd.parse(argv=["-v", "--count", "3", "alice"], env=empty_env()) catch { + _ => panic() + } + assert_true(matches.flags is { "verbose": true, .. }) + assert_true(matches.values is { "count": ["3"], "name": ["alice"], .. }) +} + +///| +test "display help and version" { + let cmd = @argparse.Command("demo", about="demo app", version="1.2.3") + + let mut help = "" + try cmd.parse(argv=["-h"], env=empty_env()) catch { + @argparse.DisplayHelp::Message(text) => help = text + _ => panic() + } noraise { + _ => panic() + } + inspect( + help, + content=( + #|Usage: demo + #| + #|demo app + #| + #|Options: + #| -h, --help Show help information. + #| -V, --version Show version information. + #| + ), + ) + let mut version = "" + try cmd.parse(argv=["--version"], env=empty_env()) catch { + @argparse.DisplayVersion::Message(text) => version = text + _ => panic() + } noraise { + _ => panic() + } + inspect(version, content="1.2.3") +} + +///| +test "parse error show is readable" { + inspect( + @argparse.ArgParseError::UnknownArgument("--verbse", Some("--verbose")).to_string(), + content=( + #|error: unexpected argument '--verbse' found + #| + #| tip: a similar argument exists: '--verbose' + ), + ) + inspect( + @argparse.ArgParseError::TooManyPositionals.to_string(), + content=( + #|error: too many positional arguments were provided + ), + ) +} + +///| +test "relationships and num args" { + let requires_cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg("mode", long="mode", requires=["config"]), + @argparse.OptionArg("config", long="config"), + ]) + + try requires_cmd.parse(argv=["--mode", "fast"], env=empty_env()) catch { + @argparse.ArgParseError::MissingRequired(name) => + inspect(name, content="config") + _ => panic() + } noraise { + _ => panic() + } + + let num_args_cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg( + "tag", + long="tag", + num_args=@argparse.ValueRange(lower=2, upper=2), + ), + ]) + + try num_args_cmd.parse(argv=["--tag", "a"], env=empty_env()) catch { + @argparse.ArgParseError::TooFewValues(name, got, min) => { + inspect(name, content="tag") + inspect(got, content="1") + inspect(min, content="2") + } + _ => panic() + } noraise { + _ => panic() + } + + try + num_args_cmd.parse( + argv=["--tag", "a", "--tag", "b", "--tag", "c"], + env=empty_env(), + ) + catch { + @argparse.ArgParseError::TooManyValues(name, got, max) => { + inspect(name, content="tag") + inspect(got, content="3") + inspect(max, content="2") + } + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "arg groups required and multiple" { + let cmd = @argparse.Command( + "demo", + groups=[@argparse.ArgGroup("mode", required=true, multiple=false)], + args=[ + @argparse.FlagArg("fast", long="fast", group="mode"), + @argparse.FlagArg("slow", long="slow", group="mode"), + ], + ) + + try cmd.parse(argv=[], env=empty_env()) catch { + @argparse.ArgParseError::MissingGroup(name) => inspect(name, content="mode") + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=["--fast", "--slow"], env=empty_env()) catch { + @argparse.ArgParseError::GroupConflict(name) => + inspect(name, content="mode") + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "arg groups requires and conflicts" { + let requires_cmd = @argparse.Command( + "demo", + groups=[ + @argparse.ArgGroup("mode", args=["fast"], requires=["output"]), + @argparse.ArgGroup("output", args=["json"]), + ], + args=[ + @argparse.FlagArg("fast", long="fast"), + @argparse.FlagArg("json", long="json"), + ], + ) + + try requires_cmd.parse(argv=["--fast"], env=empty_env()) catch { + @argparse.ArgParseError::MissingGroup(name) => + inspect(name, content="output") + _ => panic() + } noraise { + _ => panic() + } + + let conflict_cmd = @argparse.Command( + "demo", + groups=[ + @argparse.ArgGroup("mode", args=["fast"], conflicts_with=["output"]), + @argparse.ArgGroup("output", args=["json"]), + ], + args=[ + @argparse.FlagArg("fast", long="fast"), + @argparse.FlagArg("json", long="json"), + ], + ) + + try conflict_cmd.parse(argv=["--fast", "--json"], env=empty_env()) catch { + @argparse.ArgParseError::GroupConflict(msg) => + inspect( + msg, + content=( + #|mode conflicts with output + ), + ) + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "subcommand parsing" { + let echo = @argparse.Command("echo", args=[ + @argparse.PositionalArg("msg", index=0), + ]) + let root = @argparse.Command("root", subcommands=[echo]) + + let matches = root.parse(argv=["echo", "hi"], env=empty_env()) catch { + _ => panic() + } + assert_true( + matches.subcommand is Some(("echo", sub)) && + sub.values is { "msg": ["hi"], .. }, + ) +} + +///| +test "full help snapshot" { + let cmd = @argparse.Command( + "demo", + about="Demo command", + args=[ + @argparse.FlagArg( + "verbose", + short='v', + long="verbose", + about="Enable verbose mode", + ), + @argparse.OptionArg("count", long="count", about="Repeat count", default_values=[ + "1", + ]), + @argparse.PositionalArg("name", index=0, about="Target name"), + ], + subcommands=[@argparse.Command("echo", about="Echo a message")], + ) + inspect( + cmd.render_help(), + content=( + #|Usage: demo [options] [name] + #| + #|Demo command + #| + #|Commands: + #| echo Echo a message + #| help Print help for the subcommand(s). + #| + #|Arguments: + #| name Target name + #| + #|Options: + #| -h, --help Show help information. + #| -v, --verbose Enable verbose mode + #| --count Repeat count (default: 1) + #| + ), + ) +} + +///| +test "value source precedence argv env default" { + let cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg("level", long="level", env="LEVEL", default_values=["1"]), + ]) + + let from_default = cmd.parse(argv=[], env=empty_env()) catch { _ => panic() } + assert_true(from_default.values is { "level": ["1"], .. }) + assert_true( + from_default.sources is { "level": @argparse.ValueSource::Default, .. }, + ) + + let from_env = cmd.parse(argv=[], env={ "LEVEL": "2" }) catch { _ => panic() } + assert_true(from_env.values is { "level": ["2"], .. }) + assert_true(from_env.sources is { "level": @argparse.ValueSource::Env, .. }) + + let from_argv = cmd.parse(argv=["--level", "3"], env={ "LEVEL": "2" }) catch { + _ => panic() + } + assert_true(from_argv.values is { "level": ["3"], .. }) + assert_true(from_argv.sources is { "level": @argparse.ValueSource::Argv, .. }) +} + +///| +test "omitted env does not read process environment by default" { + let cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg("count", long="count", env="COUNT"), + ]) + let matches = cmd.parse(argv=[]) catch { _ => panic() } + assert_true(matches.values is { "count"? : None, .. }) + assert_true(matches.sources is { "count"? : None, .. }) +} + +///| +test "options and multiple values" { + let serve = @argparse.Command("serve") + let cmd = @argparse.Command( + "demo", + args=[ + @argparse.OptionArg("count", short='c', long="count"), + @argparse.OptionArg( + "tag", + long="tag", + action=@argparse.OptionAction::Append, + ), + ], + subcommands=[serve], + ) + + let long_count = cmd.parse(argv=["--count", "2"], env=empty_env()) catch { + _ => panic() + } + assert_true(long_count.values is { "count": ["2"], .. }) + + let short_count = cmd.parse(argv=["-c", "3"], env=empty_env()) catch { + _ => panic() + } + assert_true(short_count.values is { "count": ["3"], .. }) + + let multi = cmd.parse(argv=["--tag", "a", "--tag", "b"], env=empty_env()) catch { + _ => panic() + } + assert_true(multi.values is { "tag": ["a", "b"], .. }) + + let subcommand = cmd.parse(argv=["serve"], env=empty_env()) catch { + _ => panic() + } + assert_true(subcommand.subcommand is Some(("serve", _))) +} + +///| +test "negatable and conflicts" { + let cmd = @argparse.Command("demo", args=[ + @argparse.FlagArg("cache", long="cache", negatable=true), + @argparse.FlagArg( + "failfast", + long="failfast", + action=@argparse.FlagAction::SetFalse, + negatable=true, + ), + @argparse.FlagArg("verbose", long="verbose", conflicts_with=["quiet"]), + @argparse.FlagArg("quiet", long="quiet"), + ]) + + let no_cache = cmd.parse(argv=["--no-cache"], env=empty_env()) catch { + _ => panic() + } + assert_true(no_cache.flags is { "cache": false, .. }) + assert_true(no_cache.sources is { "cache": @argparse.ValueSource::Argv, .. }) + + let no_failfast = cmd.parse(argv=["--no-failfast"], env=empty_env()) catch { + _ => panic() + } + assert_true(no_failfast.flags is { "failfast": true, .. }) + + try cmd.parse(argv=["--verbose", "--quiet"], env=empty_env()) catch { + @argparse.ArgParseError::InvalidArgument(msg) => + inspect( + msg, + content=( + #|conflicting arguments: verbose and quiet + ), + ) + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "flag does not accept inline value" { + let cmd = @argparse.Command("demo", args=[ + @argparse.FlagArg("verbose", long="verbose"), + ]) + try cmd.parse(argv=["--verbose=true"], env=empty_env()) catch { + @argparse.ArgParseError::InvalidArgument(arg) => + inspect(arg, content="--verbose=true") + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "built-in long flags do not accept inline value" { + let cmd = @argparse.Command("demo", version="1.2.3") + + try cmd.parse(argv=["--help=1"], env=empty_env()) catch { + @argparse.ArgParseError::InvalidArgument(arg) => + inspect(arg, content="--help=1") + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=["--version=1"], env=empty_env()) catch { + @argparse.ArgParseError::InvalidArgument(arg) => + inspect(arg, content="--version=1") + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "command policies" { + let help_cmd = @argparse.Command("demo", arg_required_else_help=true) + try help_cmd.parse(argv=[], env=empty_env()) catch { + @argparse.DisplayHelp::Message(text) => + inspect( + text, + content=( + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + let sub_cmd = @argparse.Command("demo", subcommand_required=true, subcommands=[ + @argparse.Command("echo"), + ]) + try sub_cmd.parse(argv=[], env=empty_env()) catch { + @argparse.ArgParseError::MissingRequired(name) => + inspect(name, content="subcommand") + _ => panic() + } noraise { + _ => panic() + } +} diff --git a/argparse/command.mbt b/argparse/command.mbt new file mode 100644 index 000000000..07254510c --- /dev/null +++ b/argparse/command.mbt @@ -0,0 +1,285 @@ +// 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 command specification. +pub struct Command { + priv name : String + priv args : Array[Arg] + priv groups : Array[ArgGroup] + priv subcommands : Array[Command] + priv about : String? + priv version : String? + priv disable_help_flag : Bool + priv disable_version_flag : Bool + priv disable_help_subcommand : Bool + priv arg_required_else_help : Bool + priv subcommand_required : Bool + priv hidden : Bool + + fn new( + name : String, + args? : Array[&ArgLike], + subcommands? : Array[Command], + about? : String, + version? : String, + disable_help_flag? : Bool, + disable_version_flag? : Bool, + disable_help_subcommand? : Bool, + arg_required_else_help? : Bool, + subcommand_required? : Bool, + hidden? : Bool, + groups? : Array[ArgGroup], + ) -> Command +} + +///| +pub fn Command::new( + name : String, + args? : Array[&ArgLike] = [], + subcommands? : Array[Command] = [], + about? : String, + version? : String, + disable_help_flag? : Bool = false, + disable_version_flag? : Bool = false, + disable_help_subcommand? : Bool = false, + arg_required_else_help? : Bool = false, + subcommand_required? : Bool = false, + hidden? : Bool = false, + groups? : Array[ArgGroup] = [], +) -> Command { + Command::{ + name, + args: normalize_args(args), + groups: clone_array_cmd(groups), + subcommands: clone_array_cmd(subcommands), + about, + version, + disable_help_flag, + disable_version_flag, + disable_help_subcommand, + arg_required_else_help, + subcommand_required, + hidden, + } +} + +///| +fn normalize_args(args : Array[&ArgLike]) -> Array[Arg] { + let out = Array::new(capacity=args.length()) + for arg in args { + out.push(arg.to_arg()) + } + out +} + +///| +/// Render help text without parsing. +pub fn Command::render_help(self : Command) -> String { + render_help(self) +} + +///| +/// Parse argv/environment according to this command spec. +pub fn Command::parse( + self : Command, + argv? : Array[String] = default_argv(), + env? : Map[String, String] = {}, +) -> Matches raise { + let raw = parse_command(self, argv, env, []) + build_matches(self, raw, []) +} + +///| +fn build_matches( + cmd : Command, + raw : Matches, + inherited_globals : Array[Arg], +) -> Matches { + let flags : Map[String, Bool] = {} + let values : Map[String, Array[String]] = {} + let flag_counts : Map[String, Int] = {} + let sources : Map[String, ValueSource] = {} + let specs = concat_decl_specs(inherited_globals, cmd.args) + + for spec in specs { + let name = arg_name(spec) + match raw.values.get(name) { + Some(vs) => values[name] = clone_array_cmd(vs) + None => () + } + let count = raw.counts.get(name).unwrap_or(0) + if count > 0 { + flag_counts[name] = count + } + let source = match raw.flag_sources.get(name) { + Some(v) => Some(v) + None => + match raw.value_sources.get(name) { + Some(vs) => highest_source(vs) + None => None + } + } + match source { + Some(source) => { + sources[name] = source + if is_flag_spec(spec) { + if is_count_flag_spec(spec) { + flags[name] = count > 0 + } else { + flags[name] = raw.flags.get(name).unwrap_or(false) + } + } + } + None => () + } + } + let child_globals = concat_decl_specs( + inherited_globals, + collect_decl_globals(cmd.args), + ) + + let subcommand = match raw.parsed_subcommand { + Some((name, sub_raw)) => + match find_decl_subcommand(cmd.subcommands, name) { + Some(sub_spec) => + Some((name, build_matches(sub_spec, sub_raw, child_globals))) + None => + Some( + ( + name, + Matches::{ + flags: {}, + values: {}, + flag_counts: {}, + sources: {}, + subcommand: None, + counts: {}, + flag_sources: {}, + value_sources: {}, + parsed_subcommand: None, + }, + ), + ) + } + None => None + } + + Matches::{ + flags, + values, + flag_counts, + sources, + subcommand, + counts: {}, + flag_sources: {}, + value_sources: {}, + parsed_subcommand: None, + } +} + +///| +fn collect_decl_globals(args : Array[Arg]) -> Array[Arg] { + let globals = [] + for arg in args { + if arg.global && (arg.long is Some(_) || arg.short is Some(_)) { + globals.push(arg) + } + } + globals +} + +///| +fn concat_decl_specs(parent : Array[Arg], more : Array[Arg]) -> Array[Arg] { + let out = clone_array_cmd(parent) + for arg in more { + out.push(arg) + } + out +} + +///| +fn find_decl_subcommand(subs : Array[Command], name : String) -> Command? { + for sub in subs { + if sub.name == name { + return Some(sub) + } + } + None +} + +///| +fn command_args(cmd : Command) -> Array[Arg] { + let args = Array::new(capacity=cmd.args.length()) + for spec in cmd.args { + args.push(spec) + } + args +} + +///| +fn command_groups(cmd : Command) -> Array[ArgGroup] { + let groups = clone_array_cmd(cmd.groups) + for arg in cmd.args { + match arg.group { + Some(group_name) => + add_arg_to_group_membership(groups, group_name, arg_name(arg)) + None => () + } + } + groups +} + +///| +fn add_arg_to_group_membership( + groups : Array[ArgGroup], + group_name : String, + arg_name : String, +) -> Unit { + let mut idx : Int? = None + for i = 0; i < groups.length(); i = i + 1 { + if groups[i].name == group_name { + idx = Some(i) + break + } + } + match idx { + Some(i) => { + if groups[i].args.contains(arg_name) { + return + } + let args = clone_array_cmd(groups[i].args) + args.push(arg_name) + groups[i] = ArgGroup::{ ..groups[i], args, } + } + None => + groups.push(ArgGroup::{ + name: group_name, + required: false, + multiple: true, + args: [arg_name], + requires: [], + conflicts_with: [], + }) + } +} + +///| +fn[T] clone_array_cmd(arr : Array[T]) -> Array[T] { + let out = Array::new(capacity=arr.length()) + for value in arr { + out.push(value) + } + out +} diff --git a/argparse/error.mbt b/argparse/error.mbt new file mode 100644 index 000000000..fcdf17ba4 --- /dev/null +++ b/argparse/error.mbt @@ -0,0 +1,85 @@ +// 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. + +///| +/// Errors raised during parsing or decoding of arguments. +pub(all) suberror ArgParseError { + UnknownArgument(String, String?) + InvalidArgument(String) + MissingValue(String) + MissingRequired(String) + TooFewValues(String, Int, Int) + TooManyValues(String, Int, Int) + TooManyPositionals + InvalidValue(String) + MissingGroup(String) + GroupConflict(String) +} + +///| +pub impl Show for ArgParseError with output(self : ArgParseError, logger) { + logger.write_string(self.arg_parse_error_message()) +} + +///| +fn ArgParseError::arg_parse_error_message(self : ArgParseError) -> String { + match self { + ArgParseError::UnknownArgument(arg, Some(hint)) => + ( + $|error: unexpected argument '\{arg}' found + $| + $| tip: a similar argument exists: '\{hint}' + ) + ArgParseError::UnknownArgument(arg, None) => + "error: unexpected argument '\{arg}' found" + ArgParseError::InvalidArgument(arg) => + if arg.has_prefix("-") { + "error: unexpected argument '\{arg}' found" + } else { + "error: \{arg}" + } + ArgParseError::MissingValue(arg) => + "error: a value is required for '\{arg}' but none was supplied" + ArgParseError::MissingRequired(name) => + "error: the following required argument was not provided: '\{name}'" + ArgParseError::TooFewValues(name, got, min) => + "error: '\{name}' requires at least \{min} values but only \{got} were provided" + ArgParseError::TooManyValues(name, got, max) => + "error: '\{name}' allows at most \{max} values but \{got} were provided" + ArgParseError::TooManyPositionals => + "error: too many positional arguments were provided" + ArgParseError::InvalidValue(msg) => "error: \{msg}" + ArgParseError::MissingGroup(name) => + "error: the following required argument group was not provided: '\{name}'" + ArgParseError::GroupConflict(name) => "error: group conflict \{name}" + } +} + +///| +/// Errors raised when building argument specifications. +pub suberror ArgBuildError { + Unsupported(String) +} derive(Show) + +///| +/// Errors raised when help information is requested. +pub suberror DisplayHelp { + Message(String) +} derive(Show) + +///| +/// Errors raised when version information is requested. +pub suberror DisplayVersion { + Message(String) +} derive(Show) diff --git a/argparse/help_render.mbt b/argparse/help_render.mbt new file mode 100644 index 000000000..cbcbefbb9 --- /dev/null +++ b/argparse/help_render.mbt @@ -0,0 +1,423 @@ +// 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. + +///| +/// Render help text for a clap-style command. +fn render_help(cmd : Command) -> String { + let usage_line = "Usage: \{cmd.name}\{usage_tail(cmd)}" + let about = command_about(cmd) + let about_section = if about == "" { + "" + } else { + ( + $| + $| + $|\{about} + ) + } + let command_lines = subcommand_entries(cmd) + let commands_section = if command_lines.length() == 0 { + "" + } else { + let body = command_lines.join("\n") + ( + $| + $| + $|Commands: + $|\{body} + ) + } + let argument_lines = positional_entries(cmd) + let arguments_section = if argument_lines.length() == 0 { + "" + } else { + let body = argument_lines.join("\n") + ( + $| + $| + $|Arguments: + $|\{body} + ) + } + let option_lines = option_entries(cmd) + let options_section = if option_lines.length() == 0 { + ( + $| + $| + $|Options: + ) + } else { + let body = option_lines.join("\n") + ( + $| + $| + $|Options: + $|\{body} + ) + } + let group_lines = group_entries(cmd) + let groups_section = if group_lines.length() == 0 { + "" + } else { + let body = group_lines.join("\n") + ( + $| + $| + $|Groups: + $|\{body} + ) + } + ( + $|\{usage_line}\{about_section}\{commands_section}\{arguments_section}\{options_section}\{groups_section} + $| + ) +} + +///| +fn usage_tail(cmd : Command) -> String { + let mut tail = "" + if has_options(cmd) { + tail = "\{tail} [options]" + } + if has_subcommands_for_help(cmd) { + tail = "\{tail} " + } + let pos = positional_usage(cmd) + if pos != "" { + tail = "\{tail} \{pos}" + } + tail +} + +///| +fn has_options(cmd : Command) -> Bool { + for arg in command_args(cmd) { + if arg_hidden(arg) { + continue + } + if arg.long is Some(_) || arg.short is Some(_) { + return true + } + } + false +} + +///| +fn positional_usage(cmd : Command) -> String { + let parts = Array::new(capacity=command_args(cmd).length()) + for arg in positional_args(command_args(cmd)) { + if arg_hidden(arg) { + continue + } + let required = is_required_arg(arg) + if arg.multiple { + if required { + parts.push("<\{arg.name}...>") + } else { + parts.push("[\{arg.name}...]") + } + } else if required { + parts.push("<\{arg.name}>") + } else { + parts.push("[\{arg.name}]") + } + } + parts.join(" ") +} + +///| +fn option_entries(cmd : Command) -> Array[String] { + let args = command_args(cmd) + let display = Array::new(capacity=args.length() + 2) + let builtin_help_short = help_flag_enabled(cmd) && + !has_short_option(args, 'h') + let builtin_help_long = help_flag_enabled(cmd) && + !has_long_option(args, "help") + let builtin_version_short = version_flag_enabled(cmd) && + !has_short_option(args, 'V') + let builtin_version_long = version_flag_enabled(cmd) && + !has_long_option(args, "version") + let builtin_help_label = builtin_option_label( + builtin_help_short, builtin_help_long, "-h", "--help", + ) + if builtin_help_label is Some(label) { + display.push((label, "Show help information.")) + } + let builtin_version_label = builtin_option_label( + builtin_version_short, builtin_version_long, "-V", "--version", + ) + if builtin_version_label is Some(label) { + display.push((label, "Show version information.")) + } + for arg in args { + if arg.long is None && arg.short is None { + continue + } + if arg_hidden(arg) { + continue + } + let name = if arg_takes_value(arg) { + "\{arg_display(arg)} <\{arg.name}>" + } else { + arg_display(arg) + } + display.push((name, arg_doc(arg))) + } + format_entries(display) +} + +///| +fn has_long_option(args : Array[Arg], name : String) -> Bool { + for arg in args { + if arg.long is Some(long) && long == name { + return true + } + } + false +} + +///| +fn has_short_option(args : Array[Arg], value : Char) -> Bool { + for arg in args { + if arg.short is Some(short) && short == value { + return true + } + } + false +} + +///| +fn builtin_option_label( + has_short : Bool, + has_long : Bool, + short_label : String, + long_label : String, +) -> String? { + if has_short && has_long { + Some("\{short_label}, \{long_label}") + } else if has_short { + Some(short_label) + } else if has_long { + Some(long_label) + } else { + None + } +} + +///| +fn positional_entries(cmd : Command) -> Array[String] { + let display = Array::new(capacity=command_args(cmd).length()) + for arg in positional_args(command_args(cmd)) { + if arg_hidden(arg) { + continue + } + display.push((positional_display(arg), arg_doc(arg))) + } + format_entries(display) +} + +///| +fn subcommand_entries(cmd : Command) -> Array[String] { + let display = Array::new(capacity=cmd.subcommands.length() + 1) + for sub in cmd.subcommands { + if command_hidden(sub) { + continue + } + display.push((command_display(sub), command_about(sub))) + } + if help_subcommand_enabled(cmd) { + display.push(("help", "Print help for the subcommand(s).")) + } + format_entries(display) +} + +///| +fn group_entries(cmd : Command) -> Array[String] { + let display = Array::new(capacity=command_groups(cmd).length()) + for group in command_groups(cmd) { + let members = group_members(cmd, group) + if members == "" { + continue + } + display.push((group_label(group), members)) + } + format_entries(display) +} + +///| +fn format_entries(display : Array[(String, String)]) -> Array[String] { + let entries = Array::new(capacity=display.length()) + let mut max_len = 0 + for item in display { + let (name, _) = item + if name.length() > max_len { + max_len = name.length() + } + } + for item in display { + let (name, doc) = item + let padding = " ".repeat(max_len - name.length() + 2) + entries.push(" \{name}\{padding}\{doc}") + } + entries +} + +///| +fn command_display(cmd : Command) -> String { + cmd.name +} + +///| +fn arg_display(arg : Arg) -> String { + let parts = Array::new(capacity=2) + if arg.short is Some(short) { + parts.push("-\{short}") + } + if arg.long is Some(long) { + if arg.negatable && !arg_takes_value(arg) { + parts.push("--[no-]\{long}") + } else { + parts.push("--\{long}") + } + } + if parts.length() == 0 { + arg.name + } else { + parts.join(", ") + } +} + +///| +fn positional_display(arg : Arg) -> String { + if arg.multiple { + "\{arg.name}..." + } else { + arg.name + } +} + +///| +fn command_about(cmd : Command) -> String { + cmd.about.unwrap_or("") +} + +///| +fn arg_help(arg : Arg) -> String { + arg.about.unwrap_or("") +} + +///| +fn arg_doc(arg : Arg) -> String { + let notes = [] + match arg.env { + Some(env_name) => notes.push("env: \{env_name}") + None => () + } + match arg.default_values { + Some(values) if values.length() > 1 => { + let defaults = values.join(", ") + notes.push("defaults: \{defaults}") + } + Some(values) if values.length() == 1 => notes.push("default: \{values[0]}") + _ => () + } + if is_required_arg(arg) { + notes.push("required") + } + let help = arg_help(arg) + if help == "" { + notes.join(", ") + } else if notes.length() > 0 { + let notes_text = notes.join(", ") + "\{help} (\{notes_text})" + } else { + help + } +} + +///| +fn arg_hidden(arg : Arg) -> Bool { + arg.hidden +} + +///| +fn command_hidden(cmd : Command) -> Bool { + cmd.hidden +} + +///| +fn has_subcommands_for_help(cmd : Command) -> Bool { + if help_subcommand_enabled(cmd) { + return true + } + for sub in cmd.subcommands { + if !command_hidden(sub) { + return true + } + } + false +} + +///| +fn is_required_arg(arg : Arg) -> Bool { + if arg.required { + true + } else { + let (min, _) = arg_min_max(arg) + min > 0 + } +} + +///| +fn group_label(group : ArgGroup) -> String { + let flags = [] + if group.required { + flags.push("required") + } + if !group.multiple { + flags.push("exclusive") + } + if flags.length() == 0 { + group.name + } else { + let flags_text = flags.join(", ") + "\{group.name} (\{flags_text})" + } +} + +///| +fn group_members(cmd : Command, group : ArgGroup) -> String { + let members = [] + for arg in command_args(cmd) { + if arg_hidden(arg) { + continue + } + if arg_in_group(arg, group) { + members.push(group_member_display(arg)) + } + } + members.join(", ") +} + +///| +fn group_member_display(arg : Arg) -> String { + let base = arg_display(arg) + if is_positional_arg(arg) { + base + } else if arg_takes_value(arg) { + "\{base} <\{arg.name}>" + } else { + base + } +} diff --git a/argparse/matches.mbt b/argparse/matches.mbt new file mode 100644 index 000000000..1ebd04108 --- /dev/null +++ b/argparse/matches.mbt @@ -0,0 +1,91 @@ +// 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. + +///| +/// Where a value/flag came from. +pub enum ValueSource { + Argv + Env + Default +} derive(Eq, Show) + +///| +/// Parse results for declarative commands. +pub struct Matches { + flags : Map[String, Bool] + values : Map[String, Array[String]] + flag_counts : Map[String, Int] + sources : Map[String, ValueSource] + subcommand : (String, Matches)? + priv counts : Map[String, Int] + priv flag_sources : Map[String, ValueSource] + priv value_sources : Map[String, Array[ValueSource]] + priv mut parsed_subcommand : (String, Matches)? +} + +///| +fn new_matches_parse_state() -> Matches { + Matches::{ + flags: {}, + values: {}, + flag_counts: {}, + sources: {}, + subcommand: None, + counts: {}, + flag_sources: {}, + value_sources: {}, + parsed_subcommand: None, + } +} + +///| +fn highest_source(sources : Array[ValueSource]) -> ValueSource? { + if sources.length() == 0 { + return None + } + let mut saw_env = false + let mut saw_default = false + for s in sources { + if s == ValueSource::Argv { + return Some(ValueSource::Argv) + } + if s == ValueSource::Env { + saw_env = true + } + if s == ValueSource::Default { + saw_default = true + } + } + if saw_env { + Some(ValueSource::Env) + } else if saw_default { + Some(ValueSource::Default) + } else { + None + } +} + +///| +/// Decode a full argument struct/enum from `Matches`. +pub(open) trait FromMatches { + from_matches(matches : Matches) -> Self raise ArgParseError +} + +///| +/// Decode a full argument struct/enum from `Matches`. +pub fn[T : FromMatches] from_matches( + matches : Matches, +) -> T raise ArgParseError { + T::from_matches(matches) +} diff --git a/argparse/moon.pkg b/argparse/moon.pkg new file mode 100644 index 000000000..cd148481a --- /dev/null +++ b/argparse/moon.pkg @@ -0,0 +1,5 @@ +import { + "moonbitlang/core/builtin", + "moonbitlang/core/env", + "moonbitlang/core/strconv", +} diff --git a/argparse/parser.mbt b/argparse/parser.mbt new file mode 100644 index 000000000..5ddf4bad6 --- /dev/null +++ b/argparse/parser.mbt @@ -0,0 +1,1767 @@ +// 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. + +///| +fn raise_help(text : String) -> Unit raise { + raise DisplayHelp::Message(text) +} + +///| +fn raise_version(text : String) -> Unit raise { + raise DisplayVersion::Message(text) +} + +///| +fn[T] raise_unknown_long( + name : String, + long_index : Map[String, Arg], +) -> T raise { + let hint = suggest_long(name, long_index) + raise ArgParseError::UnknownArgument("--\{name}", hint) +} + +///| +fn[T] raise_unknown_short( + short : Char, + short_index : Map[Char, Arg], +) -> T raise { + let hint = suggest_short(short, short_index) + raise ArgParseError::UnknownArgument("-\{short}", hint) +} + +///| +fn render_help_for_context( + cmd : Command, + inherited_globals : Array[Arg], +) -> String { + let help_cmd = if inherited_globals.length() == 0 { + cmd + } else { + Command::{ + ..cmd, + args: concat_globals(inherited_globals, command_args(cmd)), + } + } + render_help(help_cmd) +} + +///| +fn default_argv() -> Array[String] { + let args = @env.args() + if args.length() > 1 { + args[1:].to_array() + } else { + [] + } +} + +///| +fn parse_command( + cmd : Command, + argv : Array[String], + env : Map[String, String], + inherited_globals : Array[Arg], +) -> Matches raise { + let args = command_args(cmd) + let groups = command_groups(cmd) + let subcommands = cmd.subcommands + validate_command(cmd, args, groups) + if cmd.arg_required_else_help && argv.length() == 0 { + raise_help(render_help_for_context(cmd, inherited_globals)) + } + let matches = new_matches_parse_state() + let globals_here = collect_globals(args) + let child_globals = concat_globals(inherited_globals, globals_here) + let long_index = build_long_index(inherited_globals, args) + let short_index = build_short_index(inherited_globals, args) + let builtin_help_short = help_flag_enabled(cmd) && + short_index.get('h') is None + let builtin_help_long = help_flag_enabled(cmd) && + long_index.get("help") is None + let builtin_version_short = version_flag_enabled(cmd) && + short_index.get('V') is None + let builtin_version_long = version_flag_enabled(cmd) && + long_index.get("version") is None + let positionals = positional_args(args) + let positional_values = [] + let last_pos_idx = last_positional_index(positionals) + let mut i = 0 + while i < argv.length() { + let arg = argv[i] + if arg == "--" { + for rest in argv[i + 1:] { + positional_values.push(rest) + } + break + } + let force_positional = match last_pos_idx { + Some(idx) => positional_values.length() >= idx + None => false + } + if force_positional { + positional_values.push(arg) + i = i + 1 + continue + } + if builtin_help_short && arg == "-h" { + raise_help(render_help_for_context(cmd, inherited_globals)) + } + if builtin_help_long && arg == "--help" { + raise_help(render_help_for_context(cmd, inherited_globals)) + } + if builtin_version_short && arg == "-V" { + raise_version(command_version(cmd)) + } + if builtin_version_long && arg == "--version" { + raise_version(command_version(cmd)) + } + if should_parse_as_positional( + arg, positionals, positional_values, long_index, short_index, + ) { + positional_values.push(arg) + i = i + 1 + continue + } + if arg.has_prefix("--") { + let (name, inline) = split_long(arg) + if builtin_help_long && name == "help" { + if inline is Some(_) { + raise ArgParseError::InvalidArgument(arg) + } + raise_help(render_help_for_context(cmd, inherited_globals)) + } + if builtin_version_long && name == "version" { + if inline is Some(_) { + raise ArgParseError::InvalidArgument(arg) + } + raise_version(command_version(cmd)) + } + match long_index.get(name) { + None => + // Support `--no-` when the underlying flag is marked `negatable`. + if name.has_prefix("no-") { + let target = match name.strip_prefix("no-") { + Some(view) => view.to_string() + None => "" + } + match long_index.get(target) { + None => raise_unknown_long(name, long_index) + Some(spec) => { + if !spec.negatable || arg_takes_value(spec) { + raise_unknown_long(name, long_index) + } + if inline is Some(_) { + raise ArgParseError::InvalidArgument(arg) + } + let value = match arg_action(spec) { + ArgAction::SetFalse => true + _ => false + } + if arg_action(spec) == ArgAction::Count { + matches.counts[spec.name] = 0 + } + matches.flags[spec.name] = value + matches.flag_sources[spec.name] = ValueSource::Argv + } + } + } else { + raise_unknown_long(name, long_index) + } + Some(spec) => + if arg_takes_value(spec) { + let value = if inline is Some(v) { + v + } else { + if i + 1 >= argv.length() { + raise ArgParseError::MissingValue("--\{name}") + } + i = i + 1 + argv[i] + } + match assign_value(matches, spec, value, ValueSource::Argv) { + Ok(_) => () + Err(e) => raise e + } + match + consume_required_option_values( + matches, + spec, + argv, + i + 1, + long_index, + short_index, + ) { + Ok(consumed) => i = i + consumed + Err(e) => raise e + } + } else { + if inline is Some(_) { + raise ArgParseError::InvalidArgument(arg) + } + match arg_action(spec) { + ArgAction::Help => + raise_help(render_help_for_context(cmd, inherited_globals)) + ArgAction::Version => raise_version(command_version(cmd)) + _ => apply_flag(matches, spec, ValueSource::Argv) + } + } + } + i = i + 1 + continue + } + if arg.has_prefix("-") && arg != "-" { + // Parse short groups like `-abc` and short values like `-c3`. + let mut pos = 1 + while pos < arg.length() { + let short = arg.get_char(pos).unwrap() + if short == 'h' && builtin_help_short { + raise_help(render_help_for_context(cmd, inherited_globals)) + } + if short == 'V' && builtin_version_short { + raise_version(command_version(cmd)) + } + let spec = match short_index.get(short) { + Some(v) => v + None => raise_unknown_short(short, short_index) + } + if arg_takes_value(spec) { + let value = if pos + 1 < arg.length() { + let rest0 = arg.unsafe_substring(start=pos + 1, end=arg.length()) + match rest0.strip_prefix("=") { + Some(view) => view.to_string() + None => rest0 + } + } else { + if i + 1 >= argv.length() { + raise ArgParseError::MissingValue("-\{short}") + } + i = i + 1 + argv[i] + } + match assign_value(matches, spec, value, ValueSource::Argv) { + Ok(_) => () + Err(e) => raise e + } + match + consume_required_option_values( + matches, + spec, + argv, + i + 1, + long_index, + short_index, + ) { + Ok(consumed) => i = i + consumed + Err(e) => raise e + } + break + } else { + match arg_action(spec) { + ArgAction::Help => + raise_help(render_help_for_context(cmd, inherited_globals)) + ArgAction::Version => raise_version(command_version(cmd)) + _ => apply_flag(matches, spec, ValueSource::Argv) + } + } + pos = pos + 1 + } + i = i + 1 + continue + } + if help_subcommand_enabled(cmd) && arg == "help" { + let rest = argv[i + 1:].to_array() + let (target, target_globals) = resolve_help_target( + cmd, rest, builtin_help_short, builtin_help_long, inherited_globals, + ) + let text = render_help_for_context(target, target_globals) + raise_help(text) + } + if subcommands.length() > 0 { + match find_subcommand(subcommands, arg) { + Some(sub) => { + let rest = argv[i + 1:].to_array() + let sub_matches = parse_command(sub, rest, env, child_globals) + matches.parsed_subcommand = Some((sub.name, sub_matches)) + // Merge argv-provided globals from the subcommand parse into the parent + // so globals work even when they appear after the subcommand name. + merge_globals_from_child(matches, sub_matches, child_globals) + let env_args = concat_globals(inherited_globals, args) + let parent_matches = finalize_matches( + cmd, args, groups, matches, positionals, positional_values, env_args, + env, + ) + match parent_matches.parsed_subcommand { + Some((sub_name, sub_m)) => { + // After parent parsing, copy the final globals into the subcommand. + propagate_globals_to_child(parent_matches, sub_m, child_globals) + parent_matches.parsed_subcommand = Some((sub_name, sub_m)) + } + None => () + } + return parent_matches + } + None => () + } + } + positional_values.push(arg) + i = i + 1 + } + let env_args = concat_globals(inherited_globals, args) + finalize_matches( + cmd, args, groups, matches, positionals, positional_values, env_args, env, + ) +} + +///| +fn finalize_matches( + cmd : Command, + args : Array[Arg], + groups : Array[ArgGroup], + matches : Matches, + positionals : Array[Arg], + positional_values : Array[String], + env_args : Array[Arg], + env : Map[String, String], +) -> Matches raise { + match assign_positionals(matches, positionals, positional_values) { + Ok(_) => () + Err(e) => raise e + } + match apply_env(matches, env_args, env) { + Ok(_) => () + Err(e) => raise e + } + apply_defaults(matches, env_args) + validate_values(args, matches) + validate_relationships(matches, env_args) + validate_groups(args, groups, matches) + validate_command_policies(cmd, matches) + matches +} + +///| +fn help_subcommand_enabled(cmd : Command) -> Bool { + !cmd.disable_help_subcommand && cmd.subcommands.length() > 0 +} + +///| +fn help_flag_enabled(cmd : Command) -> Bool { + !cmd.disable_help_flag +} + +///| +fn version_flag_enabled(cmd : Command) -> Bool { + !cmd.disable_version_flag && cmd.version is Some(_) +} + +///| +fn command_version(cmd : Command) -> String { + cmd.version.unwrap_or("") +} + +///| +fn validate_command( + cmd : Command, + args : Array[Arg], + groups : Array[ArgGroup], +) -> Unit raise ArgBuildError { + validate_group_defs(groups) + validate_group_refs(args, groups) + validate_arg_defs(args) + validate_subcommand_defs(cmd.subcommands) + validate_subcommand_required_policy(cmd) + validate_help_subcommand(cmd) + validate_version_actions(cmd) + for arg in args { + validate_arg(arg) + } + for sub in cmd.subcommands { + validate_command(sub, command_args(sub), command_groups(sub)) + } +} + +///| +fn validate_arg(arg : Arg) -> Unit raise ArgBuildError { + let positional = is_positional_arg(arg) + let has_positional_only = arg.index is Some(_) || + arg.allow_hyphen_values || + arg.last + if !positional && has_positional_only { + raise ArgBuildError::Unsupported( + "positional-only settings require no short/long", + ) + } + if arg.negatable && arg_takes_value(arg) { + raise ArgBuildError::Unsupported("negatable is only supported for flags") + } + if arg_action(arg) == ArgAction::Count && arg_takes_value(arg) { + raise ArgBuildError::Unsupported("count is only supported for flags") + } + if arg_action(arg) == ArgAction::Help || arg_action(arg) == ArgAction::Version { + if arg_takes_value(arg) { + raise ArgBuildError::Unsupported("help/version actions require flags") + } + if arg.negatable { + raise ArgBuildError::Unsupported( + "help/version actions do not support negatable", + ) + } + if arg.env is Some(_) || arg.default_values is Some(_) { + raise ArgBuildError::Unsupported( + "help/version actions do not support env/defaults", + ) + } + if arg.num_args is Some(_) || arg.multiple { + raise ArgBuildError::Unsupported( + "help/version actions do not support multiple values", + ) + } + let has_option = arg.long is Some(_) || arg.short is Some(_) + if !has_option { + raise ArgBuildError::Unsupported( + "help/version actions require short/long option", + ) + } + } + if arg.num_args is Some(_) && !arg_takes_value(arg) { + raise ArgBuildError::Unsupported( + "min/max values require value-taking arguments", + ) + } + let (min, max) = arg_min_max_for_validate(arg) + let allow_multi = arg.multiple || arg_action(arg) == ArgAction::Append + if (min > 1 || (max is Some(m) && m > 1)) && !allow_multi { + raise ArgBuildError::Unsupported( + "multiple values require action=Append or num_args allowing >1", + ) + } + if arg.default_values is Some(_) && !arg_takes_value(arg) { + raise ArgBuildError::Unsupported( + "default values require value-taking arguments", + ) + } + match arg.default_values { + Some(values) if values.length() > 1 && + !arg.multiple && + arg_action(arg) != ArgAction::Append => + raise ArgBuildError::Unsupported( + "default_values require action=Append or num_args allowing >1", + ) + _ => () + } +} + +///| +fn validate_group_defs(groups : Array[ArgGroup]) -> Unit raise ArgBuildError { + let seen : Map[String, Bool] = {} + for group in groups { + if seen.get(group.name) is Some(_) { + raise ArgBuildError::Unsupported("duplicate group: \{group.name}") + } + seen[group.name] = true + } + for group in groups { + for required in group.requires { + if required == group.name { + raise ArgBuildError::Unsupported( + "group cannot require itself: \{group.name}", + ) + } + if seen.get(required) is None { + raise ArgBuildError::Unsupported( + "unknown group requires target: \{group.name} -> \{required}", + ) + } + } + for conflict in group.conflicts_with { + if conflict == group.name { + raise ArgBuildError::Unsupported( + "group cannot conflict with itself: \{group.name}", + ) + } + if seen.get(conflict) is None { + raise ArgBuildError::Unsupported( + "unknown group conflicts_with target: \{group.name} -> \{conflict}", + ) + } + } + } +} + +///| +fn validate_group_refs( + args : Array[Arg], + groups : Array[ArgGroup], +) -> Unit raise ArgBuildError { + if groups.length() == 0 { + return + } + let group_index : Map[String, Bool] = {} + for group in groups { + group_index[group.name] = true + } + let arg_index : Map[String, Bool] = {} + for arg in args { + arg_index[arg.name] = true + } + for arg in args { + match arg.group { + Some(name) => + if group_index.get(name) is None { + raise ArgBuildError::Unsupported("unknown group: \{name}") + } + None => () + } + } + for group in groups { + for name in group.args { + if arg_index.get(name) is None { + raise ArgBuildError::Unsupported( + "unknown group arg: \{group.name} -> \{name}", + ) + } + } + } +} + +///| +fn validate_arg_defs(args : Array[Arg]) -> Unit raise ArgBuildError { + let seen_names : Map[String, Bool] = {} + let seen_long : Map[String, Bool] = {} + let seen_short : Map[Char, Bool] = {} + for arg in args { + if seen_names.get(arg.name) is Some(_) { + raise ArgBuildError::Unsupported("duplicate arg name: \{arg.name}") + } + seen_names[arg.name] = true + for name in collect_long_names(arg) { + if seen_long.get(name) is Some(_) { + raise ArgBuildError::Unsupported("duplicate long option: --\{name}") + } + seen_long[name] = true + } + for short in collect_short_names(arg) { + if seen_short.get(short) is Some(_) { + raise ArgBuildError::Unsupported("duplicate short option: -\{short}") + } + seen_short[short] = true + } + } + for arg in args { + for required in arg.requires { + if required == arg.name { + raise ArgBuildError::Unsupported( + "arg cannot require itself: \{arg.name}", + ) + } + if seen_names.get(required) is None { + raise ArgBuildError::Unsupported( + "unknown requires target: \{arg.name} -> \{required}", + ) + } + } + for conflict in arg.conflicts_with { + if conflict == arg.name { + raise ArgBuildError::Unsupported( + "arg cannot conflict with itself: \{arg.name}", + ) + } + if seen_names.get(conflict) is None { + raise ArgBuildError::Unsupported( + "unknown conflicts_with target: \{arg.name} -> \{conflict}", + ) + } + } + } +} + +///| +fn validate_subcommand_defs(subs : Array[Command]) -> Unit raise ArgBuildError { + if subs.length() == 0 { + return + } + let seen : Map[String, Bool] = {} + for sub in subs { + for name in collect_subcommand_names(sub) { + if seen.get(name) is Some(_) { + raise ArgBuildError::Unsupported("duplicate subcommand: \{name}") + } + seen[name] = true + } + } +} + +///| +fn validate_subcommand_required_policy( + cmd : Command, +) -> Unit raise ArgBuildError { + if cmd.subcommand_required && cmd.subcommands.length() == 0 { + raise ArgBuildError::Unsupported( + "subcommand_required requires at least one subcommand", + ) + } +} + +///| +fn validate_help_subcommand(cmd : Command) -> Unit raise ArgBuildError { + if !help_subcommand_enabled(cmd) { + return + } + if find_subcommand(cmd.subcommands, "help") is Some(_) { + raise ArgBuildError::Unsupported( + "subcommand name reserved for built-in help: help (disable with disable_help_subcommand)", + ) + } +} + +///| +fn validate_version_actions(cmd : Command) -> Unit raise ArgBuildError { + if cmd.version is Some(_) { + return + } + for arg in command_args(cmd) { + if arg_action(arg) == ArgAction::Version { + raise ArgBuildError::Unsupported( + "version action requires command version text", + ) + } + } +} + +///| +fn validate_command_policies(cmd : Command, matches : Matches) -> Unit raise { + if cmd.subcommand_required && + cmd.subcommands.length() > 0 && + matches.parsed_subcommand is None { + raise ArgParseError::MissingRequired("subcommand") + } +} + +///| +fn validate_groups( + args : Array[Arg], + groups : Array[ArgGroup], + matches : Matches, +) -> Unit raise { + if groups.length() == 0 { + return + } + let group_presence : Map[String, Int] = {} + for group in groups { + let mut count = 0 + for arg in args { + if !arg_in_group(arg, group) { + continue + } + if matches_has_value_or_flag(matches, arg.name) { + count = count + 1 + } + } + group_presence[group.name] = count + if group.required && count == 0 { + raise ArgParseError::MissingGroup(group.name) + } + if !group.multiple && count > 1 { + raise ArgParseError::GroupConflict(group.name) + } + } + for group in groups { + let count = group_presence[group.name] + if count == 0 { + continue + } + for required in group.requires { + if group_presence.get(required).unwrap_or(0) == 0 { + raise ArgParseError::MissingGroup(required) + } + } + for conflict in group.conflicts_with { + if group_presence.get(conflict).unwrap_or(0) > 0 { + raise ArgParseError::GroupConflict( + "\{group.name} conflicts with \{conflict}", + ) + } + } + } +} + +///| +fn arg_in_group(arg : Arg, group : ArgGroup) -> Bool { + let from_arg = arg.group is Some(name) && name == group.name + from_arg || group.args.contains(arg.name) +} + +///| +fn validate_values(args : Array[Arg], matches : Matches) -> Unit raise { + for arg in args { + let present = matches_has_value_or_flag(matches, arg.name) + if arg.required && !present { + raise ArgParseError::MissingRequired(arg.name) + } + if !arg_takes_value(arg) { + continue + } + let values = matches.values.get(arg.name).unwrap_or([]) + let count = values.length() + let (min, max) = arg_min_max(arg) + if count < min { + raise ArgParseError::TooFewValues(arg.name, count, min) + } + match max { + Some(max) if count > max => + raise ArgParseError::TooManyValues(arg.name, count, max) + _ => () + } + } +} + +///| +fn validate_relationships(matches : Matches, args : Array[Arg]) -> Unit raise { + for arg in args { + if !matches_has_value_or_flag(matches, arg.name) { + continue + } + for required in arg.requires { + if !matches_has_value_or_flag(matches, required) { + raise ArgParseError::MissingRequired(required) + } + } + for conflict in arg.conflicts_with { + if matches_has_value_or_flag(matches, conflict) { + raise ArgParseError::InvalidArgument( + "conflicting arguments: \{arg.name} and \{conflict}", + ) + } + } + } +} + +///| +fn is_positional_arg(arg : Arg) -> Bool { + arg.short is None && arg.long is None +} + +///| +fn assign_positionals( + matches : Matches, + positionals : Array[Arg], + values : Array[String], +) -> Result[Unit, ArgParseError] { + let mut cursor = 0 + for idx in 0.. max_count => take = max_count + _ => () + } + if take < min { + take = min + } + if take > remaining { + take = remaining + } + let mut taken = 0 + while taken < take { + match + add_value( + matches, + arg.name, + values[cursor + taken], + arg, + ValueSource::Argv, + ) { + Ok(_) => () + Err(e) => return Err(e) + } + taken = taken + 1 + } + cursor = cursor + taken + continue + } + if remaining > 0 { + match + add_value(matches, arg.name, values[cursor], arg, ValueSource::Argv) { + Ok(_) => () + Err(e) => return Err(e) + } + cursor = cursor + 1 + } + } + if cursor < values.length() { + return Err(ArgParseError::TooManyPositionals) + } + Ok(()) +} + +///| +fn positional_min_required(arg : Arg) -> Int { + let (min, _) = arg_min_max(arg) + if min > 0 { + min + } else if arg.required { + 1 + } else { + 0 + } +} + +///| +fn remaining_positional_min(positionals : Array[Arg], start : Int) -> Int { + let mut total = 0 + let mut idx = start + while idx < positionals.length() { + total = total + positional_min_required(positionals[idx]) + idx = idx + 1 + } + total +} + +///| +fn add_value( + matches : Matches, + name : String, + value : String, + arg : Arg, + source : ValueSource, +) -> Result[Unit, ArgParseError] { + if arg.multiple || arg_action(arg) == ArgAction::Append { + let arr = matches.values.get(name).unwrap_or([]) + arr.push(value) + matches.values[name] = arr + let srcs = matches.value_sources.get(name).unwrap_or([]) + srcs.push(source) + matches.value_sources[name] = srcs + } else { + matches.values[name] = [value] + matches.value_sources[name] = [source] + } + Ok(()) +} + +///| +fn assign_value( + matches : Matches, + arg : Arg, + value : String, + source : ValueSource, +) -> Result[Unit, ArgParseError] { + match arg_action(arg) { + ArgAction::Append => add_value(matches, arg.name, value, arg, source) + ArgAction::Set => add_value(matches, arg.name, value, arg, source) + ArgAction::SetTrue => + match parse_bool(value) { + Ok(flag) => { + matches.flags[arg.name] = flag + matches.flag_sources[arg.name] = source + Ok(()) + } + Err(e) => Err(e) + } + ArgAction::SetFalse => + match parse_bool(value) { + Ok(flag) => { + matches.flags[arg.name] = !flag + matches.flag_sources[arg.name] = source + Ok(()) + } + Err(e) => Err(e) + } + ArgAction::Count => + match parse_count(value) { + Ok(count) => { + matches.counts[arg.name] = count + matches.flags[arg.name] = count > 0 + matches.flag_sources[arg.name] = source + Ok(()) + } + Err(e) => Err(e) + } + ArgAction::Help => + Err(ArgParseError::InvalidArgument("help action does not take values")) + ArgAction::Version => + Err(ArgParseError::InvalidArgument("version action does not take values")) + } +} + +///| +fn required_option_value_count(matches : Matches, arg : Arg) -> Int { + match arg.num_args { + None => 0 + Some(_) => { + let (min, _) = arg_min_max(arg) + if min <= 0 { + return 0 + } + let count = matches.values.get(arg.name).unwrap_or([]).length() + if count >= min { + 0 + } else { + min - count + } + } + } +} + +///| +fn consume_required_option_values( + matches : Matches, + arg : Arg, + argv : Array[String], + start : Int, + long_index : Map[String, Arg], + short_index : Map[Char, Arg], +) -> Result[Int, ArgParseError] { + let need = required_option_value_count(matches, arg) + if need == 0 { + return Ok(0) + } + let mut consumed = 0 + while consumed < need && start + consumed < argv.length() { + let value = argv[start + consumed] + if starts_known_option(value, long_index, short_index) { + break + } + match assign_value(matches, arg, value, ValueSource::Argv) { + Ok(_) => () + Err(e) => return Err(e) + } + consumed = consumed + 1 + } + Ok(consumed) +} + +///| +fn starts_known_option( + arg : String, + long_index : Map[String, Arg], + short_index : Map[Char, Arg], +) -> Bool { + if !arg.has_prefix("-") || arg == "-" { + return false + } + if arg.has_prefix("--") { + let (name, _) = split_long(arg) + if long_index.get(name) is Some(_) { + return true + } + if name.has_prefix("no-") { + let target = match name.strip_prefix("no-") { + Some(view) => view.to_string() + None => "" + } + match long_index.get(target) { + Some(spec) => !arg_takes_value(spec) && spec.negatable + None => false + } + } else { + false + } + } else { + match arg.get_char(1) { + Some(ch) => short_index.get(ch) is Some(_) + None => false + } + } +} + +///| +fn apply_env( + matches : Matches, + args : Array[Arg], + env : Map[String, String], +) -> Result[Unit, ArgParseError] { + for arg in args { + let name = arg.name + if matches_has_value_or_flag(matches, name) { + continue + } + let env_name = match arg.env { + Some(value) => value + None => continue + } + let value = match env.get(env_name) { + Some(v) => v + None => continue + } + if arg_takes_value(arg) { + match assign_value(matches, arg, value, ValueSource::Env) { + Ok(_) => () + Err(e) => return Err(e) + } + continue + } + match arg_action(arg) { + ArgAction::Count => + match parse_count(value) { + Ok(count) => { + matches.counts[name] = count + matches.flags[name] = count > 0 + matches.flag_sources[name] = ValueSource::Env + } + Err(e) => return Err(e) + } + ArgAction::SetFalse => + match parse_bool(value) { + Ok(flag) => { + matches.flags[name] = !flag + matches.flag_sources[name] = ValueSource::Env + } + Err(e) => return Err(e) + } + ArgAction::SetTrue => + match parse_bool(value) { + Ok(flag) => { + matches.flags[name] = flag + matches.flag_sources[name] = ValueSource::Env + } + Err(e) => return Err(e) + } + ArgAction::Set => + match parse_bool(value) { + Ok(flag) => { + matches.flags[name] = flag + matches.flag_sources[name] = ValueSource::Env + } + Err(e) => return Err(e) + } + ArgAction::Append => () + ArgAction::Help => () + ArgAction::Version => () + } + } + Ok(()) +} + +///| +fn apply_defaults(matches : Matches, args : Array[Arg]) -> Unit { + for arg in args { + if !arg_takes_value(arg) { + continue + } + if matches_has_value_or_flag(matches, arg.name) { + continue + } + match arg.default_values { + Some(values) if values.length() > 0 => + for value in values { + let _ = add_value(matches, arg.name, value, arg, ValueSource::Default) + } + _ => () + } + } +} + +///| +fn matches_has_value_or_flag(matches : Matches, name : String) -> Bool { + matches.flags.get(name) is Some(_) || matches.values.get(name) is Some(_) +} + +///| +fn collect_long_names(arg : Arg) -> Array[String] { + let names = [] + match arg.long { + Some(value) => { + names.push(value) + if arg.negatable && !arg_takes_value(arg) { + names.push("no-\{value}") + } + } + None => () + } + names +} + +///| +fn collect_short_names(arg : Arg) -> Array[Char] { + let names = [] + match arg.short { + Some(value) => names.push(value) + None => () + } + names +} + +///| +fn collect_subcommand_names(cmd : Command) -> Array[String] { + [cmd.name] +} + +///| +fn apply_flag(matches : Matches, arg : Arg, source : ValueSource) -> Unit { + match arg_action(arg) { + ArgAction::SetTrue => matches.flags[arg.name] = true + ArgAction::SetFalse => matches.flags[arg.name] = false + ArgAction::Count => { + let current = matches.counts.get(arg.name).unwrap_or(0) + matches.counts[arg.name] = current + 1 + matches.flags[arg.name] = true + } + ArgAction::Help => () + ArgAction::Version => () + _ => matches.flags[arg.name] = true + } + matches.flag_sources[arg.name] = source +} + +///| +fn parse_bool(value : String) -> Result[Bool, ArgParseError] { + if value == "1" || value == "true" || value == "yes" || value == "on" { + Ok(true) + } else if value == "0" || value == "false" || value == "no" || value == "off" { + Ok(false) + } else { + Err( + ArgParseError::InvalidValue( + "invalid value '\{value}' for boolean flag; expected one of: 1, 0, true, false, yes, no, on, off", + ), + ) + } +} + +///| +fn parse_count(value : String) -> Result[Int, ArgParseError] { + let res : Result[Int, Error] = try? @strconv.parse_int(value) + match res { + Ok(v) => + if v >= 0 { + Ok(v) + } else { + Err( + ArgParseError::InvalidValue( + "invalid value '\{value}' for count; expected a non-negative integer", + ), + ) + } + Err(_) => + Err( + ArgParseError::InvalidValue( + "invalid value '\{value}' for count; expected a non-negative integer", + ), + ) + } +} + +///| +fn suggest_long(name : String, long_index : Map[String, Arg]) -> String? { + let candidates = map_string_keys(long_index) + match suggest_name(name, candidates) { + Some(best) => Some("--\{best}") + None => None + } +} + +///| +fn suggest_short(short : Char, short_index : Map[Char, Arg]) -> String? { + let candidates = map_char_keys(short_index) + let input = short.to_string() + match suggest_name(input, candidates) { + Some(best) => Some("-\{best}") + None => None + } +} + +///| +fn map_string_keys(map : Map[String, Arg]) -> Array[String] { + let keys = [] + for key, _ in map { + keys.push(key) + } + keys +} + +///| +fn map_char_keys(map : Map[Char, Arg]) -> Array[String] { + let keys = [] + for key, _ in map { + keys.push(key.to_string()) + } + keys +} + +///| +fn suggest_name(input : String, candidates : Array[String]) -> String? { + let mut best : String? = None + let mut best_dist = 0 + let mut has_best = false + let max_dist = suggestion_threshold(input.length()) + for cand in candidates { + let dist = levenshtein(input, cand) + if !has_best || dist < best_dist { + best_dist = dist + best = Some(cand) + has_best = true + } + } + match best { + Some(name) if best_dist <= max_dist => Some(name) + _ => None + } +} + +///| +fn suggestion_threshold(len : Int) -> Int { + if len <= 4 { + 1 + } else if len <= 8 { + 2 + } else { + 3 + } +} + +///| +fn levenshtein(a : String, b : String) -> Int { + let aa = string_chars(a) + let bb = string_chars(b) + let m = aa.length() + let n = bb.length() + if m == 0 { + return n + } + if n == 0 { + return m + } + let mut prev = Array::new(capacity=n + 1) + let mut curr = Array::new(capacity=n + 1) + let mut j = 0 + while j <= n { + prev.push(j) + curr.push(0) + j = j + 1 + } + let mut i = 1 + while i <= m { + curr[0] = i + let mut j2 = 1 + while j2 <= n { + let cost = if aa[i - 1] == bb[j2 - 1] { 0 } else { 1 } + let del = prev[j2] + 1 + let ins = curr[j2 - 1] + 1 + let sub = prev[j2 - 1] + cost + curr[j2] = min3(del, ins, sub) + j2 = j2 + 1 + } + let temp = prev + prev = curr + curr = temp + i = i + 1 + } + prev[n] +} + +///| +fn string_chars(s : String) -> Array[Char] { + let out = [] + for ch in s { + out.push(ch) + } + out +} + +///| +fn min3(a : Int, b : Int, c : Int) -> Int { + let m = if a < b { a } else { b } + if c < m { + c + } else { + m + } +} + +///| +fn build_long_index( + globals : Array[Arg], + args : Array[Arg], +) -> Map[String, Arg] { + let index : Map[String, Arg] = {} + for arg in globals { + if arg.long is Some(name) { + index[name] = arg + } + } + for arg in args { + if arg.long is Some(name) { + index[name] = arg + } + } + index +} + +///| +fn build_short_index(globals : Array[Arg], args : Array[Arg]) -> Map[Char, Arg] { + let index : Map[Char, Arg] = {} + for arg in globals { + if arg.short is Some(value) { + index[value] = arg + } + } + for arg in args { + if arg.short is Some(value) { + index[value] = arg + } + } + index +} + +///| +fn collect_globals(args : Array[Arg]) -> Array[Arg] { + let out = [] + for arg in args { + if arg.global && (arg.long is Some(_) || arg.short is Some(_)) { + out.push(arg) + } + } + out +} + +///| +fn concat_globals(parent : Array[Arg], more : Array[Arg]) -> Array[Arg] { + let out = clone_array(parent) + for arg in more { + out.push(arg) + } + out +} + +///| +fn source_priority(source : ValueSource?) -> Int { + match source { + Some(ValueSource::Argv) => 3 + Some(ValueSource::Env) => 2 + Some(ValueSource::Default) => 1 + None => 0 + } +} + +///| +fn prefer_child_source( + parent_source : ValueSource?, + child_source : ValueSource?, +) -> Bool { + let parent_priority = source_priority(parent_source) + let child_priority = source_priority(child_source) + if child_priority > parent_priority { + true + } else if child_priority < parent_priority { + false + } else { + child_source is Some(ValueSource::Argv) + } +} + +///| +fn strongest_source( + parent_source : ValueSource?, + child_source : ValueSource?, +) -> ValueSource? { + if prefer_child_source(parent_source, child_source) { + child_source + } else { + match parent_source { + Some(source) => Some(source) + None => child_source + } + } +} + +///| +fn source_from_values(sources : Array[ValueSource]?) -> ValueSource? { + match sources { + Some(items) if items.length() > 0 => highest_source(items) + _ => None + } +} + +///| +fn merge_globals_from_child( + parent : Matches, + child : Matches, + globals : Array[Arg], +) -> Unit { + for arg in globals { + let name = arg.name + if arg_takes_value(arg) { + let parent_vals = parent.values.get(name) + let child_vals = child.values.get(name) + let parent_srcs = parent.value_sources.get(name) + let child_srcs = child.value_sources.get(name) + let has_parent = parent_vals is Some(pv) && pv.length() > 0 + let has_child = child_vals is Some(cv) && cv.length() > 0 + if !has_parent && !has_child { + continue + } + let parent_source = source_from_values(parent_srcs) + let child_source = source_from_values(child_srcs) + if arg.multiple || arg_action(arg) == ArgAction::Append { + let both_argv = parent_source is Some(ValueSource::Argv) && + child_source is Some(ValueSource::Argv) + if both_argv { + let merged = [] + let merged_srcs = [] + if parent_vals is Some(pv) { + for v in pv { + merged.push(v) + } + } + if parent_srcs is Some(ps) { + for s in ps { + merged_srcs.push(s) + } + } + if child_vals is Some(cv) { + for v in cv { + merged.push(v) + } + } + if child_srcs is Some(cs) { + for s in cs { + merged_srcs.push(s) + } + } + if merged.length() > 0 { + parent.values[name] = merged + parent.value_sources[name] = merged_srcs + } + } else { + let choose_child = has_child && + (!has_parent || prefer_child_source(parent_source, child_source)) + if choose_child { + if child_vals is Some(cv) && cv.length() > 0 { + parent.values[name] = clone_array(cv) + } + if child_srcs is Some(cs) && cs.length() > 0 { + parent.value_sources[name] = clone_array(cs) + } + } else if parent_vals is Some(pv) && pv.length() > 0 { + parent.values[name] = clone_array(pv) + if parent_srcs is Some(ps) && ps.length() > 0 { + parent.value_sources[name] = clone_array(ps) + } + } + } + } else { + let choose_child = has_child && + (!has_parent || prefer_child_source(parent_source, child_source)) + if choose_child { + if child_vals is Some(cv) && cv.length() > 0 { + parent.values[name] = clone_array(cv) + } + if child_srcs is Some(cs) && cs.length() > 0 { + parent.value_sources[name] = clone_array(cs) + } + } else if parent_vals is Some(pv) && pv.length() > 0 { + parent.values[name] = clone_array(pv) + if parent_srcs is Some(ps) && ps.length() > 0 { + parent.value_sources[name] = clone_array(ps) + } + } + } + } else { + match child.flags.get(name) { + Some(v) => + if arg_action(arg) == ArgAction::Count { + let has_parent = parent.flags.get(name) is Some(_) + let parent_source = parent.flag_sources.get(name) + let child_source = child.flag_sources.get(name) + let both_argv = parent_source is Some(ValueSource::Argv) && + child_source is Some(ValueSource::Argv) + if both_argv { + let parent_count = parent.counts.get(name).unwrap_or(0) + let child_count = child.counts.get(name).unwrap_or(0) + let total = parent_count + child_count + parent.counts[name] = total + parent.flags[name] = total > 0 + match strongest_source(parent_source, child_source) { + Some(src) => parent.flag_sources[name] = src + None => () + } + } else { + let choose_child = !has_parent || + prefer_child_source(parent_source, child_source) + if choose_child { + let child_count = child.counts.get(name).unwrap_or(0) + parent.counts[name] = child_count + parent.flags[name] = child_count > 0 + match child_source { + Some(src) => parent.flag_sources[name] = src + None => () + } + } + } + } else { + let has_parent = parent.flags.get(name) is Some(_) + let parent_source = parent.flag_sources.get(name) + let child_source = child.flag_sources.get(name) + let choose_child = !has_parent || + prefer_child_source(parent_source, child_source) + if choose_child { + parent.flags[name] = v + match child_source { + Some(src) => parent.flag_sources[name] = src + None => () + } + } + } + None => () + } + } + } +} + +///| +fn propagate_globals_to_child( + parent : Matches, + child : Matches, + globals : Array[Arg], +) -> Unit { + for arg in globals { + let name = arg.name + if arg_takes_value(arg) { + match parent.values.get(name) { + Some(values) => { + child.values[name] = clone_array(values) + match parent.value_sources.get(name) { + Some(srcs) => child.value_sources[name] = clone_array(srcs) + None => () + } + } + None => () + } + } else { + match parent.flags.get(name) { + Some(v) => { + child.flags[name] = v + match parent.flag_sources.get(name) { + Some(src) => child.flag_sources[name] = src + None => () + } + if arg_action(arg) == ArgAction::Count { + match parent.counts.get(name) { + Some(c) => child.counts[name] = c + None => () + } + } + } + None => () + } + } + } +} + +///| + +///| + +///| +fn positional_args(args : Array[Arg]) -> Array[Arg] { + let with_index = [] + let without_index = [] + for arg in args { + if arg.long is None && arg.short is None { + if arg.index is Some(idx) { + with_index.push((idx, arg)) + } else { + without_index.push(arg) + } + } + } + sort_positionals(with_index) + let ordered = [] + for item in with_index { + let (_, arg) = item + ordered.push(arg) + } + for arg in without_index { + ordered.push(arg) + } + ordered +} + +///| +fn last_positional_index(positionals : Array[Arg]) -> Int? { + let mut i = 0 + while i < positionals.length() { + if positionals[i].last { + return Some(i) + } + i = i + 1 + } + None +} + +///| +fn next_positional(positionals : Array[Arg], collected : Array[String]) -> Arg? { + if collected.length() < positionals.length() { + Some(positionals[collected.length()]) + } else { + None + } +} + +///| +fn should_parse_as_positional( + arg : String, + positionals : Array[Arg], + collected : Array[String], + long_index : Map[String, Arg], + short_index : Map[Char, Arg], +) -> Bool { + if !arg.has_prefix("-") || arg == "-" { + return false + } + let next = match next_positional(positionals, collected) { + Some(v) => v + None => return false + } + let allow = next.allow_hyphen_values || is_negative_number(arg) + if !allow { + return false + } + if arg.has_prefix("--") { + let (name, _) = split_long(arg) + return long_index.get(name) is None + } + let short = arg.get_char(1) + match short { + Some(ch) => short_index.get(ch) is None + None => true + } +} + +///| +fn is_negative_number(arg : String) -> Bool { + if arg.length() < 2 { + return false + } + guard arg.get_char(0) is Some('-') else { return false } + let mut i = 1 + while i < arg.length() { + let ch = arg.get_char(i).unwrap() + if ch < '0' || ch > '9' { + return false + } + i = i + 1 + } + true +} + +///| +fn sort_positionals(items : Array[(Int, Arg)]) -> Unit { + let mut i = 1 + while i < items.length() { + let key = items[i] + let mut j = i - 1 + while j >= 0 && items[j].0 > key.0 { + items[j + 1] = items[j] + if j == 0 { + j = -1 + } else { + j = j - 1 + } + } + items[j + 1] = key + i = i + 1 + } +} + +///| +fn find_subcommand(subs : Array[Command], name : String) -> Command? { + for sub in subs { + if sub.name == name { + return Some(sub) + } + } + None +} + +///| +fn resolve_help_target( + cmd : Command, + argv : Array[String], + builtin_help_short : Bool, + builtin_help_long : Bool, + inherited_globals : Array[Arg], +) -> (Command, Array[Arg]) raise { + let targets = if argv.length() == 0 { + argv + } else { + let last = argv[argv.length() - 1] + if (last == "-h" && builtin_help_short) || + (last == "--help" && builtin_help_long) { + argv[:argv.length() - 1].to_array() + } else { + argv + } + } + let mut current = cmd + let mut current_globals = inherited_globals + let mut subs = cmd.subcommands + for name in targets { + if name.has_prefix("-") { + raise ArgParseError::InvalidArgument("unexpected help argument: \{name}") + } + match find_subcommand(subs, name) { + Some(sub) => { + current_globals = concat_globals( + current_globals, + collect_globals(command_args(current)), + ) + current = sub + subs = sub.subcommands + } + None => + raise ArgParseError::InvalidArgument("unknown subcommand: \{name}") + } + } + (current, current_globals) +} + +///| +fn split_long(arg : String) -> (String, String?) { + let parts = [] + for part in arg.split("=") { + parts.push(part.to_string()) + } + if parts.length() <= 1 { + let name = match parts[0].strip_prefix("--") { + Some(view) => view.to_string() + None => parts[0] + } + (name, None) + } else { + let name = match parts[0].strip_prefix("--") { + Some(view) => view.to_string() + None => parts[0] + } + let value = parts[1:].to_array().join("=") + (name, Some(value)) + } +} + +///| +fn[T] clone_array(arr : Array[T]) -> Array[T] { + let out = Array::new(capacity=arr.length()) + for value in arr { + out.push(value) + } + out +} diff --git a/argparse/pkg.generated.mbti b/argparse/pkg.generated.mbti new file mode 100644 index 000000000..6dc9ea171 --- /dev/null +++ b/argparse/pkg.generated.mbti @@ -0,0 +1,131 @@ +// Generated using `moon info`, DON'T EDIT IT +package "moonbitlang/core/argparse" + +// Values +pub fn[T : FromMatches] from_matches(Matches) -> T raise ArgParseError + +// Errors +pub suberror ArgBuildError { + Unsupported(String) +} +pub impl Show for ArgBuildError + +pub(all) suberror ArgParseError { + UnknownArgument(String, String?) + InvalidArgument(String) + MissingValue(String) + MissingRequired(String) + TooFewValues(String, Int, Int) + TooManyValues(String, Int, Int) + TooManyPositionals + InvalidValue(String) + MissingGroup(String) + GroupConflict(String) +} +pub impl Show for ArgParseError + +pub suberror DisplayHelp { + Message(String) +} +pub impl Show for DisplayHelp + +pub suberror DisplayVersion { + Message(String) +} +pub impl Show for DisplayVersion + +// Types and methods +pub struct ArgGroup { + // private fields + + fn new(String, required? : Bool, multiple? : Bool, args? : Array[String], requires? : Array[String], conflicts_with? : Array[String]) -> ArgGroup +} +pub fn ArgGroup::new(String, required? : Bool, multiple? : Bool, args? : Array[String], requires? : Array[String], conflicts_with? : Array[String]) -> Self + +pub struct Command { + // private fields + + fn new(String, args? : Array[&ArgLike], subcommands? : Array[Command], about? : String, version? : String, disable_help_flag? : Bool, disable_version_flag? : Bool, disable_help_subcommand? : Bool, arg_required_else_help? : Bool, subcommand_required? : Bool, hidden? : Bool, groups? : Array[ArgGroup]) -> Command +} +pub fn Command::new(String, args? : Array[&ArgLike], subcommands? : Array[Self], about? : String, version? : String, disable_help_flag? : Bool, disable_version_flag? : Bool, disable_help_subcommand? : Bool, arg_required_else_help? : Bool, subcommand_required? : Bool, hidden? : Bool, groups? : Array[ArgGroup]) -> Self +pub fn Command::parse(Self, argv? : Array[String], env? : Map[String, String]) -> Matches raise +pub fn Command::render_help(Self) -> String + +pub(all) enum FlagAction { + SetTrue + SetFalse + Count + Help + Version +} +pub impl Eq for FlagAction +pub impl Show for FlagAction + +pub struct FlagArg { + // private fields + + fn new(String, short? : Char, long? : String, about? : String, action? : FlagAction, env? : String, requires? : Array[String], conflicts_with? : Array[String], group? : String, required? : Bool, global? : Bool, negatable? : Bool, hidden? : Bool) -> FlagArg +} +pub fn FlagArg::new(String, short? : Char, long? : String, about? : String, action? : FlagAction, env? : String, requires? : Array[String], conflicts_with? : Array[String], group? : String, required? : Bool, global? : Bool, negatable? : Bool, hidden? : Bool) -> Self +pub impl ArgLike for FlagArg + +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 +} + +pub(all) enum OptionAction { + Set + Append +} +pub impl Eq for OptionAction +pub impl Show for OptionAction + +pub struct OptionArg { + // private fields + + fn new(String, short? : Char, long? : String, about? : String, action? : OptionAction, env? : String, default_values? : Array[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : Array[String], conflicts_with? : Array[String], group? : String, required? : Bool, global? : Bool, hidden? : Bool) -> OptionArg +} +pub fn OptionArg::new(String, short? : Char, long? : String, about? : String, action? : OptionAction, env? : String, default_values? : Array[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : Array[String], conflicts_with? : Array[String], group? : String, required? : Bool, global? : Bool, hidden? : Bool) -> Self +pub impl ArgLike for OptionArg + +pub struct PositionalArg { + // private fields + + fn new(String, index? : Int, about? : String, env? : String, default_values? : Array[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : Array[String], conflicts_with? : Array[String], group? : String, required? : Bool, global? : Bool, hidden? : Bool) -> PositionalArg +} +pub fn PositionalArg::new(String, index? : Int, about? : String, env? : String, default_values? : Array[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : Array[String], conflicts_with? : Array[String], group? : String, required? : Bool, global? : Bool, hidden? : Bool) -> Self +pub impl ArgLike for PositionalArg + +pub struct ValueRange { + // private fields + + fn new(lower? : Int, upper? : Int, lower_inclusive? : Bool, upper_inclusive? : Bool) -> ValueRange +} +pub fn ValueRange::empty() -> Self +pub fn ValueRange::new(lower? : Int, upper? : Int, lower_inclusive? : Bool, upper_inclusive? : Bool) -> Self +pub fn ValueRange::single() -> Self +pub impl Eq for ValueRange +pub impl Show for ValueRange + +pub enum ValueSource { + Argv + Env + Default +} +pub impl Eq for ValueSource +pub impl Show for ValueSource + +// Type aliases + +// Traits +trait ArgLike + +pub(open) trait FromMatches { + from_matches(Matches) -> Self raise ArgParseError +} + diff --git a/argparse/value_range.mbt b/argparse/value_range.mbt new file mode 100644 index 000000000..651eb27f3 --- /dev/null +++ b/argparse/value_range.mbt @@ -0,0 +1,49 @@ +// 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. + +///| +/// Number-of-values constraint for an argument. +pub struct ValueRange { + priv lower : Int? + priv upper : Int? + priv lower_inclusive : Bool + priv upper_inclusive : Bool + + fn new( + lower? : Int, + upper? : Int, + lower_inclusive? : Bool, + upper_inclusive? : Bool, + ) -> ValueRange +} derive(Eq, Show) + +///| +pub fn ValueRange::empty() -> ValueRange { + ValueRange(lower=0, upper=0) +} + +///| +pub fn ValueRange::single() -> ValueRange { + ValueRange(lower=1, upper=1) +} + +///| +pub fn ValueRange::new( + lower? : Int, + upper? : Int, + lower_inclusive? : Bool = true, + upper_inclusive? : Bool = true, +) -> ValueRange { + ValueRange::{ lower, upper, lower_inclusive, upper_inclusive } +} From 044992f2786e9f93b7c4a91fccd495f22ad0aa55 Mon Sep 17 00:00:00 2001 From: zihang Date: Tue, 10 Feb 2026 15:11:50 +0800 Subject: [PATCH 02/40] fix: clap parity --- argparse/README.mbt.md | 19 +- argparse/arg_action.mbt | 35 +- argparse/arg_group.mbt | 15 +- argparse/arg_spec.mbt | 48 +- argparse/argparse_blackbox_test.mbt | 931 +++++++++++++++++++++++++++- argparse/argparse_test.mbt | 77 ++- argparse/command.mbt | 70 +-- argparse/help_render.mbt | 146 ++--- argparse/parser.mbt | 651 ++++++++++--------- argparse/value_range.mbt | 14 +- 10 files changed, 1452 insertions(+), 554 deletions(-) diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index 57502db5a..dd493e82b 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -4,18 +4,21 @@ Declarative argument parsing for MoonBit. ## Argument Shape Rule -If an argument has neither `short` nor `long`, it is parsed as a positional -argument. - -This applies even for `OptionArg("name")`. For readability, prefer -`PositionalArg("name", index=...)` when you mean positional input. +`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 behaves as positional" { +test "name-only option is rejected" { let cmd = @argparse.Command("demo", args=[@argparse.OptionArg("input")]) - let matches = cmd.parse(argv=["file.txt"], env={}) catch { _ => panic() } - assert_true(matches.values is { "input": ["file.txt"], .. }) + try cmd.parse(argv=["file.txt"], env={}) catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect(msg, content="flag/option args require short/long") + _ => panic() + } noraise { + _ => panic() + } } ``` diff --git a/argparse/arg_action.mbt b/argparse/arg_action.mbt index f70bbae67..d051bbbd5 100644 --- a/argparse/arg_action.mbt +++ b/argparse/arg_action.mbt @@ -47,34 +47,17 @@ fn arg_action(arg : Arg) -> ArgAction { } } -///| -fn resolve_value_range(range : ValueRange) -> (Int, Int?) { - let min = match range.lower { - Some(value) => if range.lower_inclusive { value } else { value + 1 } - None => 0 - } - let max = match range.upper { - Some(value) => Some(if range.upper_inclusive { value } else { value - 1 }) - None => None - } - (min, max) -} - -///| -fn validate_value_range(range : ValueRange) -> (Int, Int?) raise ArgBuildError { - let (min, max) = resolve_value_range(range) - match max { - Some(max_value) if max_value < min => - raise ArgBuildError::Unsupported("max values must be >= min values") - _ => () - } - (min, max) -} - ///| fn arg_min_max_for_validate(arg : Arg) -> (Int, Int?) raise ArgBuildError { match arg.num_args { - Some(range) => validate_value_range(range) + Some(range) => { + match range.upper { + Some(max_value) if max_value < range.lower => + raise ArgBuildError::Unsupported("max values must be >= min values") + _ => () + } + (range.lower, range.upper) + } None => (0, None) } } @@ -82,7 +65,7 @@ fn arg_min_max_for_validate(arg : Arg) -> (Int, Int?) raise ArgBuildError { ///| fn arg_min_max(arg : Arg) -> (Int, Int?) { match arg.num_args { - Some(range) => resolve_value_range(range) + Some(range) => (range.lower, range.upper) None => (0, None) } } diff --git a/argparse/arg_group.mbt b/argparse/arg_group.mbt index 6ab3d87fa..f7736335c 100644 --- a/argparse/arg_group.mbt +++ b/argparse/arg_group.mbt @@ -45,17 +45,8 @@ pub fn ArgGroup::new( name, required, multiple, - args: clone_array_group_decl(args), - requires: clone_array_group_decl(requires), - conflicts_with: clone_array_group_decl(conflicts_with), + args: args.copy(), + requires: requires.copy(), + conflicts_with: conflicts_with.copy(), } } - -///| -fn[T] clone_array_group_decl(arr : Array[T]) -> Array[T] { - let out = Array::new(capacity=arr.length()) - for value in arr { - out.push(value) - } - out -} diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt index 1cbe26d49..29909fb0d 100644 --- a/argparse/arg_spec.mbt +++ b/argparse/arg_spec.mbt @@ -122,8 +122,8 @@ pub fn FlagArg::new( multiple: false, allow_hyphen_values: false, last: false, - requires: clone_array_spec(requires), - conflicts_with: clone_array_spec(conflicts_with), + requires: requires.copy(), + conflicts_with: conflicts_with.copy(), group, required, global, @@ -194,13 +194,13 @@ pub fn OptionArg::new( flag_action: FlagAction::SetTrue, option_action: action, env, - default_values: clone_optional_array_string(default_values), + default_values: default_values.map(Array::copy), num_args, multiple: allows_multiple_values(num_args, action), allow_hyphen_values, last, - requires: clone_array_spec(requires), - conflicts_with: clone_array_spec(conflicts_with), + requires: requires.copy(), + conflicts_with: conflicts_with.copy(), group, required, global, @@ -267,13 +267,13 @@ pub fn PositionalArg::new( flag_action: FlagAction::SetTrue, option_action: OptionAction::Set, env, - default_values: clone_optional_array_string(default_values), + default_values: default_values.map(Array::copy), num_args, multiple: range_allows_multiple(num_args), allow_hyphen_values, last, - requires: clone_array_spec(requires), - conflicts_with: clone_array_spec(conflicts_with), + requires: requires.copy(), + conflicts_with: conflicts_with.copy(), group, required, global, @@ -309,41 +309,15 @@ fn allows_multiple_values( ///| fn range_allows_multiple(range : ValueRange?) -> Bool { match range { - Some(r) => { - let min = match r.lower { - Some(value) => if r.lower_inclusive { value } else { value + 1 } - None => 0 - } - let max = match r.upper { - Some(value) => Some(if r.upper_inclusive { value } else { value - 1 }) - None => None - } - if min > 1 { + Some(r) => + if r.lower > 1 { true } else { - match max { + match r.upper { Some(value) => value > 1 None => true } } - } None => false } } - -///| -fn[T] clone_array_spec(arr : Array[T]) -> Array[T] { - let out = Array::new(capacity=arr.length()) - for value in arr { - out.push(value) - } - out -} - -///| -fn clone_optional_array_string(values : Array[String]?) -> Array[String]? { - match values { - Some(arr) => Some(clone_array_spec(arr)) - None => None - } -} diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 65526ff14..b1a1325e2 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -683,6 +683,22 @@ test "positionals force mode and dash handling" { } } +///| +test "variadic positional keeps accepting hyphen values after first token" { + let cmd = @argparse.Command("demo", args=[ + @argparse.PositionalArg( + "tail", + index=0, + num_args=@argparse.ValueRange(lower=0), + allow_hyphen_values=true, + ), + ]) + let parsed = cmd.parse(argv=["a", "-b", "--mystery"], env=empty_env()) catch { + _ => panic() + } + assert_true(parsed.values is { "tail": ["a", "-b", "--mystery"], .. }) +} + ///| test "bounded positional does not greedily consume later required values" { let cmd = @argparse.Command("demo", args=[ @@ -801,7 +817,7 @@ test "defaults and value range helpers through public API" { "mode", long="mode", default_values=["a", "b"], - num_args=@argparse.ValueRange(lower=0), + num_args=@argparse.ValueRange(lower=1), ), @argparse.OptionArg("one", long="one", default_values=["x"]), ]) @@ -822,24 +838,17 @@ test "defaults and value range helpers through public API" { @argparse.OptionArg( "tag", long="tag", - num_args=@argparse.ValueRange(upper=2), + action=@argparse.OptionAction::Append, + num_args=@argparse.ValueRange(lower=1, upper=2), ), ]) - try - upper_only.parse( - argv=["--tag", "a", "--tag", "b", "--tag", "c"], - env=empty_env(), - ) - catch { - @argparse.ArgParseError::TooManyValues(name, got, max) => { - assert_true(name == "tag") - assert_true(got == 3) - assert_true(max == 2) - } - _ => panic() - } noraise { + let upper_parsed = upper_only.parse( + argv=["--tag", "a", "--tag", "b", "--tag", "c"], + env=empty_env(), + ) catch { _ => panic() } + assert_true(upper_parsed.values is { "tag": ["a", "b", "c"], .. }) let lower_only = @argparse.Command("demo", args=[ @argparse.OptionArg( @@ -848,12 +857,13 @@ test "defaults and value range helpers through public API" { num_args=@argparse.ValueRange(lower=1), ), ]) - try lower_only.parse(argv=[], env=empty_env()) catch { - @argparse.ArgParseError::TooFewValues(name, got, min) => { - assert_true(name == "tag") - assert_true(got == 0) - assert_true(min == 1) - } + let lower_absent = lower_only.parse(argv=[], env=empty_env()) catch { + _ => panic() + } + assert_true(lower_absent.values is { "tag"? : None, .. }) + + try lower_only.parse(argv=["--tag"], env=empty_env()) catch { + @argparse.ArgParseError::MissingValue(name) => assert_true(name == "--tag") _ => panic() } noraise { _ => panic() @@ -864,7 +874,7 @@ test "defaults and value range helpers through public API" { inspect( (empty_range, single_range), content=( - #|({lower: Some(0), upper: Some(0), lower_inclusive: true, upper_inclusive: true}, {lower: Some(1), upper: Some(1), lower_inclusive: true, upper_inclusive: true}) + #|({lower: 0, upper: Some(0)}, {lower: 1, upper: Some(1)}) ), ) } @@ -885,6 +895,164 @@ test "num_args options consume argv values in one occurrence" { assert_true(parsed.sources is { "tag": @argparse.ValueSource::Argv, .. }) } +///| +test "set options reject duplicate occurrences" { + let cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg("mode", long="mode"), + ]) + try cmd.parse(argv=["--mode", "a", "--mode", "b"], env=empty_env()) catch { + @argparse.ArgParseError::InvalidArgument(msg) => + inspect(msg, content="argument '--mode' cannot be used multiple times") + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "flag and option args require short or long names" { + try + @argparse.Command("demo", args=[@argparse.OptionArg("input")]).parse( + argv=[], + env=empty_env(), + ) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect(msg, content="flag/option args require short/long") + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", args=[@argparse.FlagArg("verbose")]).parse( + argv=[], + env=empty_env(), + ) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect(msg, content="flag/option args require short/long") + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "num_args range option consumes optional extra argv value" { + let cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg( + "arg", + long="arg", + num_args=@argparse.ValueRange(lower=1, upper=2), + ), + ]) + let parsed = cmd.parse(argv=["--arg", "x", "y"], env=empty_env()) catch { + _ => panic() + } + assert_true(parsed.values is { "arg": ["x", "y"], .. }) + assert_true(parsed.sources is { "arg": @argparse.ValueSource::Argv, .. }) +} + +///| +test "num_args range option stops at the next option token" { + let cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg( + "arg", + short='a', + long="arg", + num_args=@argparse.ValueRange(lower=1, upper=2), + ), + @argparse.FlagArg("verbose", long="verbose"), + ]) + + let stopped = cmd.parse(argv=["--arg", "x", "--verbose"], env=empty_env()) catch { + _ => panic() + } + assert_true(stopped.values is { "arg": ["x"], .. }) + assert_true(stopped.flags is { "verbose": true, .. }) + + let inline = cmd.parse(argv=["--arg=x", "y", "--verbose"], env=empty_env()) catch { + _ => panic() + } + assert_true(inline.values is { "arg": ["x", "y"], .. }) + assert_true(inline.flags is { "verbose": true, .. }) + + let short_inline = cmd.parse(argv=["-ax", "y", "--verbose"], env=empty_env()) catch { + _ => panic() + } + assert_true(short_inline.values is { "arg": ["x", "y"], .. }) + assert_true(short_inline.flags is { "verbose": true, .. }) +} + +///| +test "option num_args cannot be flag-like" { + try + @argparse.Command("demo", args=[ + @argparse.OptionArg( + "opt", + long="opt", + num_args=@argparse.ValueRange(lower=0, upper=1), + ), + @argparse.FlagArg("verbose", long="verbose"), + ]).parse(argv=["--opt", "--verbose"], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect(msg, content="option args require at least one value") + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", args=[ + @argparse.OptionArg( + "opt", + long="opt", + required=true, + num_args=@argparse.ValueRange(lower=0, upper=0), + ), + ]).parse(argv=["--opt"], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect(msg, content="option args require at least one value") + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "option values reject hyphen tokens unless allow_hyphen_values is enabled" { + let strict = @argparse.Command("demo", args=[ + @argparse.OptionArg("pattern", long="pattern"), + ]) + let mut rejected = false + try strict.parse(argv=["--pattern", "-file"], env=empty_env()) catch { + @argparse.ArgParseError::MissingValue(name) => { + assert_true(name == "--pattern") + rejected = true + } + @argparse.ArgParseError::UnknownArgument(arg, _) => { + assert_true(arg == "-f") + rejected = true + } + _ => panic() + } noraise { + _ => () + } + assert_true(rejected) + + let permissive = @argparse.Command("demo", args=[ + @argparse.OptionArg("pattern", long="pattern", allow_hyphen_values=true), + ]) + let parsed = permissive.parse(argv=["--pattern", "-file"], env=empty_env()) catch { + _ => panic() + } + assert_true(parsed.values is { "pattern": ["-file"], .. }) + assert_true(parsed.sources is { "pattern": @argparse.ValueSource::Argv, .. }) +} + ///| test "from_matches uses public decoding hook" { let cmd = @argparse.Command("demo", args=[ @@ -939,7 +1107,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|help/version actions require short/long option + #|flag/option args require short/long ), ) _ => panic() @@ -991,16 +1159,24 @@ test "validation branches exposed through parse" { _ => panic() } - let ranged = @argparse.Command("demo", args=[ - @argparse.OptionArg( - "x", - long="x", - num_args=@argparse.ValueRange(lower=2, upper=2), - ), - ]).parse(argv=["--x", "a", "--x", "b"], env=empty_env()) catch { + try + @argparse.Command("demo", args=[ + @argparse.OptionArg( + "x", + long="x", + num_args=@argparse.ValueRange(lower=2, upper=2), + ), + ]).parse(argv=["--x", "a", "--x", "b"], env=empty_env()) + catch { + @argparse.ArgParseError::TooFewValues(name, got, min) => { + assert_true(name == "x") + assert_true(got == 1) + assert_true(min == 2) + } + _ => panic() + } noraise { _ => panic() } - assert_true(ranged.values is { "x": ["a", "b"], .. }) try @argparse.Command("demo", args=[ @@ -1343,3 +1519,696 @@ test "validation branches exposed through parse" { _ => panic() } } + +///| +test "builtin and custom help/version dispatch edge paths" { + let versioned = @argparse.Command("demo", version="1.2.3") + try versioned.parse(argv=["-V"], env=empty_env()) catch { + @argparse.DisplayVersion::Message(text) => assert_true(text == "1.2.3") + _ => panic() + } noraise { + _ => panic() + } + + try versioned.parse(argv=["--help"], env=empty_env()) catch { + @argparse.DisplayHelp::Message(text) => + assert_true(text.has_prefix("Usage: demo")) + _ => panic() + } noraise { + _ => panic() + } + + try versioned.parse(argv=["-hV"], env=empty_env()) catch { + @argparse.DisplayHelp::Message(_) => () + _ => panic() + } noraise { + _ => panic() + } + + try versioned.parse(argv=["-Vh"], env=empty_env()) catch { + @argparse.DisplayVersion::Message(text) => assert_true(text == "1.2.3") + _ => panic() + } noraise { + _ => panic() + } + + let long_help = @argparse.Command("demo", args=[ + @argparse.FlagArg( + "assist", + long="assist", + action=@argparse.FlagAction::Help, + ), + ]) + try long_help.parse(argv=["--assist"], env=empty_env()) catch { + @argparse.DisplayHelp::Message(text) => + assert_true(text.has_prefix("Usage: demo")) + _ => panic() + } noraise { + _ => panic() + } + + let short_help = @argparse.Command("demo", args=[ + @argparse.FlagArg("assist", short='?', action=@argparse.FlagAction::Help), + ]) + try short_help.parse(argv=["-?"], env=empty_env()) catch { + @argparse.DisplayHelp::Message(text) => + assert_true(text.has_prefix("Usage: demo")) + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "subcommand lookup falls back to positional value" { + let cmd = @argparse.Command( + "demo", + args=[@argparse.PositionalArg("input", index=0)], + subcommands=[@argparse.Command("run")], + ) + let parsed = cmd.parse(argv=["raw"], env=empty_env()) catch { _ => panic() } + assert_true(parsed.values is { "input": ["raw"], .. }) + assert_true(parsed.subcommand is None) +} + +///| +test "group validation catches unknown requires target" { + try + @argparse.Command("demo", groups=[ + @argparse.ArgGroup("g", requires=["missing"]), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect(msg, content="unknown group requires target: g -> missing") + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "group validation catches unknown conflicts_with target" { + try + @argparse.Command("demo", groups=[ + @argparse.ArgGroup("g", conflicts_with=["missing"]), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect(msg, content="unknown group conflicts_with target: g -> missing") + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "group assignment auto-creates missing group definition" { + let cmd = @argparse.Command("demo", groups=[@argparse.ArgGroup("known")], args=[ + @argparse.FlagArg("x", long="x", group="missing"), + ]) + let parsed = cmd.parse(argv=["--x"], env=empty_env()) catch { _ => panic() } + assert_true(parsed.flags is { "x": true, .. }) + let help = cmd.render_help() + assert_true(help.has_prefix("Usage: demo [options]")) +} + +///| +test "arg validation catches unknown requires target" { + try + @argparse.Command("demo", args=[ + @argparse.OptionArg("mode", long="mode", requires=["missing"]), + ]).parse(argv=["--mode", "fast"], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect(msg, content="unknown requires target: mode -> missing") + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "arg validation catches unknown conflicts_with target" { + try + @argparse.Command("demo", args=[ + @argparse.OptionArg("mode", long="mode", conflicts_with=["missing"]), + ]).parse(argv=["--mode", "fast"], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect(msg, content="unknown conflicts_with target: mode -> missing") + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "empty groups without presence do not fail" { + let grouped_ok = @argparse.Command( + "demo", + groups=[@argparse.ArgGroup("left"), @argparse.ArgGroup("right")], + args=[ + @argparse.FlagArg("l", long="left", group="left"), + @argparse.FlagArg("r", long="right", group="right"), + ], + ) + let parsed = grouped_ok.parse(argv=["--left"], env=empty_env()) catch { + _ => panic() + } + assert_true(parsed.flags is { "l": true, .. }) +} + +///| +test "help rendering edge paths stay stable" { + let required_many = @argparse.Command("demo", args=[ + @argparse.PositionalArg( + "files", + index=0, + required=true, + num_args=@argparse.ValueRange(lower=1), + ), + ]) + let required_help = required_many.render_help() + assert_true(required_help.has_prefix("Usage: demo ")) + + let short_only_builtin = @argparse.Command("demo", args=[ + @argparse.OptionArg("helpopt", long="help"), + ]) + let short_only_text = short_only_builtin.render_help() + assert_true(short_only_text.has_prefix("Usage: demo")) + try short_only_builtin.parse(argv=["-h"], env=empty_env()) catch { + @argparse.DisplayHelp::Message(_) => () + _ => panic() + } noraise { + _ => panic() + } + try short_only_builtin.parse(argv=["--help"], env=empty_env()) catch { + @argparse.ArgParseError::MissingValue(name) => assert_true(name == "--help") + _ => panic() + } noraise { + _ => panic() + } + + let long_only_builtin = @argparse.Command("demo", args=[ + @argparse.FlagArg("custom_h", short='h'), + ]) + let long_only_text = long_only_builtin.render_help() + assert_true(long_only_text.has_prefix("Usage: demo")) + try long_only_builtin.parse(argv=["--help"], env=empty_env()) catch { + @argparse.DisplayHelp::Message(_) => () + _ => panic() + } noraise { + _ => panic() + } + let custom_h = long_only_builtin.parse(argv=["-h"], env=empty_env()) catch { + _ => panic() + } + assert_true(custom_h.flags is { "custom_h": true, .. }) + + let empty_options = @argparse.Command( + "demo", + disable_help_flag=true, + disable_version_flag=true, + ) + let empty_options_help = empty_options.render_help() + assert_true(empty_options_help.has_prefix("Usage: demo")) + + let implicit_group = @argparse.Command("demo", args=[ + @argparse.PositionalArg("item", index=0, group="dyn"), + ]) + let implicit_group_help = implicit_group.render_help() + assert_true(implicit_group_help.has_prefix("Usage: demo [item]")) + + let sub_visible = @argparse.Command("demo", disable_help_subcommand=true, subcommands=[ + @argparse.Command("run"), + ]) + let sub_help = sub_visible.render_help() + assert_true(sub_help.has_prefix("Usage: demo ")) +} + +///| +test "parse error formatting covers public variants" { + assert_true( + @argparse.ArgParseError::UnknownArgument("--oops", None).to_string() == + "error: unexpected argument '--oops' found", + ) + assert_true( + @argparse.ArgParseError::InvalidArgument("--bad").to_string() == + "error: unexpected argument '--bad' found", + ) + assert_true( + @argparse.ArgParseError::InvalidArgument("custom message").to_string() == + "error: custom message", + ) + assert_true( + @argparse.ArgParseError::MissingValue("--name").to_string() == + "error: a value is required for '--name' but none was supplied", + ) + assert_true( + @argparse.ArgParseError::MissingRequired("name").to_string() == + "error: the following required argument was not provided: 'name'", + ) + assert_true( + @argparse.ArgParseError::TooFewValues("tag", 1, 2).to_string() == + "error: 'tag' requires at least 2 values but only 1 were provided", + ) + assert_true( + @argparse.ArgParseError::TooManyValues("tag", 3, 2).to_string() == + "error: 'tag' allows at most 2 values but 3 were provided", + ) + assert_true( + @argparse.ArgParseError::InvalidValue("bad int").to_string() == + "error: bad int", + ) + assert_true( + @argparse.ArgParseError::MissingGroup("mode").to_string() == + "error: the following required argument group was not provided: 'mode'", + ) + assert_true( + @argparse.ArgParseError::GroupConflict("mode").to_string() == + "error: group conflict mode", + ) +} + +///| +test "range constructors with open lower bound still validate shape rules" { + try + @argparse.Command("demo", args=[ + @argparse.OptionArg( + "tag", + long="tag", + num_args=@argparse.ValueRange::new(upper=2), + ), + ]).parse(argv=["--tag", "x"], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect(msg, content="option args require at least one value") + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "short option with bounded values reports per occurrence too few values" { + let cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg( + "x", + short='x', + num_args=@argparse.ValueRange(lower=2, upper=2), + ), + @argparse.FlagArg("verbose", short='v'), + ]) + try cmd.parse(argv=["-x", "a", "-v"], env=empty_env()) catch { + @argparse.ArgParseError::TooFewValues(name, got, min) => { + assert_true(name == "x") + assert_true(got == 1) + assert_true(min == 2) + } + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "version action dispatches on custom long and short flags" { + let cmd = @argparse.Command("demo", version="2.0.0", args=[ + @argparse.FlagArg( + "show_long", + long="show-version", + action=@argparse.FlagAction::Version, + ), + @argparse.FlagArg( + "show_short", + short='S', + action=@argparse.FlagAction::Version, + ), + ]) + + try cmd.parse(argv=["--show-version"], env=empty_env()) catch { + @argparse.DisplayVersion::Message(text) => assert_true(text == "2.0.0") + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=["-S"], env=empty_env()) catch { + @argparse.DisplayVersion::Message(text) => assert_true(text == "2.0.0") + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "required and env-fed ranged values validate after parsing" { + let required_cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg("input", long="input", required=true), + ]) + try required_cmd.parse(argv=[], env=empty_env()) catch { + @argparse.ArgParseError::MissingRequired(name) => + assert_true(name == "input") + _ => panic() + } noraise { + _ => panic() + } + + let env_min_cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg( + "pair", + long="pair", + env="PAIR", + num_args=@argparse.ValueRange(lower=2, upper=3), + ), + ]) + try env_min_cmd.parse(argv=[], env={ "PAIR": "one" }) catch { + @argparse.ArgParseError::TooFewValues(name, got, min) => { + assert_true(name == "pair") + assert_true(got == 1) + assert_true(min == 2) + } + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "positionals hit balancing branches and explicit index sorting" { + let cmd = @argparse.Command("demo", args=[ + @argparse.PositionalArg( + "first", + index=0, + num_args=@argparse.ValueRange(lower=2, upper=3), + ), + @argparse.PositionalArg("late", index=2, required=true), + @argparse.PositionalArg( + "mid", + index=1, + num_args=@argparse.ValueRange(lower=2, upper=2), + ), + ]) + + try cmd.parse(argv=["a"], env=empty_env()) catch { + @argparse.ArgParseError::TooFewValues(name, got, min) => { + assert_true(name == "first") + assert_true(got == 1) + assert_true(min == 2) + } + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "positional max clamp leaves trailing value for next positional" { + let cmd = @argparse.Command("demo", args=[ + @argparse.PositionalArg( + "items", + index=0, + num_args=@argparse.ValueRange(lower=0, upper=2), + ), + @argparse.PositionalArg("tail", index=1), + ]) + + let parsed = cmd.parse(argv=["a", "b", "c"], env=empty_env()) catch { + _ => panic() + } + assert_true(parsed.values is { "items": ["a", "b"], "tail": ["c"], .. }) +} + +///| +test "open upper range options consume option-like values with allow_hyphen_values" { + let cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg( + "arg", + long="arg", + allow_hyphen_values=true, + num_args=@argparse.ValueRange(lower=1), + ), + @argparse.FlagArg("verbose", long="verbose"), + @argparse.FlagArg("cache", long="cache", negatable=true), + @argparse.FlagArg("quiet", short='q'), + ]) + + let known_long = cmd.parse(argv=["--arg", "x", "--verbose"], env=empty_env()) catch { + _ => panic() + } + assert_true(known_long.values is { "arg": ["x", "--verbose"], .. }) + assert_true(known_long.flags is { "verbose"? : None, .. }) + + let negated = cmd.parse(argv=["--arg", "x", "--no-cache"], env=empty_env()) catch { + _ => panic() + } + assert_true(negated.values is { "arg": ["x", "--no-cache"], .. }) + assert_true(negated.flags is { "cache"? : None, .. }) + + let unknown_long_value = cmd.parse( + argv=["--arg", "x", "--mystery"], + env=empty_env(), + ) catch { + _ => panic() + } + assert_true(unknown_long_value.values is { "arg": ["x", "--mystery"], .. }) + + let known_short = cmd.parse(argv=["--arg", "x", "-q"], env=empty_env()) catch { + _ => panic() + } + assert_true(known_short.values is { "arg": ["x", "-q"], .. }) + assert_true(known_short.flags is { "quiet"? : None, .. }) + + let cmd_with_rest = @argparse.Command("demo", args=[ + @argparse.OptionArg( + "arg", + long="arg", + allow_hyphen_values=true, + num_args=@argparse.ValueRange(lower=1), + ), + @argparse.PositionalArg( + "rest", + index=0, + num_args=@argparse.ValueRange(lower=0), + allow_hyphen_values=true, + ), + ]) + let sentinel_stop = cmd_with_rest.parse( + argv=["--arg", "x", "--", "tail"], + env=empty_env(), + ) catch { + _ => panic() + } + assert_true( + sentinel_stop.values is { "arg": ["x", "--", "tail"], "rest"? : None, .. }, + ) +} + +///| +test "fixed upper range avoids consuming additional option values" { + let cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg( + "one", + long="one", + num_args=@argparse.ValueRange(lower=1, upper=1), + ), + @argparse.FlagArg("verbose", long="verbose"), + ]) + + let parsed = cmd.parse(argv=["--one", "x", "--verbose"], env=empty_env()) catch { + _ => panic() + } + assert_true(parsed.values is { "one": ["x"], .. }) + assert_true(parsed.flags is { "verbose": true, .. }) +} + +///| +test "bounded long options report too few values when next token is another option" { + let cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg( + "arg", + long="arg", + num_args=@argparse.ValueRange(lower=2, upper=2), + ), + @argparse.FlagArg("verbose", long="verbose"), + ]) + + let ok = cmd.parse(argv=["--arg", "x", "y", "--verbose"], env=empty_env()) catch { + _ => panic() + } + assert_true(ok.values is { "arg": ["x", "y"], .. }) + assert_true(ok.flags is { "verbose": true, .. }) + + try cmd.parse(argv=["--arg", "x", "--verbose"], env=empty_env()) catch { + @argparse.ArgParseError::TooFewValues(name, got, min) => { + assert_true(name == "arg") + assert_true(got == 1) + assert_true(min == 2) + } + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "short-only set options use short label in duplicate errors" { + let cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg("mode", short='m'), + ]) + try cmd.parse(argv=["-m", "a", "-m", "b"], env=empty_env()) catch { + @argparse.ArgParseError::InvalidArgument(msg) => + inspect(msg, content="argument '-m' cannot be used multiple times") + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "unknown short suggestion can be absent" { + let cmd = @argparse.Command("demo", disable_help_flag=true, args=[ + @argparse.OptionArg("name", long="name"), + ]) + + try cmd.parse(argv=["-x"], env=empty_env()) catch { + @argparse.ArgParseError::UnknownArgument(arg, hint) => { + assert_true(arg == "-x") + assert_true(hint is None) + } + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "setfalse flags apply false when present" { + let cmd = @argparse.Command("demo", args=[ + @argparse.FlagArg( + "failfast", + long="failfast", + action=@argparse.FlagAction::SetFalse, + ), + ]) + let parsed = cmd.parse(argv=["--failfast"], env=empty_env()) catch { + _ => panic() + } + assert_true(parsed.flags is { "failfast": false, .. }) + assert_true(parsed.sources is { "failfast": @argparse.ValueSource::Argv, .. }) +} + +///| +test "allow_hyphen positional treats unknown long token as value" { + let cmd = @argparse.Command("demo", args=[ + @argparse.PositionalArg("input", index=0, allow_hyphen_values=true), + @argparse.FlagArg("known", long="known"), + ]) + let parsed = cmd.parse(argv=["--mystery"], env=empty_env()) catch { + _ => panic() + } + assert_true(parsed.values is { "input": ["--mystery"], .. }) +} + +///| +test "global value from child default is merged back to parent" { + let cmd = @argparse.Command( + "demo", + args=[ + @argparse.OptionArg( + "mode", + long="mode", + default_values=["safe"], + global=true, + ), + @argparse.OptionArg("unused", long="unused", global=true), + ], + subcommands=[@argparse.Command("run")], + ) + + let parsed = cmd.parse(argv=["run"], env=empty_env()) catch { _ => panic() } + assert_true(parsed.values is { "mode": ["safe"], "unused"? : None, .. }) + assert_true(parsed.sources is { "mode": @argparse.ValueSource::Default, .. }) + assert_true( + parsed.subcommand is Some(("run", sub)) && + sub.values is { "mode": ["safe"], .. } && + sub.sources is { "mode": @argparse.ValueSource::Default, .. }, + ) +} + +///| +test "child local arg with global name does not update parent global" { + let cmd = @argparse.Command( + "demo", + args=[ + @argparse.OptionArg( + "mode", + long="mode", + default_values=["safe"], + global=true, + ), + ], + subcommands=[ + @argparse.Command("run", args=[@argparse.OptionArg("mode", long="mode")]), + ], + ) + + let parsed = cmd.parse(argv=["run", "--mode", "fast"], env=empty_env()) catch { + _ => panic() + } + assert_true(parsed.values is { "mode": ["safe"], .. }) + assert_true(parsed.sources is { "mode": @argparse.ValueSource::Default, .. }) + assert_true( + parsed.subcommand is Some(("run", sub)) && + sub.values is { "mode": ["fast"], .. } && + sub.sources is { "mode": @argparse.ValueSource::Argv, .. }, + ) +} + +///| +test "global append env value from child is merged back to parent" { + let cmd = @argparse.Command( + "demo", + args=[ + @argparse.OptionArg( + "tag", + long="tag", + action=@argparse.OptionAction::Append, + env="TAG", + global=true, + ), + ], + subcommands=[@argparse.Command("run")], + ) + + let parsed = cmd.parse(argv=["run"], env={ "TAG": "env-tag" }) catch { + _ => panic() + } + assert_true(parsed.values is { "tag": ["env-tag"], .. }) + assert_true(parsed.sources is { "tag": @argparse.ValueSource::Env, .. }) + assert_true( + parsed.subcommand is Some(("run", sub)) && + sub.values is { "tag": ["env-tag"], .. } && + sub.sources is { "tag": @argparse.ValueSource::Env, .. }, + ) +} + +///| +test "global flag set in child argv is merged back to parent" { + let cmd = @argparse.Command( + "demo", + args=[@argparse.FlagArg("verbose", long="verbose", global=true)], + subcommands=[@argparse.Command("run")], + ) + + let parsed = cmd.parse(argv=["run", "--verbose"], env=empty_env()) catch { + _ => panic() + } + assert_true(parsed.flags is { "verbose": true, .. }) + assert_true(parsed.sources is { "verbose": @argparse.ValueSource::Argv, .. }) + assert_true( + parsed.subcommand is Some(("run", sub)) && + sub.flags is { "verbose": true, .. } && + sub.sources is { "verbose": @argparse.ValueSource::Argv, .. }, + ) +} diff --git a/argparse/argparse_test.mbt b/argparse/argparse_test.mbt index 4f01465c4..4f72c1549 100644 --- a/argparse/argparse_test.mbt +++ b/argparse/argparse_test.mbt @@ -123,15 +123,86 @@ test "relationships and num args" { env=empty_env(), ) catch { - @argparse.ArgParseError::TooManyValues(name, got, max) => { + @argparse.ArgParseError::TooFewValues(name, got, min) => { inspect(name, content="tag") - inspect(got, content="3") - inspect(max, content="2") + inspect(got, content="1") + inspect(min, content="2") } _ => panic() } noraise { _ => panic() } + + let append_num_args_cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg( + "tag", + long="tag", + action=@argparse.OptionAction::Append, + num_args=@argparse.ValueRange(lower=1, upper=2), + ), + ]) + let appended = append_num_args_cmd.parse( + argv=["--tag", "a", "--tag", "b", "--tag", "c"], + env=empty_env(), + ) catch { + _ => panic() + } + assert_true(appended.values is { "tag": ["a", "b", "c"], .. }) + + let append_fixed_cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg( + "tag", + long="tag", + action=@argparse.OptionAction::Append, + num_args=@argparse.ValueRange(lower=2, upper=2), + ), + ]) + try + append_fixed_cmd.parse(argv=["--tag", "a", "--tag", "b"], env=empty_env()) + catch { + @argparse.ArgParseError::TooFewValues(name, got, min) => { + inspect(name, content="tag") + inspect(got, content="1") + inspect(min, content="2") + } + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", args=[ + @argparse.OptionArg( + "opt", + long="opt", + num_args=@argparse.ValueRange(lower=0, upper=1), + ), + @argparse.FlagArg("verbose", long="verbose"), + ]).parse(argv=["--opt", "--verbose"], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect(msg, content="option args require at least one value") + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", args=[ + @argparse.OptionArg( + "opt", + long="opt", + num_args=@argparse.ValueRange(lower=0, upper=0), + required=true, + ), + ]).parse(argv=["--opt"], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect(msg, content="option args require at least one value") + _ => panic() + } noraise { + _ => panic() + } } ///| diff --git a/argparse/command.mbt b/argparse/command.mbt index 07254510c..523c4d2a6 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -61,9 +61,9 @@ pub fn Command::new( ) -> Command { Command::{ name, - args: normalize_args(args), - groups: clone_array_cmd(groups), - subcommands: clone_array_cmd(subcommands), + args: args.map(x => x.to_arg()), + groups: groups.copy(), + subcommands: subcommands.copy(), about, version, disable_help_flag, @@ -75,15 +75,6 @@ pub fn Command::new( } } -///| -fn normalize_args(args : Array[&ArgLike]) -> Array[Arg] { - let out = Array::new(capacity=args.length()) - for arg in args { - out.push(arg.to_arg()) - } - out -} - ///| /// Render help text without parsing. pub fn Command::render_help(self : Command) -> String { @@ -111,12 +102,12 @@ fn build_matches( let values : Map[String, Array[String]] = {} let flag_counts : Map[String, Int] = {} let sources : Map[String, ValueSource] = {} - let specs = concat_decl_specs(inherited_globals, cmd.args) + let specs = inherited_globals + cmd.args for spec in specs { let name = arg_name(spec) match raw.values.get(name) { - Some(vs) => values[name] = clone_array_cmd(vs) + Some(vs) => values[name] = vs.copy() None => () } let count = raw.counts.get(name).unwrap_or(0) @@ -145,10 +136,10 @@ fn build_matches( None => () } } - let child_globals = concat_decl_specs( - inherited_globals, - collect_decl_globals(cmd.args), - ) + let child_globals = inherited_globals + + cmd.args.filter(arg => { + arg.global && (arg.long is Some(_) || arg.short is Some(_)) + }) let subcommand = match raw.parsed_subcommand { Some((name, sub_raw)) => @@ -189,26 +180,6 @@ fn build_matches( } } -///| -fn collect_decl_globals(args : Array[Arg]) -> Array[Arg] { - let globals = [] - for arg in args { - if arg.global && (arg.long is Some(_) || arg.short is Some(_)) { - globals.push(arg) - } - } - globals -} - -///| -fn concat_decl_specs(parent : Array[Arg], more : Array[Arg]) -> Array[Arg] { - let out = clone_array_cmd(parent) - for arg in more { - out.push(arg) - } - out -} - ///| fn find_decl_subcommand(subs : Array[Command], name : String) -> Command? { for sub in subs { @@ -219,18 +190,9 @@ fn find_decl_subcommand(subs : Array[Command], name : String) -> Command? { None } -///| -fn command_args(cmd : Command) -> Array[Arg] { - let args = Array::new(capacity=cmd.args.length()) - for spec in cmd.args { - args.push(spec) - } - args -} - ///| fn command_groups(cmd : Command) -> Array[ArgGroup] { - let groups = clone_array_cmd(cmd.groups) + let groups = cmd.groups.copy() for arg in cmd.args { match arg.group { Some(group_name) => @@ -259,8 +221,7 @@ fn add_arg_to_group_membership( if groups[i].args.contains(arg_name) { return } - let args = clone_array_cmd(groups[i].args) - args.push(arg_name) + let args = [..groups[i].args, arg_name] groups[i] = ArgGroup::{ ..groups[i], args, } } None => @@ -274,12 +235,3 @@ fn add_arg_to_group_membership( }) } } - -///| -fn[T] clone_array_cmd(arr : Array[T]) -> Array[T] { - let out = Array::new(capacity=arr.length()) - for value in arr { - out.push(value) - } - out -} diff --git a/argparse/help_render.mbt b/argparse/help_render.mbt index cbcbefbb9..31a43fb62 100644 --- a/argparse/help_render.mbt +++ b/argparse/help_render.mbt @@ -16,7 +16,7 @@ /// Render help text for a clap-style command. fn render_help(cmd : Command) -> String { let usage_line = "Usage: \{cmd.name}\{usage_tail(cmd)}" - let about = command_about(cmd) + let about = cmd.about.unwrap_or("") let about_section = if about == "" { "" } else { @@ -26,58 +26,14 @@ fn render_help(cmd : Command) -> String { $|\{about} ) } - let command_lines = subcommand_entries(cmd) - let commands_section = if command_lines.length() == 0 { - "" - } else { - let body = command_lines.join("\n") - ( - $| - $| - $|Commands: - $|\{body} - ) - } - let argument_lines = positional_entries(cmd) - let arguments_section = if argument_lines.length() == 0 { - "" - } else { - let body = argument_lines.join("\n") - ( - $| - $| - $|Arguments: - $|\{body} - ) - } - let option_lines = option_entries(cmd) - let options_section = if option_lines.length() == 0 { - ( - $| - $| - $|Options: - ) - } else { - let body = option_lines.join("\n") - ( - $| - $| - $|Options: - $|\{body} - ) - } - let group_lines = group_entries(cmd) - let groups_section = if group_lines.length() == 0 { - "" - } else { - let body = group_lines.join("\n") - ( - $| - $| - $|Groups: - $|\{body} - ) - } + let commands_section = render_section("Commands:", subcommand_entries(cmd)) + let arguments_section = render_section("Arguments:", positional_entries(cmd)) + let options_section = render_section( + "Options:", + option_entries(cmd), + keep_empty=true, + ) + let groups_section = render_section("Groups:", group_entries(cmd)) ( $|\{usage_line}\{about_section}\{commands_section}\{arguments_section}\{options_section}\{groups_section} $| @@ -102,8 +58,8 @@ fn usage_tail(cmd : Command) -> String { ///| fn has_options(cmd : Command) -> Bool { - for arg in command_args(cmd) { - if arg_hidden(arg) { + for arg in cmd.args { + if arg.hidden { continue } if arg.long is Some(_) || arg.short is Some(_) { @@ -115,9 +71,9 @@ fn has_options(cmd : Command) -> Bool { ///| fn positional_usage(cmd : Command) -> String { - let parts = Array::new(capacity=command_args(cmd).length()) - for arg in positional_args(command_args(cmd)) { - if arg_hidden(arg) { + let parts = Array::new(capacity=cmd.args.length()) + for arg in positional_args(cmd.args) { + if arg.hidden { continue } let required = is_required_arg(arg) @@ -138,7 +94,7 @@ fn positional_usage(cmd : Command) -> String { ///| fn option_entries(cmd : Command) -> Array[String] { - let args = command_args(cmd) + let args = cmd.args let display = Array::new(capacity=args.length() + 2) let builtin_help_short = help_flag_enabled(cmd) && !has_short_option(args, 'h') @@ -164,7 +120,7 @@ fn option_entries(cmd : Command) -> Array[String] { if arg.long is None && arg.short is None { continue } - if arg_hidden(arg) { + if arg.hidden { continue } let name = if arg_takes_value(arg) { @@ -217,9 +173,9 @@ fn builtin_option_label( ///| fn positional_entries(cmd : Command) -> Array[String] { - let display = Array::new(capacity=command_args(cmd).length()) - for arg in positional_args(command_args(cmd)) { - if arg_hidden(arg) { + let display = Array::new(capacity=cmd.args.length()) + for arg in positional_args(cmd.args) { + if arg.hidden { continue } display.push((positional_display(arg), arg_doc(arg))) @@ -231,10 +187,10 @@ fn positional_entries(cmd : Command) -> Array[String] { fn subcommand_entries(cmd : Command) -> Array[String] { let display = Array::new(capacity=cmd.subcommands.length() + 1) for sub in cmd.subcommands { - if command_hidden(sub) { + if sub.hidden { continue } - display.push((command_display(sub), command_about(sub))) + display.push((sub.name, sub.about.unwrap_or(""))) } if help_subcommand_enabled(cmd) { display.push(("help", "Print help for the subcommand(s).")) @@ -255,6 +211,33 @@ fn group_entries(cmd : Command) -> Array[String] { format_entries(display) } +///| +fn render_section( + header : String, + lines : Array[String], + keep_empty? : Bool = false, +) -> String { + if lines.length() == 0 { + if keep_empty { + ( + $| + $| + $|\{header} + ) + } else { + "" + } + } else { + let body = lines.join("\n") + ( + $| + $| + $|\{header} + $|\{body} + ) + } +} + ///| fn format_entries(display : Array[(String, String)]) -> Array[String] { let entries = Array::new(capacity=display.length()) @@ -273,11 +256,6 @@ fn format_entries(display : Array[(String, String)]) -> Array[String] { entries } -///| -fn command_display(cmd : Command) -> String { - cmd.name -} - ///| fn arg_display(arg : Arg) -> String { let parts = Array::new(capacity=2) @@ -307,16 +285,6 @@ fn positional_display(arg : Arg) -> String { } } -///| -fn command_about(cmd : Command) -> String { - cmd.about.unwrap_or("") -} - -///| -fn arg_help(arg : Arg) -> String { - arg.about.unwrap_or("") -} - ///| fn arg_doc(arg : Arg) -> String { let notes = [] @@ -335,7 +303,7 @@ fn arg_doc(arg : Arg) -> String { if is_required_arg(arg) { notes.push("required") } - let help = arg_help(arg) + let help = arg.about.unwrap_or("") if help == "" { notes.join(", ") } else if notes.length() > 0 { @@ -346,23 +314,13 @@ fn arg_doc(arg : Arg) -> String { } } -///| -fn arg_hidden(arg : Arg) -> Bool { - arg.hidden -} - -///| -fn command_hidden(cmd : Command) -> Bool { - cmd.hidden -} - ///| fn has_subcommands_for_help(cmd : Command) -> Bool { if help_subcommand_enabled(cmd) { return true } for sub in cmd.subcommands { - if !command_hidden(sub) { + if !sub.hidden { return true } } @@ -399,8 +357,8 @@ fn group_label(group : ArgGroup) -> String { ///| fn group_members(cmd : Command, group : ArgGroup) -> String { let members = [] - for arg in command_args(cmd) { - if arg_hidden(arg) { + for arg in cmd.args { + if arg.hidden { continue } if arg_in_group(arg, group) { diff --git a/argparse/parser.mbt b/argparse/parser.mbt index 5ddf4bad6..acecded95 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -48,14 +48,19 @@ fn render_help_for_context( let help_cmd = if inherited_globals.length() == 0 { cmd } else { - Command::{ - ..cmd, - args: concat_globals(inherited_globals, command_args(cmd)), - } + Command::{ ..cmd, args: inherited_globals + cmd.args } } render_help(help_cmd) } +///| +fn raise_context_help( + cmd : Command, + inherited_globals : Array[Arg], +) -> Unit raise { + raise_help(render_help_for_context(cmd, inherited_globals)) +} + ///| fn default_argv() -> Array[String] { let args = @env.args() @@ -73,16 +78,16 @@ fn parse_command( env : Map[String, String], inherited_globals : Array[Arg], ) -> Matches raise { - let args = command_args(cmd) + let args = cmd.args let groups = command_groups(cmd) let subcommands = cmd.subcommands validate_command(cmd, args, groups) if cmd.arg_required_else_help && argv.length() == 0 { - raise_help(render_help_for_context(cmd, inherited_globals)) + raise_context_help(cmd, inherited_globals) } let matches = new_matches_parse_state() let globals_here = collect_globals(args) - let child_globals = concat_globals(inherited_globals, globals_here) + let child_globals = inherited_globals + globals_here let long_index = build_long_index(inherited_globals, args) let short_index = build_short_index(inherited_globals, args) let builtin_help_short = help_flag_enabled(cmd) && @@ -115,10 +120,10 @@ fn parse_command( continue } if builtin_help_short && arg == "-h" { - raise_help(render_help_for_context(cmd, inherited_globals)) + raise_context_help(cmd, inherited_globals) } if builtin_help_long && arg == "--help" { - raise_help(render_help_for_context(cmd, inherited_globals)) + raise_context_help(cmd, inherited_globals) } if builtin_version_short && arg == "-V" { raise_version(command_version(cmd)) @@ -139,7 +144,7 @@ fn parse_command( if inline is Some(_) { raise ArgParseError::InvalidArgument(arg) } - raise_help(render_help_for_context(cmd, inherited_globals)) + raise_context_help(cmd, inherited_globals) } if builtin_version_long && name == "version" { if inline is Some(_) { @@ -180,38 +185,56 @@ fn parse_command( } Some(spec) => if arg_takes_value(spec) { - let value = if inline is Some(v) { - v + check_duplicate_set_occurrence(matches, spec) + let min_values = option_occurrence_min(spec) + let accepts_values = option_accepts_values(spec) + let mut values_start = i + 1 + let mut consumed_first = false + if inline is Some(v) { + if !accepts_values { + raise ArgParseError::InvalidArgument(arg) + } + assign_value(matches, spec, v, ValueSource::Argv) + consumed_first = true } else { - if i + 1 >= argv.length() { + let can_take_next = i + 1 < argv.length() && + !should_stop_option_value( + argv[i + 1], + spec, + long_index, + short_index, + ) + if can_take_next && accepts_values { + i = i + 1 + assign_value(matches, spec, argv[i], ValueSource::Argv) + values_start = i + 1 + consumed_first = true + } else if min_values > 0 { raise ArgParseError::MissingValue("--\{name}") + } else { + mark_option_present(matches, spec, ValueSource::Argv) } - i = i + 1 - argv[i] } - match assign_value(matches, spec, value, ValueSource::Argv) { - Ok(_) => () - Err(e) => raise e - } - match - consume_required_option_values( - matches, - spec, - argv, - i + 1, - long_index, - short_index, - ) { - Ok(consumed) => i = i + consumed - Err(e) => raise e + if consumed_first { + let consumed_more = consume_additional_option_values( + matches, spec, argv, values_start, long_index, short_index, + ) + i = i + consumed_more + let occurrence_values = 1 + consumed_more + if occurrence_values < min_values { + raise ArgParseError::TooFewValues( + spec.name, + occurrence_values, + min_values, + ) + } } } else { if inline is Some(_) { raise ArgParseError::InvalidArgument(arg) } match arg_action(spec) { - ArgAction::Help => - raise_help(render_help_for_context(cmd, inherited_globals)) + ArgAction::Help => raise_context_help(cmd, inherited_globals) ArgAction::Version => raise_version(command_version(cmd)) _ => apply_flag(matches, spec, ValueSource::Argv) } @@ -226,7 +249,7 @@ fn parse_command( while pos < arg.length() { let short = arg.get_char(pos).unwrap() if short == 'h' && builtin_help_short { - raise_help(render_help_for_context(cmd, inherited_globals)) + raise_context_help(cmd, inherited_globals) } if short == 'V' && builtin_version_short { raise_version(command_version(cmd)) @@ -236,40 +259,59 @@ fn parse_command( None => raise_unknown_short(short, short_index) } if arg_takes_value(spec) { - let value = if pos + 1 < arg.length() { - let rest0 = arg.unsafe_substring(start=pos + 1, end=arg.length()) - match rest0.strip_prefix("=") { + check_duplicate_set_occurrence(matches, spec) + let min_values = option_occurrence_min(spec) + let accepts_values = option_accepts_values(spec) + let mut values_start = i + 1 + let mut consumed_first = false + if pos + 1 < arg.length() { + let rest = arg.unsafe_substring(start=pos + 1, end=arg.length()) + let inline = match rest.strip_prefix("=") { Some(view) => view.to_string() - None => rest0 + None => rest + } + if !accepts_values { + raise ArgParseError::InvalidArgument(arg) } + assign_value(matches, spec, inline, ValueSource::Argv) + consumed_first = true } else { - if i + 1 >= argv.length() { + let can_take_next = i + 1 < argv.length() && + !should_stop_option_value( + argv[i + 1], + spec, + long_index, + short_index, + ) + if can_take_next && accepts_values { + i = i + 1 + assign_value(matches, spec, argv[i], ValueSource::Argv) + values_start = i + 1 + consumed_first = true + } else if min_values > 0 { raise ArgParseError::MissingValue("-\{short}") + } else { + mark_option_present(matches, spec, ValueSource::Argv) } - i = i + 1 - argv[i] } - match assign_value(matches, spec, value, ValueSource::Argv) { - Ok(_) => () - Err(e) => raise e - } - match - consume_required_option_values( - matches, - spec, - argv, - i + 1, - long_index, - short_index, - ) { - Ok(consumed) => i = i + consumed - Err(e) => raise e + if consumed_first { + let consumed_more = consume_additional_option_values( + matches, spec, argv, values_start, long_index, short_index, + ) + i = i + consumed_more + let occurrence_values = 1 + consumed_more + if occurrence_values < min_values { + raise ArgParseError::TooFewValues( + spec.name, + occurrence_values, + min_values, + ) + } } break } else { match arg_action(spec) { - ArgAction::Help => - raise_help(render_help_for_context(cmd, inherited_globals)) + ArgAction::Help => raise_context_help(cmd, inherited_globals) ArgAction::Version => raise_version(command_version(cmd)) _ => apply_flag(matches, spec, ValueSource::Argv) } @@ -292,11 +334,14 @@ fn parse_command( Some(sub) => { let rest = argv[i + 1:].to_array() let sub_matches = parse_command(sub, rest, env, child_globals) + let child_local_non_globals = collect_non_global_names(sub.args) matches.parsed_subcommand = Some((sub.name, sub_matches)) // Merge argv-provided globals from the subcommand parse into the parent // so globals work even when they appear after the subcommand name. - merge_globals_from_child(matches, sub_matches, child_globals) - let env_args = concat_globals(inherited_globals, args) + merge_globals_from_child( + matches, sub_matches, child_globals, child_local_non_globals, + ) + let env_args = inherited_globals + args let parent_matches = finalize_matches( cmd, args, groups, matches, positionals, positional_values, env_args, env, @@ -304,7 +349,9 @@ fn parse_command( match parent_matches.parsed_subcommand { Some((sub_name, sub_m)) => { // After parent parsing, copy the final globals into the subcommand. - propagate_globals_to_child(parent_matches, sub_m, child_globals) + propagate_globals_to_child( + parent_matches, sub_m, child_globals, child_local_non_globals, + ) parent_matches.parsed_subcommand = Some((sub_name, sub_m)) } None => () @@ -317,7 +364,7 @@ fn parse_command( positional_values.push(arg) i = i + 1 } - let env_args = concat_globals(inherited_globals, args) + let env_args = inherited_globals + args finalize_matches( cmd, args, groups, matches, positionals, positional_values, env_args, env, ) @@ -334,14 +381,8 @@ fn finalize_matches( env_args : Array[Arg], env : Map[String, String], ) -> Matches raise { - match assign_positionals(matches, positionals, positional_values) { - Ok(_) => () - Err(e) => raise e - } - match apply_env(matches, env_args, env) { - Ok(_) => () - Err(e) => raise e - } + assign_positionals(matches, positionals, positional_values) + apply_env(matches, env_args, env) apply_defaults(matches, env_args) validate_values(args, matches) validate_relationships(matches, env_args) @@ -387,16 +428,23 @@ fn validate_command( validate_arg(arg) } for sub in cmd.subcommands { - validate_command(sub, command_args(sub), command_groups(sub)) + validate_command(sub, sub.args, command_groups(sub)) } } ///| fn validate_arg(arg : Arg) -> Unit raise ArgBuildError { let positional = is_positional_arg(arg) - let has_positional_only = arg.index is Some(_) || - arg.allow_hyphen_values || - arg.last + let has_option_name = arg.long is Some(_) || arg.short is Some(_) + if positional && has_option_name { + raise ArgBuildError::Unsupported( + "positional args do not support short/long", + ) + } + if !positional && !has_option_name { + raise ArgBuildError::Unsupported("flag/option args require short/long") + } + let has_positional_only = arg.index is Some(_) || arg.last if !positional && has_positional_only { raise ArgBuildError::Unsupported( "positional-only settings require no short/long", @@ -440,6 +488,9 @@ fn validate_arg(arg : Arg) -> Unit raise ArgBuildError { ) } let (min, max) = arg_min_max_for_validate(arg) + if !positional && arg_takes_value(arg) && arg.num_args is Some(_) && min == 0 { + raise ArgBuildError::Unsupported("option args require at least one value") + } let allow_multi = arg.multiple || arg_action(arg) == ArgAction::Append if (min > 1 || (max is Some(m) && m > 1)) && !allow_multi { raise ArgBuildError::Unsupported( @@ -630,7 +681,7 @@ fn validate_version_actions(cmd : Command) -> Unit raise ArgBuildError { if cmd.version is Some(_) { return } - for arg in command_args(cmd) { + for arg in cmd.args { if arg_action(arg) == ArgAction::Version { raise ArgBuildError::Unsupported( "version action requires command version text", @@ -712,16 +763,21 @@ fn validate_values(args : Array[Arg], matches : Matches) -> Unit raise { if !arg_takes_value(arg) { continue } + if !present { + continue + } let values = matches.values.get(arg.name).unwrap_or([]) let count = values.length() let (min, max) = arg_min_max(arg) if count < min { raise ArgParseError::TooFewValues(arg.name, count, min) } - match max { - Some(max) if count > max => - raise ArgParseError::TooManyValues(arg.name, count, max) - _ => () + if arg_action(arg) != ArgAction::Append { + match max { + Some(max) if count > max => + raise ArgParseError::TooManyValues(arg.name, count, max) + _ => () + } } } } @@ -749,7 +805,7 @@ fn validate_relationships(matches : Matches, args : Array[Arg]) -> Unit raise { ///| fn is_positional_arg(arg : Arg) -> Bool { - arg.short is None && arg.long is None + arg.is_positional } ///| @@ -757,7 +813,7 @@ fn assign_positionals( matches : Matches, positionals : Array[Arg], values : Array[String], -) -> Result[Unit, ArgParseError] { +) -> Unit raise ArgParseError { let mut cursor = 0 for idx in 0.. () - Err(e) => return Err(e) - } + add_value( + matches, + arg.name, + values[cursor + taken], + arg, + ValueSource::Argv, + ) taken = taken + 1 } cursor = cursor + taken continue } if remaining > 0 { - match - add_value(matches, arg.name, values[cursor], arg, ValueSource::Argv) { - Ok(_) => () - Err(e) => return Err(e) - } + add_value(matches, arg.name, values[cursor], arg, ValueSource::Argv) cursor = cursor + 1 } } if cursor < values.length() { - return Err(ArgParseError::TooManyPositionals) + raise ArgParseError::TooManyPositionals } - Ok(()) } ///| @@ -842,7 +889,7 @@ fn add_value( value : String, arg : Arg, source : ValueSource, -) -> Result[Unit, ArgParseError] { +) -> Unit { if arg.multiple || arg_action(arg) == ArgAction::Append { let arr = matches.values.get(name).unwrap_or([]) arr.push(value) @@ -854,7 +901,6 @@ fn add_value( matches.values[name] = [value] matches.value_sources[name] = [source] } - Ok(()) } ///| @@ -863,124 +909,167 @@ fn assign_value( arg : Arg, value : String, source : ValueSource, -) -> Result[Unit, ArgParseError] { +) -> Unit raise ArgParseError { match arg_action(arg) { ArgAction::Append => add_value(matches, arg.name, value, arg, source) ArgAction::Set => add_value(matches, arg.name, value, arg, source) - ArgAction::SetTrue => - match parse_bool(value) { - Ok(flag) => { - matches.flags[arg.name] = flag - matches.flag_sources[arg.name] = source - Ok(()) - } - Err(e) => Err(e) - } - ArgAction::SetFalse => - match parse_bool(value) { - Ok(flag) => { - matches.flags[arg.name] = !flag - matches.flag_sources[arg.name] = source - Ok(()) - } - Err(e) => Err(e) - } - ArgAction::Count => - match parse_count(value) { - Ok(count) => { - matches.counts[arg.name] = count - matches.flags[arg.name] = count > 0 - matches.flag_sources[arg.name] = source - Ok(()) - } - Err(e) => Err(e) - } + ArgAction::SetTrue => { + let flag = parse_bool(value) + matches.flags[arg.name] = flag + matches.flag_sources[arg.name] = source + } + ArgAction::SetFalse => { + let flag = parse_bool(value) + matches.flags[arg.name] = !flag + matches.flag_sources[arg.name] = source + } + ArgAction::Count => { + let count = parse_count(value) + matches.counts[arg.name] = count + matches.flags[arg.name] = count > 0 + matches.flag_sources[arg.name] = source + } ArgAction::Help => - Err(ArgParseError::InvalidArgument("help action does not take values")) + raise ArgParseError::InvalidArgument("help action does not take values") ArgAction::Version => - Err(ArgParseError::InvalidArgument("version action does not take values")) + raise ArgParseError::InvalidArgument( + "version action does not take values", + ) } } ///| -fn required_option_value_count(matches : Matches, arg : Arg) -> Int { +fn option_occurrence_min(arg : Arg) -> Int { match arg.num_args { - None => 0 Some(_) => { let (min, _) = arg_min_max(arg) - if min <= 0 { - return 0 + min + } + None => 1 + } +} + +///| +fn option_accepts_values(arg : Arg) -> Bool { + match arg.num_args { + Some(_) => { + let (_, max) = arg_min_max(arg) + match max { + Some(max_count) => max_count > 0 + None => true } - let count = matches.values.get(arg.name).unwrap_or([]).length() - if count >= min { - 0 - } else { - min - count + } + None => true + } +} + +///| +fn option_conflict_label(arg : Arg) -> String { + match arg.long { + Some(name) => "--\{name}" + None => + match arg.short { + Some(short) => "-\{short}" + None => arg.name + } + } +} + +///| +fn check_duplicate_set_occurrence( + matches : Matches, + arg : Arg, +) -> Unit raise ArgParseError { + if arg_action(arg) != ArgAction::Set { + return + } + if matches.values.get(arg.name) is Some(_) { + raise ArgParseError::InvalidArgument( + "argument '\{option_conflict_label(arg)}' cannot be used multiple times", + ) + } +} + +///| +fn mark_option_present( + matches : Matches, + arg : Arg, + source : ValueSource, +) -> Unit { + if matches.values.get(arg.name) is None { + matches.values[arg.name] = [] + } + let srcs = matches.value_sources.get(arg.name).unwrap_or([]) + srcs.push(source) + matches.value_sources[arg.name] = srcs +} + +///| +fn required_option_value_count(arg : Arg) -> Int { + match arg.num_args { + None => 0 + Some(_) => { + let (_, max) = arg_min_max(arg) + match max { + Some(max_count) if max_count <= 1 => 0 + Some(max_count) => max_count - 1 + None => -1 } } } } ///| -fn consume_required_option_values( +fn consume_additional_option_values( matches : Matches, arg : Arg, argv : Array[String], start : Int, long_index : Map[String, Arg], short_index : Map[Char, Arg], -) -> Result[Int, ArgParseError] { - let need = required_option_value_count(matches, arg) - if need == 0 { - return Ok(0) +) -> Int raise ArgParseError { + let max_more = required_option_value_count(arg) + if max_more == 0 { + return 0 } let mut consumed = 0 - while consumed < need && start + consumed < argv.length() { - let value = argv[start + consumed] - if starts_known_option(value, long_index, short_index) { + while start + consumed < argv.length() { + if max_more > 0 && consumed >= max_more { break } - match assign_value(matches, arg, value, ValueSource::Argv) { - Ok(_) => () - Err(e) => return Err(e) + let value = argv[start + consumed] + if should_stop_option_value(value, arg, long_index, short_index) { + break } + assign_value(matches, arg, value, ValueSource::Argv) consumed = consumed + 1 } - Ok(consumed) + consumed } ///| -fn starts_known_option( - arg : String, - long_index : Map[String, Arg], - short_index : Map[Char, Arg], +fn should_stop_option_value( + value : String, + arg : Arg, + _long_index : Map[String, Arg], + _short_index : Map[Char, Arg], ) -> Bool { - if !arg.has_prefix("-") || arg == "-" { + if !value.has_prefix("-") || value == "-" { return false } - if arg.has_prefix("--") { - let (name, _) = split_long(arg) - if long_index.get(name) is Some(_) { - return true - } - if name.has_prefix("no-") { - let target = match name.strip_prefix("no-") { - Some(view) => view.to_string() - None => "" - } - match long_index.get(target) { - Some(spec) => !arg_takes_value(spec) && spec.negatable - None => false - } - } else { - false - } - } else { - match arg.get_char(1) { - Some(ch) => short_index.get(ch) is Some(_) - None => false - } + if arg.allow_hyphen_values { + // Rust clap parity: + // - `clap_builder/src/parser/parser.rs`: `parse_long_arg` / `parse_short_arg` + // return `ParseResult::MaybeHyphenValue` when the pending arg in + // `ParseState::Opt` or `ParseState::Pos` has `allow_hyphen_values`. + // - `clap_builder/src/builder/arg.rs` (`Arg::allow_hyphen_values` docs): + // prior args with this setting take precedence over known flags/options. + // - `tests/builder/opts.rs` (`leading_hyphen_with_flag_after`): + // a pending option consumes `-f` as a value rather than parsing flag `-f`. + // This also means `--` is consumed as a value while the option remains pending. + return false } + true } ///| @@ -988,7 +1077,7 @@ fn apply_env( matches : Matches, args : Array[Arg], env : Map[String, String], -) -> Result[Unit, ArgParseError] { +) -> Unit raise ArgParseError { for arg in args { let name = arg.name if matches_has_value_or_flag(matches, name) { @@ -1003,52 +1092,36 @@ fn apply_env( None => continue } if arg_takes_value(arg) { - match assign_value(matches, arg, value, ValueSource::Env) { - Ok(_) => () - Err(e) => return Err(e) - } + assign_value(matches, arg, value, ValueSource::Env) continue } match arg_action(arg) { - ArgAction::Count => - match parse_count(value) { - Ok(count) => { - matches.counts[name] = count - matches.flags[name] = count > 0 - matches.flag_sources[name] = ValueSource::Env - } - Err(e) => return Err(e) - } - ArgAction::SetFalse => - match parse_bool(value) { - Ok(flag) => { - matches.flags[name] = !flag - matches.flag_sources[name] = ValueSource::Env - } - Err(e) => return Err(e) - } - ArgAction::SetTrue => - match parse_bool(value) { - Ok(flag) => { - matches.flags[name] = flag - matches.flag_sources[name] = ValueSource::Env - } - Err(e) => return Err(e) - } - ArgAction::Set => - match parse_bool(value) { - Ok(flag) => { - matches.flags[name] = flag - matches.flag_sources[name] = ValueSource::Env - } - Err(e) => return Err(e) - } + ArgAction::Count => { + let count = parse_count(value) + matches.counts[name] = count + matches.flags[name] = count > 0 + matches.flag_sources[name] = ValueSource::Env + } + ArgAction::SetFalse => { + let flag = parse_bool(value) + matches.flags[name] = !flag + matches.flag_sources[name] = ValueSource::Env + } + ArgAction::SetTrue => { + let flag = parse_bool(value) + matches.flags[name] = flag + matches.flag_sources[name] = ValueSource::Env + } + ArgAction::Set => { + let flag = parse_bool(value) + matches.flags[name] = flag + matches.flag_sources[name] = ValueSource::Env + } ArgAction::Append => () ArgAction::Help => () ArgAction::Version => () } } - Ok(()) } ///| @@ -1123,40 +1196,31 @@ fn apply_flag(matches : Matches, arg : Arg, source : ValueSource) -> Unit { } ///| -fn parse_bool(value : String) -> Result[Bool, ArgParseError] { +fn parse_bool(value : String) -> Bool raise ArgParseError { if value == "1" || value == "true" || value == "yes" || value == "on" { - Ok(true) + true } else if value == "0" || value == "false" || value == "no" || value == "off" { - Ok(false) + false } else { - Err( - ArgParseError::InvalidValue( - "invalid value '\{value}' for boolean flag; expected one of: 1, 0, true, false, yes, no, on, off", - ), + raise ArgParseError::InvalidValue( + "invalid value '\{value}' for boolean flag; expected one of: 1, 0, true, false, yes, no, on, off", ) } } ///| -fn parse_count(value : String) -> Result[Int, ArgParseError] { - let res : Result[Int, Error] = try? @strconv.parse_int(value) - match res { - Ok(v) => - if v >= 0 { - Ok(v) - } else { - Err( - ArgParseError::InvalidValue( - "invalid value '\{value}' for count; expected a non-negative integer", - ), - ) - } - Err(_) => - Err( - ArgParseError::InvalidValue( - "invalid value '\{value}' for count; expected a non-negative integer", - ), +fn parse_count(value : String) -> Int raise ArgParseError { + try @strconv.parse_int(value) catch { + _ => + raise ArgParseError::InvalidValue( + "invalid value '\{value}' for count; expected a non-negative integer", + ) + } noraise { + _..<0 => + raise ArgParseError::InvalidValue( + "invalid value '\{value}' for count; expected a non-negative integer", ) + v => v } } @@ -1334,12 +1398,14 @@ fn collect_globals(args : Array[Arg]) -> Array[Arg] { } ///| -fn concat_globals(parent : Array[Arg], more : Array[Arg]) -> Array[Arg] { - let out = clone_array(parent) - for arg in more { - out.push(arg) +fn collect_non_global_names(args : Array[Arg]) -> Map[String, Bool] { + let names : Map[String, Bool] = {} + for arg in args { + if !arg.global { + names[arg.name] = true + } } - out + names } ///| @@ -1396,9 +1462,13 @@ fn merge_globals_from_child( parent : Matches, child : Matches, globals : Array[Arg], + child_local_non_globals : Map[String, Bool], ) -> Unit { for arg in globals { let name = arg.name + if child_local_non_globals.get(name) is Some(_) { + continue + } if arg_takes_value(arg) { let parent_vals = parent.values.get(name) let child_vals = child.values.get(name) @@ -1446,15 +1516,15 @@ fn merge_globals_from_child( (!has_parent || prefer_child_source(parent_source, child_source)) if choose_child { if child_vals is Some(cv) && cv.length() > 0 { - parent.values[name] = clone_array(cv) + parent.values[name] = cv.copy() } if child_srcs is Some(cs) && cs.length() > 0 { - parent.value_sources[name] = clone_array(cs) + parent.value_sources[name] = cs.copy() } } else if parent_vals is Some(pv) && pv.length() > 0 { - parent.values[name] = clone_array(pv) + parent.values[name] = pv.copy() if parent_srcs is Some(ps) && ps.length() > 0 { - parent.value_sources[name] = clone_array(ps) + parent.value_sources[name] = ps.copy() } } } @@ -1463,15 +1533,15 @@ fn merge_globals_from_child( (!has_parent || prefer_child_source(parent_source, child_source)) if choose_child { if child_vals is Some(cv) && cv.length() > 0 { - parent.values[name] = clone_array(cv) + parent.values[name] = cv.copy() } if child_srcs is Some(cs) && cs.length() > 0 { - parent.value_sources[name] = clone_array(cs) + parent.value_sources[name] = cs.copy() } } else if parent_vals is Some(pv) && pv.length() > 0 { - parent.values[name] = clone_array(pv) + parent.values[name] = pv.copy() if parent_srcs is Some(ps) && ps.length() > 0 { - parent.value_sources[name] = clone_array(ps) + parent.value_sources[name] = ps.copy() } } } @@ -1532,15 +1602,19 @@ fn propagate_globals_to_child( parent : Matches, child : Matches, globals : Array[Arg], + child_local_non_globals : Map[String, Bool], ) -> Unit { for arg in globals { let name = arg.name + if child_local_non_globals.get(name) is Some(_) { + continue + } if arg_takes_value(arg) { match parent.values.get(name) { Some(values) => { - child.values[name] = clone_array(values) + child.values[name] = values.copy() match parent.value_sources.get(name) { - Some(srcs) => child.value_sources[name] = clone_array(srcs) + Some(srcs) => child.value_sources[name] = srcs.copy() None => () } } @@ -1567,16 +1641,12 @@ fn propagate_globals_to_child( } } -///| - -///| - ///| fn positional_args(args : Array[Arg]) -> Array[Arg] { let with_index = [] let without_index = [] for arg in args { - if arg.long is None && arg.short is None { + if is_positional_arg(arg) { if arg.index is Some(idx) { with_index.push((idx, arg)) } else { @@ -1610,11 +1680,44 @@ fn last_positional_index(positionals : Array[Arg]) -> Int? { ///| fn next_positional(positionals : Array[Arg], collected : Array[String]) -> Arg? { - if collected.length() < positionals.length() { - Some(positionals[collected.length()]) - } else { - None + let target = collected.length() + let total = target + 1 + let mut cursor = 0 + for idx in 0..= total { + break + } + let arg = positionals[idx] + let remaining = total - cursor + let take = if arg.multiple { + let (min, max) = arg_min_max(arg) + let reserve = remaining_positional_min(positionals, idx + 1) + let mut take = remaining - reserve + if take < 0 { + take = 0 + } + match max { + Some(max_count) if take > max_count => take = max_count + _ => () + } + if take < min { + take = min + } + if take > remaining { + take = remaining + } + take + } else if remaining > 0 { + 1 + } else { + 0 + } + if take > 0 && target < cursor + take { + return Some(arg) + } + cursor = cursor + take } + None } ///| @@ -1721,10 +1824,7 @@ fn resolve_help_target( } match find_subcommand(subs, name) { Some(sub) => { - current_globals = concat_globals( - current_globals, - collect_globals(command_args(current)), - ) + current_globals = current_globals + collect_globals(current.args) current = sub subs = sub.subcommands } @@ -1756,12 +1856,3 @@ fn split_long(arg : String) -> (String, String?) { (name, Some(value)) } } - -///| -fn[T] clone_array(arr : Array[T]) -> Array[T] { - let out = Array::new(capacity=arr.length()) - for value in arr { - out.push(value) - } - out -} diff --git a/argparse/value_range.mbt b/argparse/value_range.mbt index 651eb27f3..91e8f9d35 100644 --- a/argparse/value_range.mbt +++ b/argparse/value_range.mbt @@ -15,10 +15,8 @@ ///| /// Number-of-values constraint for an argument. pub struct ValueRange { - priv lower : Int? + priv lower : Int priv upper : Int? - priv lower_inclusive : Bool - priv upper_inclusive : Bool fn new( lower? : Int, @@ -45,5 +43,13 @@ pub fn ValueRange::new( lower_inclusive? : Bool = true, upper_inclusive? : Bool = true, ) -> ValueRange { - ValueRange::{ lower, upper, lower_inclusive, upper_inclusive } + let lower = match lower { + None => 0 + Some(lower) => if lower_inclusive { lower } else { lower + 1 } + } + let upper = match upper { + None => None + Some(upper) => Some(if upper_inclusive { upper } else { upper - 1 }) + } + ValueRange::{ lower, upper } } From 01a0e80cbae9149494313352b19887cadb1115c7 Mon Sep 17 00:00:00 2001 From: flycloudc Date: Wed, 11 Feb 2026 17:17:58 +0800 Subject: [PATCH 03/40] refactor: build the command group in advance --- argparse/README.mbt.md | 15 + argparse/arg_action.mbt | 26 +- argparse/arg_group.mbt | 7 + argparse/arg_spec.mbt | 78 +- argparse/argparse_blackbox_test.mbt | 608 +++++++--- argparse/argparse_test.mbt | 112 +- argparse/command.mbt | 122 +- argparse/help_render.mbt | 23 +- argparse/moon.pkg | 1 + argparse/parser.mbt | 1636 ++------------------------- argparse/parser_globals_merge.mbt | 266 +++++ argparse/parser_lookup.mbt | 116 ++ argparse/parser_positionals.mbt | 158 +++ argparse/parser_suggest.mbt | 115 ++ argparse/parser_validate.mbt | 518 +++++++++ argparse/parser_values.mbt | 317 ++++++ argparse/pkg.generated.mbti | 17 +- argparse/value_range.mbt | 34 +- 18 files changed, 2216 insertions(+), 1953 deletions(-) create mode 100644 argparse/parser_globals_merge.mbt create mode 100644 argparse/parser_lookup.mbt create mode 100644 argparse/parser_positionals.mbt create mode 100644 argparse/parser_suggest.mbt create mode 100644 argparse/parser_validate.mbt create mode 100644 argparse/parser_values.mbt diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index dd493e82b..bc895e2b8 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -2,6 +2,21 @@ 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`. diff --git a/argparse/arg_action.mbt b/argparse/arg_action.mbt index d051bbbd5..3f09e86e2 100644 --- a/argparse/arg_action.mbt +++ b/argparse/arg_action.mbt @@ -49,16 +49,26 @@ fn arg_action(arg : Arg) -> ArgAction { ///| fn arg_min_max_for_validate(arg : Arg) -> (Int, Int?) raise ArgBuildError { - match arg.num_args { - Some(range) => { - match range.upper { - Some(max_value) if max_value < range.lower => - raise ArgBuildError::Unsupported("max values must be >= min values") - _ => () + 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) } - None => (0, None) + (range.lower, range.upper) + } else { + (0, None) } } diff --git a/argparse/arg_group.mbt b/argparse/arg_group.mbt index f7736335c..5147ae5fd 100644 --- a/argparse/arg_group.mbt +++ b/argparse/arg_group.mbt @@ -22,6 +22,7 @@ pub struct ArgGroup { priv requires : Array[String] priv conflicts_with : Array[String] + /// Create an argument group. fn new( name : String, required? : Bool, @@ -33,6 +34,12 @@ pub struct 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, diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt index 29909fb0d..c00949976 100644 --- a/argparse/arg_spec.mbt +++ b/argparse/arg_spec.mbt @@ -49,7 +49,6 @@ priv struct Arg { last : Bool requires : Array[String] conflicts_with : Array[String] - group : String? required : Bool global : Bool negatable : Bool @@ -60,6 +59,7 @@ priv struct Arg { /// Trait for declarative arg constructors. trait ArgLike { to_arg(Self) -> Arg + validate(Self, ValidationCtx) -> Unit raise ArgBuildError } ///| @@ -67,6 +67,7 @@ trait ArgLike { pub struct FlagArg { priv arg : Arg + /// Create a flag argument. fn new( name : String, short? : Char, @@ -76,7 +77,6 @@ pub struct FlagArg { env? : String, requires? : Array[String], conflicts_with? : Array[String], - group? : String, required? : Bool, global? : Bool, negatable? : Bool, @@ -90,6 +90,18 @@ pub impl ArgLike for FlagArg with to_arg(self : FlagArg) { } ///| +pub impl ArgLike for FlagArg with validate(self, ctx) { + validate_flag_arg(self.arg, ctx) +} + +///| +/// Create a flag argument. +/// +/// At least one of `short` or `long` must be provided. +/// +/// `global=true` makes the flag available in subcommands. +/// +/// If `negatable=true`, `--no-` is accepted for long flags. pub fn FlagArg::new( name : String, short? : Char, @@ -99,7 +111,6 @@ pub fn FlagArg::new( env? : String, requires? : Array[String] = [], conflicts_with? : Array[String] = [], - group? : String, required? : Bool = false, global? : Bool = false, negatable? : Bool = false, @@ -124,7 +135,6 @@ pub fn FlagArg::new( last: false, requires: requires.copy(), conflicts_with: conflicts_with.copy(), - group, required, global, negatable, @@ -138,6 +148,7 @@ pub fn FlagArg::new( pub struct OptionArg { priv arg : Arg + /// Create an option argument. fn new( name : String, short? : Char, @@ -146,12 +157,10 @@ pub struct OptionArg { action? : OptionAction, env? : String, default_values? : Array[String], - num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : Array[String], conflicts_with? : Array[String], - group? : String, required? : Bool, global? : Bool, hidden? : Bool, @@ -164,6 +173,18 @@ pub impl ArgLike for OptionArg with to_arg(self : OptionArg) { } ///| +pub impl ArgLike for OptionArg with validate(self, ctx) { + validate_option_arg(self.arg, ctx) +} + +///| +/// Create an option argument that consumes one value per occurrence. +/// +/// At least one of `short` or `long` must be provided. +/// +/// Use `action=Append` for repeated occurrences. +/// +/// `global=true` makes the option available in subcommands. pub fn OptionArg::new( name : String, short? : Char, @@ -172,12 +193,10 @@ pub fn OptionArg::new( action? : OptionAction = OptionAction::Set, env? : String, default_values? : Array[String], - num_args? : ValueRange, allow_hyphen_values? : Bool = false, last? : Bool = false, requires? : Array[String] = [], conflicts_with? : Array[String] = [], - group? : String, required? : Bool = false, global? : Bool = false, hidden? : Bool = false, @@ -195,13 +214,12 @@ pub fn OptionArg::new( option_action: action, env, default_values: default_values.map(Array::copy), - num_args, - multiple: allows_multiple_values(num_args, action), + num_args: None, + multiple: allows_multiple_values(action), allow_hyphen_values, last, requires: requires.copy(), conflicts_with: conflicts_with.copy(), - group, required, global, negatable: false, @@ -215,6 +233,7 @@ pub fn OptionArg::new( pub struct PositionalArg { priv arg : Arg + /// Create a positional argument. fn new( name : String, index? : Int, @@ -226,7 +245,6 @@ pub struct PositionalArg { last? : Bool, requires? : Array[String], conflicts_with? : Array[String], - group? : String, required? : Bool, global? : Bool, hidden? : Bool, @@ -239,6 +257,23 @@ pub impl ArgLike for PositionalArg with to_arg(self : PositionalArg) { } ///| +pub impl ArgLike for PositionalArg with validate(self, ctx) { + validate_positional_arg(self.arg, ctx) +} + +///| +/// Create a positional argument. +/// +/// Positional ordering: +/// - `index` is zero-based. +/// - Indexed positionals are sorted by `index`. +/// - Unindexed positionals are appended after indexed ones in declaration order. +/// +/// `num_args` controls the accepted value count. +/// +/// For indexed positionals that are not the last positional, `num_args` must be +/// omitted or exactly `ValueRange::single()` (`1..1`); other ranges are rejected +/// at build time. pub fn PositionalArg::new( name : String, index? : Int, @@ -250,7 +285,6 @@ pub fn PositionalArg::new( last? : Bool = false, requires? : Array[String] = [], conflicts_with? : Array[String] = [], - group? : String, required? : Bool = false, global? : Bool = false, hidden? : Bool = false, @@ -274,7 +308,6 @@ pub fn PositionalArg::new( last, requires: requires.copy(), conflicts_with: conflicts_with.copy(), - group, required, global, negatable: false, @@ -299,24 +332,17 @@ fn is_count_flag_spec(arg : Arg) -> Bool { } ///| -fn allows_multiple_values( - num_args : ValueRange?, - action : OptionAction, -) -> Bool { - action == OptionAction::Append || range_allows_multiple(num_args) +fn allows_multiple_values(action : OptionAction) -> Bool { + action == OptionAction::Append } ///| fn range_allows_multiple(range : ValueRange?) -> Bool { match range { Some(r) => - if r.lower > 1 { - true - } else { - match r.upper { - Some(value) => value > 1 - None => true - } + match r.upper { + Some(upper) => r.lower != upper || r.lower > 1 + None => true } None => false } diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index b1a1325e2..262bbd422 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -33,7 +33,7 @@ test "render help snapshot with groups and hidden entries" { "render", groups=[ @argparse.ArgGroup("mode", required=true, multiple=false, args=[ - "fast", "path", + "fast", "slow", "path", ]), ], subcommands=[ @@ -41,8 +41,8 @@ test "render help snapshot with groups and hidden entries" { @argparse.Command("hidden", about="hidden", hidden=true), ], args=[ - @argparse.FlagArg("fast", short='f', long="fast", group="mode"), - @argparse.FlagArg("slow", long="slow", group="mode", hidden=true), + @argparse.FlagArg("fast", short='f', long="fast"), + @argparse.FlagArg("slow", long="slow", hidden=true), @argparse.FlagArg("cache", long="cache", negatable=true, about="cache"), @argparse.OptionArg( "path", @@ -51,7 +51,6 @@ test "render help snapshot with groups and hidden entries" { env="PATH_ENV", default_values=["a", "b"], required=true, - group="mode", ), @argparse.PositionalArg("target", index=0, required=true), @argparse.PositionalArg( @@ -65,7 +64,7 @@ test "render help snapshot with groups and hidden entries" { inspect( cmd.render_help(), content=( - #|Usage: render [options] [rest...] + #|Usage: render [options] [rest...] [command] #| #|Commands: #| run run @@ -103,7 +102,6 @@ test "render help conversion coverage snapshot" { required=true, global=true, hidden=true, - group="grp", ), @argparse.OptionArg( "opt", @@ -117,7 +115,6 @@ test "render help conversion coverage snapshot" { global=true, hidden=true, conflicts_with=["pos"], - group="grp", ), @argparse.PositionalArg( "pos", @@ -129,7 +126,6 @@ test "render help conversion coverage snapshot" { last=true, requires=["opt"], conflicts_with=["f"], - group="grp", required=true, global=true, hidden=true, @@ -197,6 +193,30 @@ test "global option merges parent and child values" { ) } +///| +test "global requires is validated after parent-child merge" { + let cmd = @argparse.Command( + "demo", + args=[ + @argparse.OptionArg("mode", long="mode", requires=["config"], global=true), + @argparse.OptionArg("config", long="config", global=true), + ], + subcommands=[@argparse.Command("run")], + ) + + let parsed = cmd.parse( + argv=["--config", "a.toml", "run", "--mode", "fast"], + env=empty_env(), + ) catch { + _ => panic() + } + assert_true( + parsed.values is { "config": ["a.toml"], "mode": ["fast"], .. } && + parsed.subcommand is Some(("run", sub)) && + sub.values is { "config": ["a.toml"], "mode": ["fast"], .. }, + ) +} + ///| test "global append keeps parent argv over child env/default" { let child = @argparse.Command("run") @@ -340,6 +360,27 @@ test "global flag keeps parent argv over child env fallback" { ) } +///| +test "subcommand cannot follow positional arguments" { + let cmd = @argparse.Command( + "demo", + args=[@argparse.PositionalArg("input", index=0)], + subcommands=[@argparse.Command("run")], + ) + try cmd.parse(argv=["raw", "run"], env=empty_env()) catch { + @argparse.ArgParseError::InvalidArgument(msg) => + inspect( + msg, + content=( + #|subcommand 'run' cannot be used with positional arguments + ), + ) + _ => panic() + } noraise { + _ => panic() + } +} + ///| test "global count source keeps env across subcommand merge" { let child = @argparse.Command("run") @@ -419,7 +460,7 @@ test "help subcommand styles and errors" { inspect( text, content=( - #|Usage: demo + #|Usage: demo [command] #| #|Commands: #| echo echo @@ -704,10 +745,9 @@ test "bounded positional does not greedily consume later required values" { let cmd = @argparse.Command("demo", args=[ @argparse.PositionalArg( "first", - index=0, num_args=@argparse.ValueRange(lower=1, upper=2), ), - @argparse.PositionalArg("second", index=1, required=true), + @argparse.PositionalArg("second", required=true), ]) let two = cmd.parse(argv=["a", "b"], env=empty_env()) catch { _ => panic() } @@ -719,6 +759,43 @@ test "bounded positional does not greedily consume later required values" { assert_true(three.values is { "first": ["a", "b"], "second": ["c"], .. }) } +///| +test "indexed non-last positional allows explicit single num_args" { + let cmd = @argparse.Command("demo", args=[ + @argparse.PositionalArg( + "first", + index=0, + num_args=@argparse.ValueRange::single(), + ), + @argparse.PositionalArg("second", index=1, required=true), + ]) + + let parsed = cmd.parse(argv=["a", "b"], env=empty_env()) catch { + _ => panic() + } + assert_true(parsed.values is { "first": ["a"], "second": ["b"], .. }) +} + +///| +test "empty positional value range is rejected at build time" { + try + @argparse.Command("demo", args=[ + @argparse.PositionalArg( + "skip", + index=0, + num_args=@argparse.ValueRange(lower=0, upper=0), + ), + @argparse.PositionalArg("name", index=1, required=true), + ]).parse(argv=["alice"], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect(msg, content="empty value range (0..0) is unsupported") + _ => panic() + } noraise { + _ => panic() + } +} + ///| test "env parsing for settrue setfalse count and invalid values" { let cmd = @argparse.Command("demo", args=[ @@ -816,8 +893,8 @@ test "defaults and value range helpers through public API" { @argparse.OptionArg( "mode", long="mode", + action=@argparse.OptionAction::Append, default_values=["a", "b"], - num_args=@argparse.ValueRange(lower=1), ), @argparse.OptionArg("one", long="one", default_values=["x"]), ]) @@ -839,7 +916,6 @@ test "defaults and value range helpers through public API" { "tag", long="tag", action=@argparse.OptionAction::Append, - num_args=@argparse.ValueRange(lower=1, upper=2), ), ]) let upper_parsed = upper_only.parse( @@ -851,11 +927,7 @@ test "defaults and value range helpers through public API" { assert_true(upper_parsed.values is { "tag": ["a", "b", "c"], .. }) let lower_only = @argparse.Command("demo", args=[ - @argparse.OptionArg( - "tag", - long="tag", - num_args=@argparse.ValueRange(lower=1), - ), + @argparse.OptionArg("tag", long="tag"), ]) let lower_absent = lower_only.parse(argv=[], env=empty_env()) catch { _ => panic() @@ -869,30 +941,32 @@ test "defaults and value range helpers through public API" { _ => panic() } - let empty_range = @argparse.ValueRange::empty() let single_range = @argparse.ValueRange::single() inspect( - (empty_range, single_range), + single_range, content=( - #|({lower: 0, upper: Some(0)}, {lower: 1, upper: Some(1)}) + #|{lower: 1, upper: Some(1)} ), ) } ///| -test "num_args options consume argv values in one occurrence" { +test "options consume exactly one value per occurrence" { let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg( - "tag", - long="tag", - num_args=@argparse.ValueRange(lower=2, upper=2), - ), + @argparse.OptionArg("tag", long="tag"), ]) - let parsed = cmd.parse(argv=["--tag", "a", "b"], env=empty_env()) catch { + let parsed = cmd.parse(argv=["--tag", "a"], env=empty_env()) catch { _ => panic() } - assert_true(parsed.values is { "tag": ["a", "b"], .. }) + assert_true(parsed.values is { "tag": ["a"], .. }) assert_true(parsed.sources is { "tag": @argparse.ValueSource::Argv, .. }) + + try cmd.parse(argv=["--tag", "a", "b"], env=empty_env()) catch { + @argparse.ArgParseError::TooManyPositionals => () + _ => panic() + } noraise { + _ => panic() + } } ///| @@ -939,15 +1013,15 @@ test "flag and option args require short or long names" { } ///| -test "num_args range option consumes optional extra argv value" { +test "append options collect values across repeated occurrences" { let cmd = @argparse.Command("demo", args=[ @argparse.OptionArg( "arg", long="arg", - num_args=@argparse.ValueRange(lower=1, upper=2), + action=@argparse.OptionAction::Append, ), ]) - let parsed = cmd.parse(argv=["--arg", "x", "y"], env=empty_env()) catch { + let parsed = cmd.parse(argv=["--arg", "x", "--arg", "y"], env=empty_env()) catch { _ => panic() } assert_true(parsed.values is { "arg": ["x", "y"], .. }) @@ -955,14 +1029,9 @@ test "num_args range option consumes optional extra argv value" { } ///| -test "num_args range option stops at the next option token" { +test "option parsing stops at the next option token" { let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg( - "arg", - short='a', - long="arg", - num_args=@argparse.ValueRange(lower=1, upper=2), - ), + @argparse.OptionArg("arg", short='a', long="arg"), @argparse.FlagArg("verbose", long="verbose"), ]) @@ -972,54 +1041,40 @@ test "num_args range option stops at the next option token" { assert_true(stopped.values is { "arg": ["x"], .. }) assert_true(stopped.flags is { "verbose": true, .. }) - let inline = cmd.parse(argv=["--arg=x", "y", "--verbose"], env=empty_env()) catch { + try cmd.parse(argv=["--arg=x", "y", "--verbose"], env=empty_env()) catch { + @argparse.ArgParseError::TooManyPositionals => () + _ => panic() + } noraise { _ => panic() } - assert_true(inline.values is { "arg": ["x", "y"], .. }) - assert_true(inline.flags is { "verbose": true, .. }) - let short_inline = cmd.parse(argv=["-ax", "y", "--verbose"], env=empty_env()) catch { + try cmd.parse(argv=["-ax", "y", "--verbose"], env=empty_env()) catch { + @argparse.ArgParseError::TooManyPositionals => () + _ => panic() + } noraise { _ => panic() } - assert_true(short_inline.values is { "arg": ["x", "y"], .. }) - assert_true(short_inline.flags is { "verbose": true, .. }) } ///| -test "option num_args cannot be flag-like" { - try - @argparse.Command("demo", args=[ - @argparse.OptionArg( - "opt", - long="opt", - num_args=@argparse.ValueRange(lower=0, upper=1), - ), - @argparse.FlagArg("verbose", long="verbose"), - ]).parse(argv=["--opt", "--verbose"], env=empty_env()) - catch { - @argparse.ArgBuildError::Unsupported(msg) => - inspect(msg, content="option args require at least one value") +test "options always require a value" { + let cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg("opt", long="opt"), + @argparse.FlagArg("verbose", long="verbose"), + ]) + try cmd.parse(argv=["--opt", "--verbose"], env=empty_env()) catch { + @argparse.ArgParseError::MissingValue(name) => assert_true(name == "--opt") _ => panic() } noraise { _ => panic() } - try - @argparse.Command("demo", args=[ - @argparse.OptionArg( - "opt", - long="opt", - required=true, - num_args=@argparse.ValueRange(lower=0, upper=0), - ), - ]).parse(argv=["--opt"], env=empty_env()) - catch { - @argparse.ArgBuildError::Unsupported(msg) => - inspect(msg, content="option args require at least one value") - _ => panic() - } noraise { + let zero_value_required = @argparse.Command("demo", args=[ + @argparse.OptionArg("opt", long="opt", required=true), + ]).parse(argv=["--opt", "x"], env=empty_env()) catch { _ => panic() } + assert_true(zero_value_required.values is { "opt": ["x"], .. }) } ///| @@ -1160,19 +1215,12 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", args=[ - @argparse.OptionArg( - "x", - long="x", - num_args=@argparse.ValueRange(lower=2, upper=2), - ), - ]).parse(argv=["--x", "a", "--x", "b"], env=empty_env()) + @argparse.Command("demo", args=[@argparse.OptionArg("x", long="x")]).parse( + argv=["--x", "a", "b"], + env=empty_env(), + ) catch { - @argparse.ArgParseError::TooFewValues(name, got, min) => { - assert_true(name == "x") - assert_true(got == 1) - assert_true(min == 2) - } + @argparse.ArgParseError::TooManyPositionals => () _ => panic() } noraise { _ => panic() @@ -1187,7 +1235,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|default_values require action=Append or num_args allowing >1 + #|default_values with multiple entries require action=Append ), ) _ => panic() @@ -1197,9 +1245,8 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", args=[ - @argparse.OptionArg( + @argparse.PositionalArg( "x", - long="x", num_args=@argparse.ValueRange(lower=3, upper=2), ), ]).parse(argv=[], env=empty_env()) @@ -1216,6 +1263,86 @@ test "validation branches exposed through parse" { _ => panic() } + try + @argparse.Command("demo", args=[ + @argparse.PositionalArg( + "x", + num_args=@argparse.ValueRange(lower=-1, upper=2), + ), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|min values must be >= 0 + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", args=[ + @argparse.PositionalArg( + "x", + num_args=@argparse.ValueRange(lower=0, upper=-1), + ), + ]).parse(argv=[], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|max values must be >= 0 + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", args=[ + @argparse.PositionalArg( + "x", + index=0, + num_args=@argparse.ValueRange(lower=0, upper=2), + ), + @argparse.PositionalArg("y", index=1), + ]).parse(argv=["a"], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|indexed positional 'x' cannot set num_args unless it is the last positional or exactly 1..1 + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try + @argparse.Command("demo", args=[ + @argparse.PositionalArg("x", index=0), + @argparse.PositionalArg("y", index=0), + ]).parse(argv=["a"], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|duplicate positional index: 0 + ), + ) + _ => panic() + } noraise { + _ => panic() + } + try @argparse.Command("demo", groups=[ @argparse.ArgGroup("g"), @@ -1622,9 +1749,63 @@ test "group validation catches unknown conflicts_with target" { } ///| -test "group assignment auto-creates missing group definition" { +test "group requires/conflicts can target argument names" { + let requires_cmd = @argparse.Command( + "demo", + groups=[@argparse.ArgGroup("mode", args=["fast"], requires=["config"])], + args=[ + @argparse.FlagArg("fast", long="fast"), + @argparse.OptionArg("config", long="config"), + ], + ) + + let ok = requires_cmd.parse( + argv=["--fast", "--config", "cfg.toml"], + env=empty_env(), + ) catch { + _ => panic() + } + assert_true(ok.flags is { "fast": true, .. }) + assert_true(ok.values is { "config": ["cfg.toml"], .. }) + + try requires_cmd.parse(argv=["--fast"], env=empty_env()) catch { + @argparse.ArgParseError::MissingRequired(name) => + assert_true(name == "config") + @argparse.ArgParseError::MissingGroup(name) => assert_true(name == "config") + _ => panic() + } noraise { + _ => panic() + } + + let conflicts_cmd = @argparse.Command( + "demo", + groups=[ + @argparse.ArgGroup("mode", args=["fast"], conflicts_with=["config"]), + ], + args=[ + @argparse.FlagArg("fast", long="fast"), + @argparse.OptionArg("config", long="config"), + ], + ) + + try + conflicts_cmd.parse( + argv=["--fast", "--config", "cfg.toml"], + env=empty_env(), + ) + catch { + @argparse.ArgParseError::GroupConflict(msg) => + inspect(msg, content="mode conflicts with config") + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "group without members has no parse effect" { let cmd = @argparse.Command("demo", groups=[@argparse.ArgGroup("known")], args=[ - @argparse.FlagArg("x", long="x", group="missing"), + @argparse.FlagArg("x", long="x"), ]) let parsed = cmd.parse(argv=["--x"], env=empty_env()) catch { _ => panic() } assert_true(parsed.flags is { "x": true, .. }) @@ -1666,10 +1847,13 @@ test "arg validation catches unknown conflicts_with target" { test "empty groups without presence do not fail" { let grouped_ok = @argparse.Command( "demo", - groups=[@argparse.ArgGroup("left"), @argparse.ArgGroup("right")], + groups=[ + @argparse.ArgGroup("left", args=["l"]), + @argparse.ArgGroup("right", args=["r"]), + ], args=[ - @argparse.FlagArg("l", long="left", group="left"), - @argparse.FlagArg("r", long="right", group="right"), + @argparse.FlagArg("l", long="left"), + @argparse.FlagArg("r", long="right"), ], ) let parsed = grouped_ok.parse(argv=["--left"], env=empty_env()) catch { @@ -1734,7 +1918,7 @@ test "help rendering edge paths stay stable" { assert_true(empty_options_help.has_prefix("Usage: demo")) let implicit_group = @argparse.Command("demo", args=[ - @argparse.PositionalArg("item", index=0, group="dyn"), + @argparse.PositionalArg("item", index=0), ]) let implicit_group_help = implicit_group.render_help() assert_true(implicit_group_help.has_prefix("Usage: demo [item]")) @@ -1743,7 +1927,7 @@ test "help rendering edge paths stay stable" { @argparse.Command("run"), ]) let sub_help = sub_visible.render_help() - assert_true(sub_help.has_prefix("Usage: demo ")) + assert_true(sub_help.has_prefix("Usage: demo [command]")) } ///| @@ -1791,18 +1975,21 @@ test "parse error formatting covers public variants" { } ///| -test "range constructors with open lower bound still validate shape rules" { +test "options require one value per occurrence" { + let with_value = @argparse.Command("demo", args=[ + @argparse.OptionArg("tag", long="tag"), + ]).parse(argv=["--tag", "x"], env=empty_env()) catch { + _ => panic() + } + assert_true(with_value.values is { "tag": ["x"], .. }) + try - @argparse.Command("demo", args=[ - @argparse.OptionArg( - "tag", - long="tag", - num_args=@argparse.ValueRange::new(upper=2), - ), - ]).parse(argv=["--tag", "x"], env=empty_env()) + @argparse.Command("demo", args=[@argparse.OptionArg("tag", long="tag")]).parse( + argv=["--tag"], + env=empty_env(), + ) catch { - @argparse.ArgBuildError::Unsupported(msg) => - inspect(msg, content="option args require at least one value") + @argparse.ArgParseError::MissingValue(name) => assert_true(name == "--tag") _ => panic() } noraise { _ => panic() @@ -1810,21 +1997,19 @@ test "range constructors with open lower bound still validate shape rules" { } ///| -test "short option with bounded values reports per occurrence too few values" { +test "short options require one value before next option token" { let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg( - "x", - short='x', - num_args=@argparse.ValueRange(lower=2, upper=2), - ), + @argparse.OptionArg("x", short='x'), @argparse.FlagArg("verbose", short='v'), ]) - try cmd.parse(argv=["-x", "a", "-v"], env=empty_env()) catch { - @argparse.ArgParseError::TooFewValues(name, got, min) => { - assert_true(name == "x") - assert_true(got == 1) - assert_true(min == 2) - } + let ok = cmd.parse(argv=["-x", "a", "-v"], env=empty_env()) catch { + _ => panic() + } + assert_true(ok.values is { "x": ["a"], .. }) + assert_true(ok.flags is { "verbose": true, .. }) + + try cmd.parse(argv=["-x", "-v"], env=empty_env()) catch { + @argparse.ArgParseError::MissingValue(name) => assert_true(name == "-x") _ => panic() } noraise { _ => panic() @@ -1861,6 +2046,45 @@ test "version action dispatches on custom long and short flags" { } } +///| +test "global version action keeps parent version text in subcommand context" { + let cmd = @argparse.Command( + "demo", + version="1.0.0", + args=[ + @argparse.FlagArg( + "show_version", + short='S', + long="show-version", + action=@argparse.FlagAction::Version, + global=true, + ), + ], + subcommands=[@argparse.Command("run")], + ) + + try cmd.parse(argv=["--show-version"], env=empty_env()) catch { + @argparse.DisplayVersion::Message(text) => assert_true(text == "1.0.0") + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=["run", "--show-version"], env=empty_env()) catch { + @argparse.DisplayVersion::Message(text) => assert_true(text == "1.0.0") + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=["run", "-S"], env=empty_env()) catch { + @argparse.DisplayVersion::Message(text) => assert_true(text == "1.0.0") + _ => panic() + } noraise { + _ => panic() + } +} + ///| test "required and env-fed ranged values validate after parsing" { let required_cmd = @argparse.Command("demo", args=[ @@ -1875,45 +2099,49 @@ test "required and env-fed ranged values validate after parsing" { } let env_min_cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg( - "pair", - long="pair", - env="PAIR", - num_args=@argparse.ValueRange(lower=2, upper=3), - ), + @argparse.OptionArg("pair", long="pair", env="PAIR"), ]) - try env_min_cmd.parse(argv=[], env={ "PAIR": "one" }) catch { - @argparse.ArgParseError::TooFewValues(name, got, min) => { - assert_true(name == "pair") - assert_true(got == 1) - assert_true(min == 2) - } + let env_value = env_min_cmd.parse(argv=[], env={ "PAIR": "one" }) catch { _ => panic() - } noraise { + } + assert_true(env_value.values is { "pair": ["one"], .. }) + assert_true(env_value.sources is { "pair": @argparse.ValueSource::Env, .. }) +} + +///| +test "positionals honor explicit index sorting with last ranged positional" { + let cmd = @argparse.Command("demo", args=[ + @argparse.PositionalArg( + "late", + index=2, + num_args=@argparse.ValueRange(lower=2, upper=2), + ), + @argparse.PositionalArg("first", index=0), + @argparse.PositionalArg("mid", index=1), + ]) + + let parsed = cmd.parse(argv=["a", "b", "c", "d"], env=empty_env()) catch { _ => panic() } + assert_true( + parsed.values is { "first": ["a"], "mid": ["b"], "late": ["c", "d"], .. }, + ) } ///| -test "positionals hit balancing branches and explicit index sorting" { +test "positional num_args lower bound rejects missing argv values" { let cmd = @argparse.Command("demo", args=[ @argparse.PositionalArg( "first", index=0, num_args=@argparse.ValueRange(lower=2, upper=3), ), - @argparse.PositionalArg("late", index=2, required=true), - @argparse.PositionalArg( - "mid", - index=1, - num_args=@argparse.ValueRange(lower=2, upper=2), - ), ]) - try cmd.parse(argv=["a"], env=empty_env()) catch { + try cmd.parse(argv=[], env=empty_env()) catch { @argparse.ArgParseError::TooFewValues(name, got, min) => { assert_true(name == "first") - assert_true(got == 1) + assert_true(got == 0) assert_true(min == 2) } _ => panic() @@ -1927,10 +2155,9 @@ test "positional max clamp leaves trailing value for next positional" { let cmd = @argparse.Command("demo", args=[ @argparse.PositionalArg( "items", - index=0, num_args=@argparse.ValueRange(lower=0, upper=2), ), - @argparse.PositionalArg("tail", index=1), + @argparse.PositionalArg("tail"), ]) let parsed = cmd.parse(argv=["a", "b", "c"], env=empty_env()) catch { @@ -1940,52 +2167,42 @@ test "positional max clamp leaves trailing value for next positional" { } ///| -test "open upper range options consume option-like values with allow_hyphen_values" { +test "options with allow_hyphen_values accept option-like single values" { let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg( - "arg", - long="arg", - allow_hyphen_values=true, - num_args=@argparse.ValueRange(lower=1), - ), + @argparse.OptionArg("arg", long="arg", allow_hyphen_values=true), @argparse.FlagArg("verbose", long="verbose"), @argparse.FlagArg("cache", long="cache", negatable=true), @argparse.FlagArg("quiet", short='q'), ]) - let known_long = cmd.parse(argv=["--arg", "x", "--verbose"], env=empty_env()) catch { + let known_long = cmd.parse(argv=["--arg", "--verbose"], env=empty_env()) catch { _ => panic() } - assert_true(known_long.values is { "arg": ["x", "--verbose"], .. }) + assert_true(known_long.values is { "arg": ["--verbose"], .. }) assert_true(known_long.flags is { "verbose"? : None, .. }) - let negated = cmd.parse(argv=["--arg", "x", "--no-cache"], env=empty_env()) catch { + let negated = cmd.parse(argv=["--arg", "--no-cache"], env=empty_env()) catch { _ => panic() } - assert_true(negated.values is { "arg": ["x", "--no-cache"], .. }) + assert_true(negated.values is { "arg": ["--no-cache"], .. }) assert_true(negated.flags is { "cache"? : None, .. }) let unknown_long_value = cmd.parse( - argv=["--arg", "x", "--mystery"], + argv=["--arg", "--mystery"], env=empty_env(), ) catch { _ => panic() } - assert_true(unknown_long_value.values is { "arg": ["x", "--mystery"], .. }) + assert_true(unknown_long_value.values is { "arg": ["--mystery"], .. }) - let known_short = cmd.parse(argv=["--arg", "x", "-q"], env=empty_env()) catch { + let known_short = cmd.parse(argv=["--arg", "-q"], env=empty_env()) catch { _ => panic() } - assert_true(known_short.values is { "arg": ["x", "-q"], .. }) + assert_true(known_short.values is { "arg": ["-q"], .. }) assert_true(known_short.flags is { "quiet"? : None, .. }) let cmd_with_rest = @argparse.Command("demo", args=[ - @argparse.OptionArg( - "arg", - long="arg", - allow_hyphen_values=true, - num_args=@argparse.ValueRange(lower=1), - ), + @argparse.OptionArg("arg", long="arg", allow_hyphen_values=true), @argparse.PositionalArg( "rest", index=0, @@ -1999,19 +2216,13 @@ test "open upper range options consume option-like values with allow_hyphen_valu ) catch { _ => panic() } - assert_true( - sentinel_stop.values is { "arg": ["x", "--", "tail"], "rest"? : None, .. }, - ) + assert_true(sentinel_stop.values is { "arg": ["x"], "rest": ["tail"], .. }) } ///| -test "fixed upper range avoids consuming additional option values" { +test "single-value options avoid consuming additional option values" { let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg( - "one", - long="one", - num_args=@argparse.ValueRange(lower=1, upper=1), - ), + @argparse.OptionArg("one", long="one"), @argparse.FlagArg("verbose", long="verbose"), ]) @@ -2023,28 +2234,20 @@ test "fixed upper range avoids consuming additional option values" { } ///| -test "bounded long options report too few values when next token is another option" { +test "missing option values are reported when next token is another option" { let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg( - "arg", - long="arg", - num_args=@argparse.ValueRange(lower=2, upper=2), - ), + @argparse.OptionArg("arg", long="arg"), @argparse.FlagArg("verbose", long="verbose"), ]) - let ok = cmd.parse(argv=["--arg", "x", "y", "--verbose"], env=empty_env()) catch { + let ok = cmd.parse(argv=["--arg", "x", "--verbose"], env=empty_env()) catch { _ => panic() } - assert_true(ok.values is { "arg": ["x", "y"], .. }) + assert_true(ok.values is { "arg": ["x"], .. }) assert_true(ok.flags is { "verbose": true, .. }) - try cmd.parse(argv=["--arg", "x", "--verbose"], env=empty_env()) catch { - @argparse.ArgParseError::TooFewValues(name, got, min) => { - assert_true(name == "arg") - assert_true(got == 1) - assert_true(min == 2) - } + try cmd.parse(argv=["--arg", "--verbose"], env=empty_env()) catch { + @argparse.ArgParseError::MissingValue(name) => assert_true(name == "--arg") _ => panic() } noraise { _ => panic() @@ -2137,7 +2340,7 @@ test "global value from child default is merged back to parent" { } ///| -test "child local arg with global name does not update parent global" { +test "child global arg with inherited global name updates parent global" { let cmd = @argparse.Command( "demo", args=[ @@ -2149,15 +2352,17 @@ test "child local arg with global name does not update parent global" { ), ], subcommands=[ - @argparse.Command("run", args=[@argparse.OptionArg("mode", long="mode")]), + @argparse.Command("run", args=[ + @argparse.OptionArg("mode", long="mode", global=true), + ]), ], ) let parsed = cmd.parse(argv=["run", "--mode", "fast"], env=empty_env()) catch { _ => panic() } - assert_true(parsed.values is { "mode": ["safe"], .. }) - assert_true(parsed.sources is { "mode": @argparse.ValueSource::Default, .. }) + assert_true(parsed.values is { "mode": ["fast"], .. }) + assert_true(parsed.sources is { "mode": @argparse.ValueSource::Argv, .. }) assert_true( parsed.subcommand is Some(("run", sub)) && sub.values is { "mode": ["fast"], .. } && @@ -2165,6 +2370,33 @@ test "child local arg with global name does not update parent global" { ) } +///| +test "child local arg shadowing inherited global is rejected at build time" { + try + @argparse.Command( + "demo", + args=[ + @argparse.OptionArg( + "mode", + long="mode", + env="MODE", + default_values=["safe"], + global=true, + ), + ], + subcommands=[ + @argparse.Command("run", args=[@argparse.OptionArg("mode", long="mode")]), + ], + ).parse(argv=["run"], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + assert_true(msg.contains("shadow")) + _ => panic() + } noraise { + _ => panic() + } +} + ///| test "global append env value from child is merged back to parent" { let cmd = @argparse.Command( diff --git a/argparse/argparse_test.mbt b/argparse/argparse_test.mbt index 4f72c1549..63e82a27b 100644 --- a/argparse/argparse_test.mbt +++ b/argparse/argparse_test.mbt @@ -98,121 +98,30 @@ test "relationships and num args" { _ => panic() } - let num_args_cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg( - "tag", - long="tag", - num_args=@argparse.ValueRange(lower=2, upper=2), - ), - ]) - - try num_args_cmd.parse(argv=["--tag", "a"], env=empty_env()) catch { - @argparse.ArgParseError::TooFewValues(name, got, min) => { - inspect(name, content="tag") - inspect(got, content="1") - inspect(min, content="2") - } - _ => panic() - } noraise { - _ => panic() - } - - try - num_args_cmd.parse( - argv=["--tag", "a", "--tag", "b", "--tag", "c"], - env=empty_env(), - ) - catch { - @argparse.ArgParseError::TooFewValues(name, got, min) => { - inspect(name, content="tag") - inspect(got, content="1") - inspect(min, content="2") - } - _ => panic() - } noraise { - _ => panic() - } - - let append_num_args_cmd = @argparse.Command("demo", args=[ + let appended = @argparse.Command("demo", args=[ @argparse.OptionArg( "tag", long="tag", action=@argparse.OptionAction::Append, - num_args=@argparse.ValueRange(lower=1, upper=2), ), - ]) - let appended = append_num_args_cmd.parse( - argv=["--tag", "a", "--tag", "b", "--tag", "c"], - env=empty_env(), - ) catch { + ]).parse(argv=["--tag", "a", "--tag", "b", "--tag", "c"], env=empty_env()) catch { _ => panic() } assert_true(appended.values is { "tag": ["a", "b", "c"], .. }) - - let append_fixed_cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg( - "tag", - long="tag", - action=@argparse.OptionAction::Append, - num_args=@argparse.ValueRange(lower=2, upper=2), - ), - ]) - try - append_fixed_cmd.parse(argv=["--tag", "a", "--tag", "b"], env=empty_env()) - catch { - @argparse.ArgParseError::TooFewValues(name, got, min) => { - inspect(name, content="tag") - inspect(got, content="1") - inspect(min, content="2") - } - _ => panic() - } noraise { - _ => panic() - } - - try - @argparse.Command("demo", args=[ - @argparse.OptionArg( - "opt", - long="opt", - num_args=@argparse.ValueRange(lower=0, upper=1), - ), - @argparse.FlagArg("verbose", long="verbose"), - ]).parse(argv=["--opt", "--verbose"], env=empty_env()) - catch { - @argparse.ArgBuildError::Unsupported(msg) => - inspect(msg, content="option args require at least one value") - _ => panic() - } noraise { - _ => panic() - } - - try - @argparse.Command("demo", args=[ - @argparse.OptionArg( - "opt", - long="opt", - num_args=@argparse.ValueRange(lower=0, upper=0), - required=true, - ), - ]).parse(argv=["--opt"], env=empty_env()) - catch { - @argparse.ArgBuildError::Unsupported(msg) => - inspect(msg, content="option args require at least one value") - _ => panic() - } noraise { - _ => panic() - } } ///| test "arg groups required and multiple" { let cmd = @argparse.Command( "demo", - groups=[@argparse.ArgGroup("mode", required=true, multiple=false)], + groups=[ + @argparse.ArgGroup("mode", required=true, multiple=false, args=[ + "fast", "slow", + ]), + ], args=[ - @argparse.FlagArg("fast", long="fast", group="mode"), - @argparse.FlagArg("slow", long="slow", group="mode"), + @argparse.FlagArg("fast", long="fast"), + @argparse.FlagArg("slow", long="slow"), ], ) @@ -318,7 +227,7 @@ test "full help snapshot" { inspect( cmd.render_help(), content=( - #|Usage: demo [options] [name] + #|Usage: demo [options] [name] [command] #| #|Demo command #| @@ -505,6 +414,7 @@ test "command policies" { let sub_cmd = @argparse.Command("demo", subcommand_required=true, subcommands=[ @argparse.Command("echo"), ]) + assert_true(sub_cmd.render_help().has_prefix("Usage: demo ")) try sub_cmd.parse(argv=[], env=empty_env()) catch { @argparse.ArgParseError::MissingRequired(name) => inspect(name, content="subcommand") diff --git a/argparse/command.mbt b/argparse/command.mbt index 523c4d2a6..37a25918e 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -27,7 +27,9 @@ pub struct Command { priv arg_required_else_help : Bool priv subcommand_required : Bool priv hidden : Bool + priv mut build_error : ArgBuildError? + /// Create a declarative command specification. fn new( name : String, args? : Array[&ArgLike], @@ -45,6 +47,12 @@ pub struct Command { } ///| +/// Create a declarative command specification. +/// +/// Notes: +/// - `args` accepts `FlagArg` / `OptionArg` / `PositionalArg` via `ArgLike`. +/// - `groups` explicitly declares all group memberships and policies. +/// - Built-in `--help`/`--version` behavior can be disabled with the flags below. pub fn Command::new( name : String, args? : Array[&ArgLike] = [], @@ -59,10 +67,12 @@ pub fn Command::new( hidden? : Bool = false, groups? : Array[ArgGroup] = [], ) -> Command { - Command::{ + let (parsed_args, arg_error) = collect_args(args) + let groups = groups.copy() + let cmd = Command::{ name, - args: args.map(x => x.to_arg()), - groups: groups.copy(), + args: parsed_args, + groups, subcommands: subcommands.copy(), about, version, @@ -72,7 +82,14 @@ pub fn Command::new( arg_required_else_help, subcommand_required, hidden, + build_error: arg_error, } + if cmd.build_error is None { + validate_command(cmd, parsed_args, groups, @set.new()) catch { + err => cmd.build_error = Some(err) + } + } + cmd } ///| @@ -83,12 +100,20 @@ pub fn Command::render_help(self : Command) -> String { ///| /// Parse argv/environment according to this command spec. +/// +/// Error and event model: +/// - Raises `DisplayHelp::Message` / `DisplayVersion::Message` for display +/// actions instead of exiting the process. +/// - Raises `ArgBuildError` when the command definition is invalid. +/// - Raises `ArgParseError` when user input does not satisfy the definition. +/// +/// Value precedence is `argv > env > default_values`. pub fn Command::parse( self : Command, argv? : Array[String] = default_argv(), env? : Map[String, String] = {}, ) -> Matches raise { - let raw = parse_command(self, argv, env, []) + let raw = parse_command(self, argv, env, [], {}, {}) build_matches(self, raw, []) } @@ -143,26 +168,25 @@ fn build_matches( let subcommand = match raw.parsed_subcommand { Some((name, sub_raw)) => - match find_decl_subcommand(cmd.subcommands, name) { - Some(sub_spec) => - Some((name, build_matches(sub_spec, sub_raw, child_globals))) - None => - Some( - ( - name, - Matches::{ - flags: {}, - values: {}, - flag_counts: {}, - sources: {}, - subcommand: None, - counts: {}, - flag_sources: {}, - value_sources: {}, - parsed_subcommand: None, - }, - ), - ) + if find_decl_subcommand(cmd.subcommands, name) is Some(sub_spec) { + Some((name, build_matches(sub_spec, sub_raw, child_globals))) + } else { + Some( + ( + name, + Matches::{ + flags: {}, + values: {}, + flag_counts: {}, + sources: {}, + subcommand: None, + counts: {}, + flag_sources: {}, + value_sources: {}, + parsed_subcommand: None, + }, + ), + ) } None => None } @@ -191,47 +215,19 @@ fn find_decl_subcommand(subs : Array[Command], name : String) -> Command? { } ///| -fn command_groups(cmd : Command) -> Array[ArgGroup] { - let groups = cmd.groups.copy() - for arg in cmd.args { - match arg.group { - Some(group_name) => - add_arg_to_group_membership(groups, group_name, arg_name(arg)) - None => () - } - } - groups -} - -///| -fn add_arg_to_group_membership( - groups : Array[ArgGroup], - group_name : String, - arg_name : String, -) -> Unit { - let mut idx : Int? = None - for i = 0; i < groups.length(); i = i + 1 { - if groups[i].name == group_name { - idx = Some(i) - break +fn collect_args(specs : Array[&ArgLike]) -> (Array[Arg], ArgBuildError?) { + let args = specs.map(spec => spec.to_arg()) + let ctx = ValidationCtx::new() + let mut first_error : ArgBuildError? = None + for spec in specs { + spec.validate(ctx) catch { + err => if first_error is None { first_error = Some(err) } } } - match idx { - Some(i) => { - if groups[i].args.contains(arg_name) { - return - } - let args = [..groups[i].args, arg_name] - groups[i] = ArgGroup::{ ..groups[i], args, } + if first_error is None { + ctx.finalize() catch { + err => first_error = Some(err) } - None => - groups.push(ArgGroup::{ - name: group_name, - required: false, - multiple: true, - args: [arg_name], - requires: [], - conflicts_with: [], - }) } + (args, first_error) } diff --git a/argparse/help_render.mbt b/argparse/help_render.mbt index 31a43fb62..592b8e3b2 100644 --- a/argparse/help_render.mbt +++ b/argparse/help_render.mbt @@ -46,16 +46,29 @@ fn usage_tail(cmd : Command) -> String { if has_options(cmd) { tail = "\{tail} [options]" } - if has_subcommands_for_help(cmd) { - tail = "\{tail} " - } let pos = positional_usage(cmd) if pos != "" { tail = "\{tail} \{pos}" } + let sub = subcommand_usage(cmd) + if sub != "" { + tail = "\{tail} \{sub}" + } tail } +///| +fn subcommand_usage(cmd : Command) -> String { + if !has_subcommands_for_help(cmd) { + return "" + } + if cmd.subcommand_required { + "" + } else { + "[command]" + } +} + ///| fn has_options(cmd : Command) -> Bool { for arg in cmd.args { @@ -200,8 +213,8 @@ fn subcommand_entries(cmd : Command) -> Array[String] { ///| fn group_entries(cmd : Command) -> Array[String] { - let display = Array::new(capacity=command_groups(cmd).length()) - for group in command_groups(cmd) { + let display = Array::new(capacity=cmd.groups.length()) + for group in cmd.groups { let members = group_members(cmd, group) if members == "" { continue diff --git a/argparse/moon.pkg b/argparse/moon.pkg index cd148481a..101bb7edf 100644 --- a/argparse/moon.pkg +++ b/argparse/moon.pkg @@ -2,4 +2,5 @@ import { "moonbitlang/core/builtin", "moonbitlang/core/env", "moonbitlang/core/strconv", + "moonbitlang/core/set", } diff --git a/argparse/parser.mbt b/argparse/parser.mbt index acecded95..95ce7c2e9 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -13,12 +13,12 @@ // limitations under the License. ///| -fn raise_help(text : String) -> Unit raise { +fn raise_help(text : String) -> Unit raise DisplayHelp { raise DisplayHelp::Message(text) } ///| -fn raise_version(text : String) -> Unit raise { +fn raise_version(text : String) -> Unit raise DisplayVersion { raise DisplayVersion::Message(text) } @@ -26,7 +26,7 @@ fn raise_version(text : String) -> Unit raise { fn[T] raise_unknown_long( name : String, long_index : Map[String, Arg], -) -> T raise { +) -> T raise ArgParseError { let hint = suggest_long(name, long_index) raise ArgParseError::UnknownArgument("--\{name}", hint) } @@ -35,11 +35,18 @@ fn[T] raise_unknown_long( fn[T] raise_unknown_short( short : Char, short_index : Map[Char, Arg], -) -> T raise { +) -> T raise ArgParseError { let hint = suggest_short(short, short_index) raise ArgParseError::UnknownArgument("-\{short}", hint) } +///| +fn[T] raise_subcommand_conflict(name : String) -> T raise ArgParseError { + raise ArgParseError::InvalidArgument( + "subcommand '\{name}' cannot be used with positional arguments", + ) +} + ///| fn render_help_for_context( cmd : Command, @@ -57,7 +64,7 @@ fn render_help_for_context( fn raise_context_help( cmd : Command, inherited_globals : Array[Arg], -) -> Unit raise { +) -> Unit raise DisplayHelp { raise_help(render_help_for_context(cmd, inherited_globals)) } @@ -77,17 +84,34 @@ fn parse_command( argv : Array[String], env : Map[String, String], inherited_globals : Array[Arg], + inherited_version_long : Map[String, String], + inherited_version_short : Map[Char, String], ) -> Matches raise { + match cmd.build_error { + Some(err) => raise err + None => () + } let args = cmd.args - let groups = command_groups(cmd) + let groups = cmd.groups let subcommands = cmd.subcommands - validate_command(cmd, args, groups) if cmd.arg_required_else_help && argv.length() == 0 { raise_context_help(cmd, inherited_globals) } let matches = new_matches_parse_state() let globals_here = collect_globals(args) let child_globals = inherited_globals + globals_here + let child_version_long = inherited_version_long.copy() + let child_version_short = inherited_version_short.copy() + for global in globals_here { + if arg_action(global) == ArgAction::Version { + if global.long is Some(name) { + child_version_long[name] = command_version(cmd) + } + if global.short is Some(short) { + child_version_short[short] = command_version(cmd) + } + } + } let long_index = build_long_index(inherited_globals, args) let short_index = build_short_index(inherited_globals, args) let builtin_help_short = help_flag_enabled(cmd) && @@ -102,9 +126,13 @@ fn parse_command( let positional_values = [] let last_pos_idx = last_positional_index(positionals) let mut i = 0 + let mut positional_arg_found = false while i < argv.length() { let arg = argv[i] if arg == "--" { + if i + 1 < argv.length() { + positional_arg_found = true + } for rest in argv[i + 1:] { positional_values.push(rest) } @@ -116,6 +144,7 @@ fn parse_command( } if force_positional { positional_values.push(arg) + positional_arg_found = true i = i + 1 continue } @@ -135,6 +164,7 @@ fn parse_command( arg, positionals, positional_values, long_index, short_index, ) { positional_values.push(arg) + positional_arg_found = true i = i + 1 continue } @@ -186,16 +216,8 @@ fn parse_command( Some(spec) => if arg_takes_value(spec) { check_duplicate_set_occurrence(matches, spec) - let min_values = option_occurrence_min(spec) - let accepts_values = option_accepts_values(spec) - let mut values_start = i + 1 - let mut consumed_first = false if inline is Some(v) { - if !accepts_values { - raise ArgParseError::InvalidArgument(arg) - } assign_value(matches, spec, v, ValueSource::Argv) - consumed_first = true } else { let can_take_next = i + 1 < argv.length() && !should_stop_option_value( @@ -204,29 +226,11 @@ fn parse_command( long_index, short_index, ) - if can_take_next && accepts_values { + if can_take_next { i = i + 1 assign_value(matches, spec, argv[i], ValueSource::Argv) - values_start = i + 1 - consumed_first = true - } else if min_values > 0 { - raise ArgParseError::MissingValue("--\{name}") } else { - mark_option_present(matches, spec, ValueSource::Argv) - } - } - if consumed_first { - let consumed_more = consume_additional_option_values( - matches, spec, argv, values_start, long_index, short_index, - ) - i = i + consumed_more - let occurrence_values = 1 + consumed_more - if occurrence_values < min_values { - raise ArgParseError::TooFewValues( - spec.name, - occurrence_values, - min_values, - ) + raise ArgParseError::MissingValue("--\{name}") } } } else { @@ -235,7 +239,12 @@ fn parse_command( } match arg_action(spec) { ArgAction::Help => raise_context_help(cmd, inherited_globals) - ArgAction::Version => raise_version(command_version(cmd)) + ArgAction::Version => + raise_version( + version_text_for_long_action( + cmd, name, inherited_version_long, + ), + ) _ => apply_flag(matches, spec, ValueSource::Argv) } } @@ -260,21 +269,13 @@ fn parse_command( } if arg_takes_value(spec) { check_duplicate_set_occurrence(matches, spec) - let min_values = option_occurrence_min(spec) - let accepts_values = option_accepts_values(spec) - let mut values_start = i + 1 - let mut consumed_first = false if pos + 1 < arg.length() { let rest = arg.unsafe_substring(start=pos + 1, end=arg.length()) let inline = match rest.strip_prefix("=") { Some(view) => view.to_string() None => rest } - if !accepts_values { - raise ArgParseError::InvalidArgument(arg) - } assign_value(matches, spec, inline, ValueSource::Argv) - consumed_first = true } else { let can_take_next = i + 1 < argv.length() && !should_stop_option_value( @@ -283,36 +284,23 @@ fn parse_command( long_index, short_index, ) - if can_take_next && accepts_values { + if can_take_next { i = i + 1 assign_value(matches, spec, argv[i], ValueSource::Argv) - values_start = i + 1 - consumed_first = true - } else if min_values > 0 { - raise ArgParseError::MissingValue("-\{short}") } else { - mark_option_present(matches, spec, ValueSource::Argv) - } - } - if consumed_first { - let consumed_more = consume_additional_option_values( - matches, spec, argv, values_start, long_index, short_index, - ) - i = i + consumed_more - let occurrence_values = 1 + consumed_more - if occurrence_values < min_values { - raise ArgParseError::TooFewValues( - spec.name, - occurrence_values, - min_values, - ) + raise ArgParseError::MissingValue("-\{short}") } } break } else { match arg_action(spec) { ArgAction::Help => raise_context_help(cmd, inherited_globals) - ArgAction::Version => raise_version(command_version(cmd)) + ArgAction::Version => + raise_version( + version_text_for_short_action( + cmd, short, inherited_version_short, + ), + ) _ => apply_flag(matches, spec, ValueSource::Argv) } } @@ -322,6 +310,9 @@ fn parse_command( continue } if help_subcommand_enabled(cmd) && arg == "help" { + if positional_arg_found { + raise_subcommand_conflict("help") + } let rest = argv[i + 1:].to_array() let (target, target_globals) = resolve_help_target( cmd, rest, builtin_help_short, builtin_help_long, inherited_globals, @@ -329,45 +320,49 @@ fn parse_command( let text = render_help_for_context(target, target_globals) raise_help(text) } - if subcommands.length() > 0 { - match find_subcommand(subcommands, arg) { - Some(sub) => { - let rest = argv[i + 1:].to_array() - let sub_matches = parse_command(sub, rest, env, child_globals) - let child_local_non_globals = collect_non_global_names(sub.args) - matches.parsed_subcommand = Some((sub.name, sub_matches)) - // Merge argv-provided globals from the subcommand parse into the parent - // so globals work even when they appear after the subcommand name. - merge_globals_from_child( - matches, sub_matches, child_globals, child_local_non_globals, - ) - let env_args = inherited_globals + args - let parent_matches = finalize_matches( - cmd, args, groups, matches, positionals, positional_values, env_args, - env, + if subcommands.iter().find_first(sub => sub.name == arg) is Some(sub) { + if positional_arg_found { + raise_subcommand_conflict(sub.name) + } + let rest = argv[i + 1:].to_array() + let sub_matches = parse_command( + sub, rest, env, child_globals, child_version_long, child_version_short, + ) + let child_local_non_globals = collect_non_global_names(sub.args) + matches.parsed_subcommand = Some((sub.name, sub_matches)) + // Merge argv-provided globals from the subcommand parse into the parent + // so globals work even when they appear after the subcommand name. + merge_globals_from_child( + matches, sub_matches, child_globals, child_local_non_globals, + ) + let env_args = inherited_globals + args + let parent_matches = finalize_matches( + cmd, args, groups, matches, positionals, positional_values, env_args, env, + ) + validate_relationships(parent_matches, args) + match parent_matches.parsed_subcommand { + Some((sub_name, sub_m)) => { + // After parent parsing, copy the final globals into the subcommand. + propagate_globals_to_child( + parent_matches, sub_m, child_globals, child_local_non_globals, ) - match parent_matches.parsed_subcommand { - Some((sub_name, sub_m)) => { - // After parent parsing, copy the final globals into the subcommand. - propagate_globals_to_child( - parent_matches, sub_m, child_globals, child_local_non_globals, - ) - parent_matches.parsed_subcommand = Some((sub_name, sub_m)) - } - None => () - } - return parent_matches + parent_matches.parsed_subcommand = Some((sub_name, sub_m)) } None => () } + return parent_matches } + positional_values.push(arg) + positional_arg_found = true i = i + 1 } let env_args = inherited_globals + args - finalize_matches( + let final_matches = finalize_matches( cmd, args, groups, matches, positionals, positional_values, env_args, env, ) + validate_relationships(final_matches, args) + final_matches } ///| @@ -380,12 +375,11 @@ fn finalize_matches( positional_values : Array[String], env_args : Array[Arg], env : Map[String, String], -) -> Matches raise { +) -> Matches raise ArgParseError { assign_positionals(matches, positionals, positional_values) apply_env(matches, env_args, env) apply_defaults(matches, env_args) validate_values(args, matches) - validate_relationships(matches, env_args) validate_groups(args, groups, matches) validate_command_policies(cmd, matches) matches @@ -412,1447 +406,33 @@ fn command_version(cmd : Command) -> String { } ///| -fn validate_command( +fn version_text_for_long_action( cmd : Command, - args : Array[Arg], - groups : Array[ArgGroup], -) -> Unit raise ArgBuildError { - validate_group_defs(groups) - validate_group_refs(args, groups) - validate_arg_defs(args) - validate_subcommand_defs(cmd.subcommands) - validate_subcommand_required_policy(cmd) - validate_help_subcommand(cmd) - validate_version_actions(cmd) - for arg in args { - validate_arg(arg) - } - for sub in cmd.subcommands { - validate_command(sub, sub.args, command_groups(sub)) - } -} - -///| -fn validate_arg(arg : Arg) -> Unit raise ArgBuildError { - let positional = is_positional_arg(arg) - let has_option_name = arg.long is Some(_) || arg.short is Some(_) - if positional && has_option_name { - raise ArgBuildError::Unsupported( - "positional args do not support short/long", - ) - } - if !positional && !has_option_name { - raise ArgBuildError::Unsupported("flag/option args require short/long") - } - let has_positional_only = arg.index is Some(_) || arg.last - if !positional && has_positional_only { - raise ArgBuildError::Unsupported( - "positional-only settings require no short/long", - ) - } - if arg.negatable && arg_takes_value(arg) { - raise ArgBuildError::Unsupported("negatable is only supported for flags") - } - if arg_action(arg) == ArgAction::Count && arg_takes_value(arg) { - raise ArgBuildError::Unsupported("count is only supported for flags") - } - if arg_action(arg) == ArgAction::Help || arg_action(arg) == ArgAction::Version { - if arg_takes_value(arg) { - raise ArgBuildError::Unsupported("help/version actions require flags") - } - if arg.negatable { - raise ArgBuildError::Unsupported( - "help/version actions do not support negatable", - ) - } - if arg.env is Some(_) || arg.default_values is Some(_) { - raise ArgBuildError::Unsupported( - "help/version actions do not support env/defaults", - ) - } - if arg.num_args is Some(_) || arg.multiple { - raise ArgBuildError::Unsupported( - "help/version actions do not support multiple values", - ) - } - let has_option = arg.long is Some(_) || arg.short is Some(_) - if !has_option { - raise ArgBuildError::Unsupported( - "help/version actions require short/long option", - ) - } - } - if arg.num_args is Some(_) && !arg_takes_value(arg) { - raise ArgBuildError::Unsupported( - "min/max values require value-taking arguments", - ) - } - let (min, max) = arg_min_max_for_validate(arg) - if !positional && arg_takes_value(arg) && arg.num_args is Some(_) && min == 0 { - raise ArgBuildError::Unsupported("option args require at least one value") - } - let allow_multi = arg.multiple || arg_action(arg) == ArgAction::Append - if (min > 1 || (max is Some(m) && m > 1)) && !allow_multi { - raise ArgBuildError::Unsupported( - "multiple values require action=Append or num_args allowing >1", - ) - } - if arg.default_values is Some(_) && !arg_takes_value(arg) { - raise ArgBuildError::Unsupported( - "default values require value-taking arguments", - ) - } - match arg.default_values { - Some(values) if values.length() > 1 && - !arg.multiple && - arg_action(arg) != ArgAction::Append => - raise ArgBuildError::Unsupported( - "default_values require action=Append or num_args allowing >1", - ) - _ => () - } -} - -///| -fn validate_group_defs(groups : Array[ArgGroup]) -> Unit raise ArgBuildError { - let seen : Map[String, Bool] = {} - for group in groups { - if seen.get(group.name) is Some(_) { - raise ArgBuildError::Unsupported("duplicate group: \{group.name}") - } - seen[group.name] = true - } - for group in groups { - for required in group.requires { - if required == group.name { - raise ArgBuildError::Unsupported( - "group cannot require itself: \{group.name}", - ) - } - if seen.get(required) is None { - raise ArgBuildError::Unsupported( - "unknown group requires target: \{group.name} -> \{required}", - ) - } - } - for conflict in group.conflicts_with { - if conflict == group.name { - raise ArgBuildError::Unsupported( - "group cannot conflict with itself: \{group.name}", - ) - } - if seen.get(conflict) is None { - raise ArgBuildError::Unsupported( - "unknown group conflicts_with target: \{group.name} -> \{conflict}", - ) - } - } - } -} - -///| -fn validate_group_refs( - args : Array[Arg], - groups : Array[ArgGroup], -) -> Unit raise ArgBuildError { - if groups.length() == 0 { - return - } - let group_index : Map[String, Bool] = {} - for group in groups { - group_index[group.name] = true - } - let arg_index : Map[String, Bool] = {} - for arg in args { - arg_index[arg.name] = true - } - for arg in args { - match arg.group { - Some(name) => - if group_index.get(name) is None { - raise ArgBuildError::Unsupported("unknown group: \{name}") - } - None => () - } - } - for group in groups { - for name in group.args { - if arg_index.get(name) is None { - raise ArgBuildError::Unsupported( - "unknown group arg: \{group.name} -> \{name}", - ) - } - } - } -} - -///| -fn validate_arg_defs(args : Array[Arg]) -> Unit raise ArgBuildError { - let seen_names : Map[String, Bool] = {} - let seen_long : Map[String, Bool] = {} - let seen_short : Map[Char, Bool] = {} - for arg in args { - if seen_names.get(arg.name) is Some(_) { - raise ArgBuildError::Unsupported("duplicate arg name: \{arg.name}") - } - seen_names[arg.name] = true - for name in collect_long_names(arg) { - if seen_long.get(name) is Some(_) { - raise ArgBuildError::Unsupported("duplicate long option: --\{name}") - } - seen_long[name] = true - } - for short in collect_short_names(arg) { - if seen_short.get(short) is Some(_) { - raise ArgBuildError::Unsupported("duplicate short option: -\{short}") - } - seen_short[short] = true - } - } - for arg in args { - for required in arg.requires { - if required == arg.name { - raise ArgBuildError::Unsupported( - "arg cannot require itself: \{arg.name}", - ) - } - if seen_names.get(required) is None { - raise ArgBuildError::Unsupported( - "unknown requires target: \{arg.name} -> \{required}", - ) - } - } - for conflict in arg.conflicts_with { - if conflict == arg.name { - raise ArgBuildError::Unsupported( - "arg cannot conflict with itself: \{arg.name}", - ) - } - if seen_names.get(conflict) is None { - raise ArgBuildError::Unsupported( - "unknown conflicts_with target: \{arg.name} -> \{conflict}", - ) - } - } - } -} - -///| -fn validate_subcommand_defs(subs : Array[Command]) -> Unit raise ArgBuildError { - if subs.length() == 0 { - return - } - let seen : Map[String, Bool] = {} - for sub in subs { - for name in collect_subcommand_names(sub) { - if seen.get(name) is Some(_) { - raise ArgBuildError::Unsupported("duplicate subcommand: \{name}") - } - seen[name] = true + long : String, + inherited_version_long : Map[String, String], +) -> String { + for arg in cmd.args { + if arg.long is Some(name) && + name == long && + arg_action(arg) == ArgAction::Version { + return command_version(cmd) } } + inherited_version_long.get(long).unwrap_or(command_version(cmd)) } ///| -fn validate_subcommand_required_policy( +fn version_text_for_short_action( cmd : Command, -) -> Unit raise ArgBuildError { - if cmd.subcommand_required && cmd.subcommands.length() == 0 { - raise ArgBuildError::Unsupported( - "subcommand_required requires at least one subcommand", - ) - } -} - -///| -fn validate_help_subcommand(cmd : Command) -> Unit raise ArgBuildError { - if !help_subcommand_enabled(cmd) { - return - } - if find_subcommand(cmd.subcommands, "help") is Some(_) { - raise ArgBuildError::Unsupported( - "subcommand name reserved for built-in help: help (disable with disable_help_subcommand)", - ) - } -} - -///| -fn validate_version_actions(cmd : Command) -> Unit raise ArgBuildError { - if cmd.version is Some(_) { - return - } + short : Char, + inherited_version_short : Map[Char, String], +) -> String { for arg in cmd.args { - if arg_action(arg) == ArgAction::Version { - raise ArgBuildError::Unsupported( - "version action requires command version text", - ) - } - } -} - -///| -fn validate_command_policies(cmd : Command, matches : Matches) -> Unit raise { - if cmd.subcommand_required && - cmd.subcommands.length() > 0 && - matches.parsed_subcommand is None { - raise ArgParseError::MissingRequired("subcommand") - } -} - -///| -fn validate_groups( - args : Array[Arg], - groups : Array[ArgGroup], - matches : Matches, -) -> Unit raise { - if groups.length() == 0 { - return - } - let group_presence : Map[String, Int] = {} - for group in groups { - let mut count = 0 - for arg in args { - if !arg_in_group(arg, group) { - continue - } - if matches_has_value_or_flag(matches, arg.name) { - count = count + 1 - } - } - group_presence[group.name] = count - if group.required && count == 0 { - raise ArgParseError::MissingGroup(group.name) - } - if !group.multiple && count > 1 { - raise ArgParseError::GroupConflict(group.name) - } - } - for group in groups { - let count = group_presence[group.name] - if count == 0 { - continue - } - for required in group.requires { - if group_presence.get(required).unwrap_or(0) == 0 { - raise ArgParseError::MissingGroup(required) - } - } - for conflict in group.conflicts_with { - if group_presence.get(conflict).unwrap_or(0) > 0 { - raise ArgParseError::GroupConflict( - "\{group.name} conflicts with \{conflict}", - ) - } - } - } -} - -///| -fn arg_in_group(arg : Arg, group : ArgGroup) -> Bool { - let from_arg = arg.group is Some(name) && name == group.name - from_arg || group.args.contains(arg.name) -} - -///| -fn validate_values(args : Array[Arg], matches : Matches) -> Unit raise { - for arg in args { - let present = matches_has_value_or_flag(matches, arg.name) - if arg.required && !present { - raise ArgParseError::MissingRequired(arg.name) - } - if !arg_takes_value(arg) { - continue - } - if !present { - continue - } - let values = matches.values.get(arg.name).unwrap_or([]) - let count = values.length() - let (min, max) = arg_min_max(arg) - if count < min { - raise ArgParseError::TooFewValues(arg.name, count, min) - } - if arg_action(arg) != ArgAction::Append { - match max { - Some(max) if count > max => - raise ArgParseError::TooManyValues(arg.name, count, max) - _ => () - } - } - } -} - -///| -fn validate_relationships(matches : Matches, args : Array[Arg]) -> Unit raise { - for arg in args { - if !matches_has_value_or_flag(matches, arg.name) { - continue - } - for required in arg.requires { - if !matches_has_value_or_flag(matches, required) { - raise ArgParseError::MissingRequired(required) - } - } - for conflict in arg.conflicts_with { - if matches_has_value_or_flag(matches, conflict) { - raise ArgParseError::InvalidArgument( - "conflicting arguments: \{arg.name} and \{conflict}", - ) - } - } - } -} - -///| -fn is_positional_arg(arg : Arg) -> Bool { - arg.is_positional -} - -///| -fn assign_positionals( - matches : Matches, - positionals : Array[Arg], - values : Array[String], -) -> Unit raise ArgParseError { - let mut cursor = 0 - for idx in 0.. max_count => take = max_count - _ => () - } - if take < min { - take = min - } - if take > remaining { - take = remaining - } - let mut taken = 0 - while taken < take { - add_value( - matches, - arg.name, - values[cursor + taken], - arg, - ValueSource::Argv, - ) - taken = taken + 1 - } - cursor = cursor + taken - continue - } - if remaining > 0 { - add_value(matches, arg.name, values[cursor], arg, ValueSource::Argv) - cursor = cursor + 1 - } - } - if cursor < values.length() { - raise ArgParseError::TooManyPositionals - } -} - -///| -fn positional_min_required(arg : Arg) -> Int { - let (min, _) = arg_min_max(arg) - if min > 0 { - min - } else if arg.required { - 1 - } else { - 0 - } -} - -///| -fn remaining_positional_min(positionals : Array[Arg], start : Int) -> Int { - let mut total = 0 - let mut idx = start - while idx < positionals.length() { - total = total + positional_min_required(positionals[idx]) - idx = idx + 1 - } - total -} - -///| -fn add_value( - matches : Matches, - name : String, - value : String, - arg : Arg, - source : ValueSource, -) -> Unit { - if arg.multiple || arg_action(arg) == ArgAction::Append { - let arr = matches.values.get(name).unwrap_or([]) - arr.push(value) - matches.values[name] = arr - let srcs = matches.value_sources.get(name).unwrap_or([]) - srcs.push(source) - matches.value_sources[name] = srcs - } else { - matches.values[name] = [value] - matches.value_sources[name] = [source] - } -} - -///| -fn assign_value( - matches : Matches, - arg : Arg, - value : String, - source : ValueSource, -) -> Unit raise ArgParseError { - match arg_action(arg) { - ArgAction::Append => add_value(matches, arg.name, value, arg, source) - ArgAction::Set => add_value(matches, arg.name, value, arg, source) - ArgAction::SetTrue => { - let flag = parse_bool(value) - matches.flags[arg.name] = flag - matches.flag_sources[arg.name] = source - } - ArgAction::SetFalse => { - let flag = parse_bool(value) - matches.flags[arg.name] = !flag - matches.flag_sources[arg.name] = source - } - ArgAction::Count => { - let count = parse_count(value) - matches.counts[arg.name] = count - matches.flags[arg.name] = count > 0 - matches.flag_sources[arg.name] = source - } - ArgAction::Help => - raise ArgParseError::InvalidArgument("help action does not take values") - ArgAction::Version => - raise ArgParseError::InvalidArgument( - "version action does not take values", - ) - } -} - -///| -fn option_occurrence_min(arg : Arg) -> Int { - match arg.num_args { - Some(_) => { - let (min, _) = arg_min_max(arg) - min - } - None => 1 - } -} - -///| -fn option_accepts_values(arg : Arg) -> Bool { - match arg.num_args { - Some(_) => { - let (_, max) = arg_min_max(arg) - match max { - Some(max_count) => max_count > 0 - None => true - } - } - None => true - } -} - -///| -fn option_conflict_label(arg : Arg) -> String { - match arg.long { - Some(name) => "--\{name}" - None => - match arg.short { - Some(short) => "-\{short}" - None => arg.name - } - } -} - -///| -fn check_duplicate_set_occurrence( - matches : Matches, - arg : Arg, -) -> Unit raise ArgParseError { - if arg_action(arg) != ArgAction::Set { - return - } - if matches.values.get(arg.name) is Some(_) { - raise ArgParseError::InvalidArgument( - "argument '\{option_conflict_label(arg)}' cannot be used multiple times", - ) - } -} - -///| -fn mark_option_present( - matches : Matches, - arg : Arg, - source : ValueSource, -) -> Unit { - if matches.values.get(arg.name) is None { - matches.values[arg.name] = [] - } - let srcs = matches.value_sources.get(arg.name).unwrap_or([]) - srcs.push(source) - matches.value_sources[arg.name] = srcs -} - -///| -fn required_option_value_count(arg : Arg) -> Int { - match arg.num_args { - None => 0 - Some(_) => { - let (_, max) = arg_min_max(arg) - match max { - Some(max_count) if max_count <= 1 => 0 - Some(max_count) => max_count - 1 - None => -1 - } - } - } -} - -///| -fn consume_additional_option_values( - matches : Matches, - arg : Arg, - argv : Array[String], - start : Int, - long_index : Map[String, Arg], - short_index : Map[Char, Arg], -) -> Int raise ArgParseError { - let max_more = required_option_value_count(arg) - if max_more == 0 { - return 0 - } - let mut consumed = 0 - while start + consumed < argv.length() { - if max_more > 0 && consumed >= max_more { - break - } - let value = argv[start + consumed] - if should_stop_option_value(value, arg, long_index, short_index) { - break - } - assign_value(matches, arg, value, ValueSource::Argv) - consumed = consumed + 1 - } - consumed -} - -///| -fn should_stop_option_value( - value : String, - arg : Arg, - _long_index : Map[String, Arg], - _short_index : Map[Char, Arg], -) -> Bool { - if !value.has_prefix("-") || value == "-" { - return false - } - if arg.allow_hyphen_values { - // Rust clap parity: - // - `clap_builder/src/parser/parser.rs`: `parse_long_arg` / `parse_short_arg` - // return `ParseResult::MaybeHyphenValue` when the pending arg in - // `ParseState::Opt` or `ParseState::Pos` has `allow_hyphen_values`. - // - `clap_builder/src/builder/arg.rs` (`Arg::allow_hyphen_values` docs): - // prior args with this setting take precedence over known flags/options. - // - `tests/builder/opts.rs` (`leading_hyphen_with_flag_after`): - // a pending option consumes `-f` as a value rather than parsing flag `-f`. - // This also means `--` is consumed as a value while the option remains pending. - return false - } - true -} - -///| -fn apply_env( - matches : Matches, - args : Array[Arg], - env : Map[String, String], -) -> Unit raise ArgParseError { - for arg in args { - let name = arg.name - if matches_has_value_or_flag(matches, name) { - continue - } - let env_name = match arg.env { - Some(value) => value - None => continue - } - let value = match env.get(env_name) { - Some(v) => v - None => continue - } - if arg_takes_value(arg) { - assign_value(matches, arg, value, ValueSource::Env) - continue - } - match arg_action(arg) { - ArgAction::Count => { - let count = parse_count(value) - matches.counts[name] = count - matches.flags[name] = count > 0 - matches.flag_sources[name] = ValueSource::Env - } - ArgAction::SetFalse => { - let flag = parse_bool(value) - matches.flags[name] = !flag - matches.flag_sources[name] = ValueSource::Env - } - ArgAction::SetTrue => { - let flag = parse_bool(value) - matches.flags[name] = flag - matches.flag_sources[name] = ValueSource::Env - } - ArgAction::Set => { - let flag = parse_bool(value) - matches.flags[name] = flag - matches.flag_sources[name] = ValueSource::Env - } - ArgAction::Append => () - ArgAction::Help => () - ArgAction::Version => () - } - } -} - -///| -fn apply_defaults(matches : Matches, args : Array[Arg]) -> Unit { - for arg in args { - if !arg_takes_value(arg) { - continue - } - if matches_has_value_or_flag(matches, arg.name) { - continue - } - match arg.default_values { - Some(values) if values.length() > 0 => - for value in values { - let _ = add_value(matches, arg.name, value, arg, ValueSource::Default) - } - _ => () - } - } -} - -///| -fn matches_has_value_or_flag(matches : Matches, name : String) -> Bool { - matches.flags.get(name) is Some(_) || matches.values.get(name) is Some(_) -} - -///| -fn collect_long_names(arg : Arg) -> Array[String] { - let names = [] - match arg.long { - Some(value) => { - names.push(value) - if arg.negatable && !arg_takes_value(arg) { - names.push("no-\{value}") - } - } - None => () - } - names -} - -///| -fn collect_short_names(arg : Arg) -> Array[Char] { - let names = [] - match arg.short { - Some(value) => names.push(value) - None => () - } - names -} - -///| -fn collect_subcommand_names(cmd : Command) -> Array[String] { - [cmd.name] -} - -///| -fn apply_flag(matches : Matches, arg : Arg, source : ValueSource) -> Unit { - match arg_action(arg) { - ArgAction::SetTrue => matches.flags[arg.name] = true - ArgAction::SetFalse => matches.flags[arg.name] = false - ArgAction::Count => { - let current = matches.counts.get(arg.name).unwrap_or(0) - matches.counts[arg.name] = current + 1 - matches.flags[arg.name] = true - } - ArgAction::Help => () - ArgAction::Version => () - _ => matches.flags[arg.name] = true - } - matches.flag_sources[arg.name] = source -} - -///| -fn parse_bool(value : String) -> Bool raise ArgParseError { - if value == "1" || value == "true" || value == "yes" || value == "on" { - true - } else if value == "0" || value == "false" || value == "no" || value == "off" { - false - } else { - raise ArgParseError::InvalidValue( - "invalid value '\{value}' for boolean flag; expected one of: 1, 0, true, false, yes, no, on, off", - ) - } -} - -///| -fn parse_count(value : String) -> Int raise ArgParseError { - try @strconv.parse_int(value) catch { - _ => - raise ArgParseError::InvalidValue( - "invalid value '\{value}' for count; expected a non-negative integer", - ) - } noraise { - _..<0 => - raise ArgParseError::InvalidValue( - "invalid value '\{value}' for count; expected a non-negative integer", - ) - v => v - } -} - -///| -fn suggest_long(name : String, long_index : Map[String, Arg]) -> String? { - let candidates = map_string_keys(long_index) - match suggest_name(name, candidates) { - Some(best) => Some("--\{best}") - None => None - } -} - -///| -fn suggest_short(short : Char, short_index : Map[Char, Arg]) -> String? { - let candidates = map_char_keys(short_index) - let input = short.to_string() - match suggest_name(input, candidates) { - Some(best) => Some("-\{best}") - None => None - } -} - -///| -fn map_string_keys(map : Map[String, Arg]) -> Array[String] { - let keys = [] - for key, _ in map { - keys.push(key) - } - keys -} - -///| -fn map_char_keys(map : Map[Char, Arg]) -> Array[String] { - let keys = [] - for key, _ in map { - keys.push(key.to_string()) - } - keys -} - -///| -fn suggest_name(input : String, candidates : Array[String]) -> String? { - let mut best : String? = None - let mut best_dist = 0 - let mut has_best = false - let max_dist = suggestion_threshold(input.length()) - for cand in candidates { - let dist = levenshtein(input, cand) - if !has_best || dist < best_dist { - best_dist = dist - best = Some(cand) - has_best = true - } - } - match best { - Some(name) if best_dist <= max_dist => Some(name) - _ => None - } -} - -///| -fn suggestion_threshold(len : Int) -> Int { - if len <= 4 { - 1 - } else if len <= 8 { - 2 - } else { - 3 - } -} - -///| -fn levenshtein(a : String, b : String) -> Int { - let aa = string_chars(a) - let bb = string_chars(b) - let m = aa.length() - let n = bb.length() - if m == 0 { - return n - } - if n == 0 { - return m - } - let mut prev = Array::new(capacity=n + 1) - let mut curr = Array::new(capacity=n + 1) - let mut j = 0 - while j <= n { - prev.push(j) - curr.push(0) - j = j + 1 - } - let mut i = 1 - while i <= m { - curr[0] = i - let mut j2 = 1 - while j2 <= n { - let cost = if aa[i - 1] == bb[j2 - 1] { 0 } else { 1 } - let del = prev[j2] + 1 - let ins = curr[j2 - 1] + 1 - let sub = prev[j2 - 1] + cost - curr[j2] = min3(del, ins, sub) - j2 = j2 + 1 - } - let temp = prev - prev = curr - curr = temp - i = i + 1 - } - prev[n] -} - -///| -fn string_chars(s : String) -> Array[Char] { - let out = [] - for ch in s { - out.push(ch) - } - out -} - -///| -fn min3(a : Int, b : Int, c : Int) -> Int { - let m = if a < b { a } else { b } - if c < m { - c - } else { - m - } -} - -///| -fn build_long_index( - globals : Array[Arg], - args : Array[Arg], -) -> Map[String, Arg] { - let index : Map[String, Arg] = {} - for arg in globals { - if arg.long is Some(name) { - index[name] = arg - } - } - for arg in args { - if arg.long is Some(name) { - index[name] = arg - } - } - index -} - -///| -fn build_short_index(globals : Array[Arg], args : Array[Arg]) -> Map[Char, Arg] { - let index : Map[Char, Arg] = {} - for arg in globals { - if arg.short is Some(value) { - index[value] = arg - } - } - for arg in args { - if arg.short is Some(value) { - index[value] = arg - } - } - index -} - -///| -fn collect_globals(args : Array[Arg]) -> Array[Arg] { - let out = [] - for arg in args { - if arg.global && (arg.long is Some(_) || arg.short is Some(_)) { - out.push(arg) - } - } - out -} - -///| -fn collect_non_global_names(args : Array[Arg]) -> Map[String, Bool] { - let names : Map[String, Bool] = {} - for arg in args { - if !arg.global { - names[arg.name] = true - } - } - names -} - -///| -fn source_priority(source : ValueSource?) -> Int { - match source { - Some(ValueSource::Argv) => 3 - Some(ValueSource::Env) => 2 - Some(ValueSource::Default) => 1 - None => 0 - } -} - -///| -fn prefer_child_source( - parent_source : ValueSource?, - child_source : ValueSource?, -) -> Bool { - let parent_priority = source_priority(parent_source) - let child_priority = source_priority(child_source) - if child_priority > parent_priority { - true - } else if child_priority < parent_priority { - false - } else { - child_source is Some(ValueSource::Argv) - } -} - -///| -fn strongest_source( - parent_source : ValueSource?, - child_source : ValueSource?, -) -> ValueSource? { - if prefer_child_source(parent_source, child_source) { - child_source - } else { - match parent_source { - Some(source) => Some(source) - None => child_source - } - } -} - -///| -fn source_from_values(sources : Array[ValueSource]?) -> ValueSource? { - match sources { - Some(items) if items.length() > 0 => highest_source(items) - _ => None - } -} - -///| -fn merge_globals_from_child( - parent : Matches, - child : Matches, - globals : Array[Arg], - child_local_non_globals : Map[String, Bool], -) -> Unit { - for arg in globals { - let name = arg.name - if child_local_non_globals.get(name) is Some(_) { - continue - } - if arg_takes_value(arg) { - let parent_vals = parent.values.get(name) - let child_vals = child.values.get(name) - let parent_srcs = parent.value_sources.get(name) - let child_srcs = child.value_sources.get(name) - let has_parent = parent_vals is Some(pv) && pv.length() > 0 - let has_child = child_vals is Some(cv) && cv.length() > 0 - if !has_parent && !has_child { - continue - } - let parent_source = source_from_values(parent_srcs) - let child_source = source_from_values(child_srcs) - if arg.multiple || arg_action(arg) == ArgAction::Append { - let both_argv = parent_source is Some(ValueSource::Argv) && - child_source is Some(ValueSource::Argv) - if both_argv { - let merged = [] - let merged_srcs = [] - if parent_vals is Some(pv) { - for v in pv { - merged.push(v) - } - } - if parent_srcs is Some(ps) { - for s in ps { - merged_srcs.push(s) - } - } - if child_vals is Some(cv) { - for v in cv { - merged.push(v) - } - } - if child_srcs is Some(cs) { - for s in cs { - merged_srcs.push(s) - } - } - if merged.length() > 0 { - parent.values[name] = merged - parent.value_sources[name] = merged_srcs - } - } else { - let choose_child = has_child && - (!has_parent || prefer_child_source(parent_source, child_source)) - if choose_child { - if child_vals is Some(cv) && cv.length() > 0 { - parent.values[name] = cv.copy() - } - if child_srcs is Some(cs) && cs.length() > 0 { - parent.value_sources[name] = cs.copy() - } - } else if parent_vals is Some(pv) && pv.length() > 0 { - parent.values[name] = pv.copy() - if parent_srcs is Some(ps) && ps.length() > 0 { - parent.value_sources[name] = ps.copy() - } - } - } - } else { - let choose_child = has_child && - (!has_parent || prefer_child_source(parent_source, child_source)) - if choose_child { - if child_vals is Some(cv) && cv.length() > 0 { - parent.values[name] = cv.copy() - } - if child_srcs is Some(cs) && cs.length() > 0 { - parent.value_sources[name] = cs.copy() - } - } else if parent_vals is Some(pv) && pv.length() > 0 { - parent.values[name] = pv.copy() - if parent_srcs is Some(ps) && ps.length() > 0 { - parent.value_sources[name] = ps.copy() - } - } - } - } else { - match child.flags.get(name) { - Some(v) => - if arg_action(arg) == ArgAction::Count { - let has_parent = parent.flags.get(name) is Some(_) - let parent_source = parent.flag_sources.get(name) - let child_source = child.flag_sources.get(name) - let both_argv = parent_source is Some(ValueSource::Argv) && - child_source is Some(ValueSource::Argv) - if both_argv { - let parent_count = parent.counts.get(name).unwrap_or(0) - let child_count = child.counts.get(name).unwrap_or(0) - let total = parent_count + child_count - parent.counts[name] = total - parent.flags[name] = total > 0 - match strongest_source(parent_source, child_source) { - Some(src) => parent.flag_sources[name] = src - None => () - } - } else { - let choose_child = !has_parent || - prefer_child_source(parent_source, child_source) - if choose_child { - let child_count = child.counts.get(name).unwrap_or(0) - parent.counts[name] = child_count - parent.flags[name] = child_count > 0 - match child_source { - Some(src) => parent.flag_sources[name] = src - None => () - } - } - } - } else { - let has_parent = parent.flags.get(name) is Some(_) - let parent_source = parent.flag_sources.get(name) - let child_source = child.flag_sources.get(name) - let choose_child = !has_parent || - prefer_child_source(parent_source, child_source) - if choose_child { - parent.flags[name] = v - match child_source { - Some(src) => parent.flag_sources[name] = src - None => () - } - } - } - None => () - } - } - } -} - -///| -fn propagate_globals_to_child( - parent : Matches, - child : Matches, - globals : Array[Arg], - child_local_non_globals : Map[String, Bool], -) -> Unit { - for arg in globals { - let name = arg.name - if child_local_non_globals.get(name) is Some(_) { - continue - } - if arg_takes_value(arg) { - match parent.values.get(name) { - Some(values) => { - child.values[name] = values.copy() - match parent.value_sources.get(name) { - Some(srcs) => child.value_sources[name] = srcs.copy() - None => () - } - } - None => () - } - } else { - match parent.flags.get(name) { - Some(v) => { - child.flags[name] = v - match parent.flag_sources.get(name) { - Some(src) => child.flag_sources[name] = src - None => () - } - if arg_action(arg) == ArgAction::Count { - match parent.counts.get(name) { - Some(c) => child.counts[name] = c - None => () - } - } - } - None => () - } - } - } -} - -///| -fn positional_args(args : Array[Arg]) -> Array[Arg] { - let with_index = [] - let without_index = [] - for arg in args { - if is_positional_arg(arg) { - if arg.index is Some(idx) { - with_index.push((idx, arg)) - } else { - without_index.push(arg) - } - } - } - sort_positionals(with_index) - let ordered = [] - for item in with_index { - let (_, arg) = item - ordered.push(arg) - } - for arg in without_index { - ordered.push(arg) - } - ordered -} - -///| -fn last_positional_index(positionals : Array[Arg]) -> Int? { - let mut i = 0 - while i < positionals.length() { - if positionals[i].last { - return Some(i) - } - i = i + 1 - } - None -} - -///| -fn next_positional(positionals : Array[Arg], collected : Array[String]) -> Arg? { - let target = collected.length() - let total = target + 1 - let mut cursor = 0 - for idx in 0..= total { - break - } - let arg = positionals[idx] - let remaining = total - cursor - let take = if arg.multiple { - let (min, max) = arg_min_max(arg) - let reserve = remaining_positional_min(positionals, idx + 1) - let mut take = remaining - reserve - if take < 0 { - take = 0 - } - match max { - Some(max_count) if take > max_count => take = max_count - _ => () - } - if take < min { - take = min - } - if take > remaining { - take = remaining - } - take - } else if remaining > 0 { - 1 - } else { - 0 - } - if take > 0 && target < cursor + take { - return Some(arg) - } - cursor = cursor + take - } - None -} - -///| -fn should_parse_as_positional( - arg : String, - positionals : Array[Arg], - collected : Array[String], - long_index : Map[String, Arg], - short_index : Map[Char, Arg], -) -> Bool { - if !arg.has_prefix("-") || arg == "-" { - return false - } - let next = match next_positional(positionals, collected) { - Some(v) => v - None => return false - } - let allow = next.allow_hyphen_values || is_negative_number(arg) - if !allow { - return false - } - if arg.has_prefix("--") { - let (name, _) = split_long(arg) - return long_index.get(name) is None - } - let short = arg.get_char(1) - match short { - Some(ch) => short_index.get(ch) is None - None => true - } -} - -///| -fn is_negative_number(arg : String) -> Bool { - if arg.length() < 2 { - return false - } - guard arg.get_char(0) is Some('-') else { return false } - let mut i = 1 - while i < arg.length() { - let ch = arg.get_char(i).unwrap() - if ch < '0' || ch > '9' { - return false - } - i = i + 1 - } - true -} - -///| -fn sort_positionals(items : Array[(Int, Arg)]) -> Unit { - let mut i = 1 - while i < items.length() { - let key = items[i] - let mut j = i - 1 - while j >= 0 && items[j].0 > key.0 { - items[j + 1] = items[j] - if j == 0 { - j = -1 - } else { - j = j - 1 - } - } - items[j + 1] = key - i = i + 1 - } -} - -///| -fn find_subcommand(subs : Array[Command], name : String) -> Command? { - for sub in subs { - if sub.name == name { - return Some(sub) - } - } - None -} - -///| -fn resolve_help_target( - cmd : Command, - argv : Array[String], - builtin_help_short : Bool, - builtin_help_long : Bool, - inherited_globals : Array[Arg], -) -> (Command, Array[Arg]) raise { - let targets = if argv.length() == 0 { - argv - } else { - let last = argv[argv.length() - 1] - if (last == "-h" && builtin_help_short) || - (last == "--help" && builtin_help_long) { - argv[:argv.length() - 1].to_array() - } else { - argv - } - } - let mut current = cmd - let mut current_globals = inherited_globals - let mut subs = cmd.subcommands - for name in targets { - if name.has_prefix("-") { - raise ArgParseError::InvalidArgument("unexpected help argument: \{name}") - } - match find_subcommand(subs, name) { - Some(sub) => { - current_globals = current_globals + collect_globals(current.args) - current = sub - subs = sub.subcommands - } - None => - raise ArgParseError::InvalidArgument("unknown subcommand: \{name}") - } - } - (current, current_globals) -} - -///| -fn split_long(arg : String) -> (String, String?) { - let parts = [] - for part in arg.split("=") { - parts.push(part.to_string()) - } - if parts.length() <= 1 { - let name = match parts[0].strip_prefix("--") { - Some(view) => view.to_string() - None => parts[0] - } - (name, None) - } else { - let name = match parts[0].strip_prefix("--") { - Some(view) => view.to_string() - None => parts[0] + if arg.short is Some(value) && + value == short && + arg_action(arg) == ArgAction::Version { + return command_version(cmd) } - let value = parts[1:].to_array().join("=") - (name, Some(value)) } + inherited_version_short.get(short).unwrap_or(command_version(cmd)) } diff --git a/argparse/parser_globals_merge.mbt b/argparse/parser_globals_merge.mbt new file mode 100644 index 000000000..a65710c8d --- /dev/null +++ b/argparse/parser_globals_merge.mbt @@ -0,0 +1,266 @@ +// 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. + +///| +fn source_priority(source : ValueSource?) -> Int { + match source { + Some(ValueSource::Argv) => 3 + Some(ValueSource::Env) => 2 + Some(ValueSource::Default) => 1 + None => 0 + } +} + +///| +fn prefer_child_source( + parent_source : ValueSource?, + child_source : ValueSource?, +) -> Bool { + let parent_priority = source_priority(parent_source) + let child_priority = source_priority(child_source) + if child_priority > parent_priority { + true + } else if child_priority < parent_priority { + false + } else { + child_source is Some(ValueSource::Argv) + } +} + +///| +fn strongest_source( + parent_source : ValueSource?, + child_source : ValueSource?, +) -> ValueSource? { + if prefer_child_source(parent_source, child_source) { + child_source + } else { + match parent_source { + Some(source) => Some(source) + None => child_source + } + } +} + +///| +fn source_from_values(sources : Array[ValueSource]?) -> ValueSource? { + match sources { + Some(items) if items.length() > 0 => highest_source(items) + _ => None + } +} + +///| +fn merge_global_value_from_child( + parent : Matches, + child : Matches, + arg : Arg, + name : String, +) -> Unit { + let parent_vals = parent.values.get(name) + let child_vals = child.values.get(name) + let parent_srcs = parent.value_sources.get(name) + let child_srcs = child.value_sources.get(name) + let has_parent = parent_vals is Some(pv) && pv.length() > 0 + let has_child = child_vals is Some(cv) && cv.length() > 0 + if !has_parent && !has_child { + return + } + let parent_source = source_from_values(parent_srcs) + let child_source = source_from_values(child_srcs) + if arg.multiple || arg_action(arg) == ArgAction::Append { + let both_argv = parent_source is Some(ValueSource::Argv) && + child_source is Some(ValueSource::Argv) + if both_argv { + let merged = [] + let merged_srcs = [] + if parent_vals is Some(pv) { + for v in pv { + merged.push(v) + } + } + if parent_srcs is Some(ps) { + for s in ps { + merged_srcs.push(s) + } + } + if child_vals is Some(cv) { + for v in cv { + merged.push(v) + } + } + if child_srcs is Some(cs) { + for s in cs { + merged_srcs.push(s) + } + } + if merged.length() > 0 { + parent.values[name] = merged + parent.value_sources[name] = merged_srcs + } + } else { + let choose_child = has_child && + (!has_parent || prefer_child_source(parent_source, child_source)) + if choose_child { + if child_vals is Some(cv) && cv.length() > 0 { + parent.values[name] = cv.copy() + } + if child_srcs is Some(cs) && cs.length() > 0 { + parent.value_sources[name] = cs.copy() + } + } else if parent_vals is Some(pv) && pv.length() > 0 { + parent.values[name] = pv.copy() + if parent_srcs is Some(ps) && ps.length() > 0 { + parent.value_sources[name] = ps.copy() + } + } + } + } else { + let choose_child = has_child && + (!has_parent || prefer_child_source(parent_source, child_source)) + if choose_child { + if child_vals is Some(cv) && cv.length() > 0 { + parent.values[name] = cv.copy() + } + if child_srcs is Some(cs) && cs.length() > 0 { + parent.value_sources[name] = cs.copy() + } + } else if parent_vals is Some(pv) && pv.length() > 0 { + parent.values[name] = pv.copy() + if parent_srcs is Some(ps) && ps.length() > 0 { + parent.value_sources[name] = ps.copy() + } + } + } +} + +///| +fn merge_global_flag_from_child( + parent : Matches, + child : Matches, + arg : Arg, + name : String, +) -> Unit { + match child.flags.get(name) { + Some(v) => + if arg_action(arg) == ArgAction::Count { + let has_parent = parent.flags.get(name) is Some(_) + let parent_source = parent.flag_sources.get(name) + let child_source = child.flag_sources.get(name) + let both_argv = parent_source is Some(ValueSource::Argv) && + child_source is Some(ValueSource::Argv) + if both_argv { + let parent_count = parent.counts.get(name).unwrap_or(0) + let child_count = child.counts.get(name).unwrap_or(0) + let total = parent_count + child_count + parent.counts[name] = total + parent.flags[name] = total > 0 + match strongest_source(parent_source, child_source) { + Some(src) => parent.flag_sources[name] = src + None => () + } + } else { + let choose_child = !has_parent || + prefer_child_source(parent_source, child_source) + if choose_child { + let child_count = child.counts.get(name).unwrap_or(0) + parent.counts[name] = child_count + parent.flags[name] = child_count > 0 + match child_source { + Some(src) => parent.flag_sources[name] = src + None => () + } + } + } + } else { + let has_parent = parent.flags.get(name) is Some(_) + let parent_source = parent.flag_sources.get(name) + let child_source = child.flag_sources.get(name) + let choose_child = !has_parent || + prefer_child_source(parent_source, child_source) + if choose_child { + parent.flags[name] = v + match child_source { + Some(src) => parent.flag_sources[name] = src + None => () + } + } + } + None => () + } +} + +///| +fn merge_globals_from_child( + parent : Matches, + child : Matches, + globals : Array[Arg], + child_local_non_globals : @set.Set[String], +) -> Unit { + for arg in globals { + let name = arg.name + if child_local_non_globals.contains(name) { + continue + } + if arg_takes_value(arg) { + merge_global_value_from_child(parent, child, arg, name) + } else { + merge_global_flag_from_child(parent, child, arg, name) + } + } +} + +///| +fn propagate_globals_to_child( + parent : Matches, + child : Matches, + globals : Array[Arg], + child_local_non_globals : @set.Set[String], +) -> Unit { + for arg in globals { + let name = arg.name + if child_local_non_globals.contains(name) { + continue + } + if arg_takes_value(arg) { + match parent.values.get(name) { + Some(values) => { + child.values[name] = values.copy() + match parent.value_sources.get(name) { + Some(srcs) => child.value_sources[name] = srcs.copy() + None => () + } + } + None => () + } + } else { + match parent.flags.get(name) { + Some(v) => { + child.flags[name] = v + match parent.flag_sources.get(name) { + Some(src) => child.flag_sources[name] = src + None => () + } + if arg_action(arg) == ArgAction::Count { + match parent.counts.get(name) { + Some(c) => child.counts[name] = c + None => () + } + } + } + None => () + } + } + } +} diff --git a/argparse/parser_lookup.mbt b/argparse/parser_lookup.mbt new file mode 100644 index 000000000..256998815 --- /dev/null +++ b/argparse/parser_lookup.mbt @@ -0,0 +1,116 @@ +// 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. + +///| +fn build_long_index( + globals : Array[Arg], + args : Array[Arg], +) -> Map[String, Arg] { + let index : Map[String, Arg] = {} + for arg in globals { + if arg.long is Some(name) { + index[name] = arg + } + } + for arg in args { + if arg.long is Some(name) { + index[name] = arg + } + } + index +} + +///| +fn build_short_index(globals : Array[Arg], args : Array[Arg]) -> Map[Char, Arg] { + let index : Map[Char, Arg] = {} + for arg in globals { + if arg.short is Some(value) { + index[value] = arg + } + } + for arg in args { + if arg.short is Some(value) { + index[value] = arg + } + } + index +} + +///| +fn collect_globals(args : Array[Arg]) -> Array[Arg] { + args.filter(arg => arg.global && (arg.long is Some(_) || arg.short is Some(_))) +} + +///| +fn collect_non_global_names(args : Array[Arg]) -> @set.Set[String] { + @set.from_iter(args.iter().filter(arg => !arg.global).map(arg => arg.name)) +} + +///| +fn resolve_help_target( + cmd : Command, + argv : Array[String], + builtin_help_short : Bool, + builtin_help_long : Bool, + inherited_globals : Array[Arg], +) -> (Command, Array[Arg]) raise ArgParseError { + let targets = if argv.length() == 0 { + argv + } else { + let last = argv[argv.length() - 1] + if (last == "-h" && builtin_help_short) || + (last == "--help" && builtin_help_long) { + argv[:argv.length() - 1].to_array() + } else { + argv + } + } + let mut current = cmd + let mut current_globals = inherited_globals + let mut subs = cmd.subcommands + for name in targets { + if name.has_prefix("-") { + raise ArgParseError::InvalidArgument("unexpected help argument: \{name}") + } + guard subs.iter().find_first(sub => sub.name == name) is Some(sub) else { + raise ArgParseError::InvalidArgument("unknown subcommand: \{name}") + } + current_globals = current_globals + collect_globals(current.args) + current = sub + subs = sub.subcommands + } + (current, current_globals) +} + +///| +fn split_long(arg : String) -> (String, String?) { + let parts = [] + for part in arg.split("=") { + parts.push(part.to_string()) + } + if parts.length() <= 1 { + let name = match parts[0].strip_prefix("--") { + Some(view) => view.to_string() + None => parts[0] + } + (name, None) + } else { + let name = match parts[0].strip_prefix("--") { + Some(view) => view.to_string() + None => parts[0] + } + let value = parts[1:].to_array().join("=") + (name, Some(value)) + } +} diff --git a/argparse/parser_positionals.mbt b/argparse/parser_positionals.mbt new file mode 100644 index 000000000..1559ab3a1 --- /dev/null +++ b/argparse/parser_positionals.mbt @@ -0,0 +1,158 @@ +// 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. + +///| +fn positional_args(args : Array[Arg]) -> Array[Arg] { + let with_index = [] + let without_index = [] + for arg in args { + if is_positional_arg(arg) { + if arg.index is Some(idx) { + with_index.push((idx, arg)) + } else { + without_index.push(arg) + } + } + } + sort_positionals(with_index) + let ordered = [] + for item in with_index { + let (_, arg) = item + ordered.push(arg) + } + for arg in without_index { + ordered.push(arg) + } + ordered +} + +///| +fn last_positional_index(positionals : Array[Arg]) -> Int? { + let mut i = 0 + while i < positionals.length() { + if positionals[i].last { + return Some(i) + } + i = i + 1 + } + None +} + +///| +fn next_positional(positionals : Array[Arg], collected : Array[String]) -> Arg? { + let target = collected.length() + let total = target + 1 + let mut cursor = 0 + for idx in 0..= total { + break + } + let arg = positionals[idx] + let remaining = total - cursor + let take = if arg.multiple { + let (min, max) = arg_min_max(arg) + let reserve = remaining_positional_min(positionals, idx + 1) + let mut take = remaining - reserve + if take < 0 { + take = 0 + } + match max { + Some(max_count) if take > max_count => take = max_count + _ => () + } + if take < min { + take = min + } + if take > remaining { + take = remaining + } + take + } else if remaining > 0 { + 1 + } else { + 0 + } + if take > 0 && target < cursor + take { + return Some(arg) + } + cursor = cursor + take + } + None +} + +///| +fn should_parse_as_positional( + arg : String, + positionals : Array[Arg], + collected : Array[String], + long_index : Map[String, Arg], + short_index : Map[Char, Arg], +) -> Bool { + if !arg.has_prefix("-") || arg == "-" { + return false + } + let next = match next_positional(positionals, collected) { + Some(v) => v + None => return false + } + let allow = next.allow_hyphen_values || is_negative_number(arg) + if !allow { + return false + } + if arg.has_prefix("--") { + let (name, _) = split_long(arg) + return long_index.get(name) is None + } + let short = arg.get_char(1) + match short { + Some(ch) => short_index.get(ch) is None + None => true + } +} + +///| +fn is_negative_number(arg : String) -> Bool { + if arg.length() < 2 { + return false + } + guard arg.get_char(0) is Some('-') else { return false } + let mut i = 1 + while i < arg.length() { + let ch = arg.get_char(i).unwrap() + if ch < '0' || ch > '9' { + return false + } + i = i + 1 + } + true +} + +///| +fn sort_positionals(items : Array[(Int, Arg)]) -> Unit { + let mut i = 1 + while i < items.length() { + let key = items[i] + let mut j = i - 1 + while j >= 0 && items[j].0 > key.0 { + items[j + 1] = items[j] + if j == 0 { + j = -1 + } else { + j = j - 1 + } + } + items[j + 1] = key + i = i + 1 + } +} diff --git a/argparse/parser_suggest.mbt b/argparse/parser_suggest.mbt new file mode 100644 index 000000000..44bcdcf05 --- /dev/null +++ b/argparse/parser_suggest.mbt @@ -0,0 +1,115 @@ +// 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. + +///| +fn suggest_long(name : String, long_index : Map[String, Arg]) -> String? { + let candidates = long_index.keys().collect() + if suggest_name(name, candidates) is Some(best) { + Some("--\{best}") + } else { + None + } +} + +///| +fn suggest_short(short : Char, short_index : Map[Char, Arg]) -> String? { + let candidates = short_index.keys().map(Char::to_string).collect() + let input = short.to_string() + if suggest_name(input, candidates) is Some(best) { + Some("-\{best}") + } else { + None + } +} + +///| +fn suggest_name(input : String, candidates : Array[String]) -> String? { + let mut best : String? = None + let mut best_dist = 0 + let mut has_best = false + let max_dist = suggestion_threshold(input.length()) + for cand in candidates { + let dist = levenshtein(input, cand) + if !has_best || dist < best_dist { + best_dist = dist + best = Some(cand) + has_best = true + } + } + match best { + Some(name) if best_dist <= max_dist => Some(name) + _ => None + } +} + +///| +fn suggestion_threshold(len : Int) -> Int { + if len <= 4 { + 1 + } else if len <= 8 { + 2 + } else { + 3 + } +} + +///| +fn levenshtein(a : String, b : String) -> Int { + let aa = a.to_array() + let bb = b.to_array() + let m = aa.length() + let n = bb.length() + if m == 0 { + return n + } + if n == 0 { + return m + } + let mut prev = Array::new(capacity=n + 1) + let mut curr = Array::new(capacity=n + 1) + let mut j = 0 + while j <= n { + prev.push(j) + curr.push(0) + j = j + 1 + } + let mut i = 1 + while i <= m { + curr[0] = i + let mut j2 = 1 + while j2 <= n { + let cost = if aa[i - 1] == bb[j2 - 1] { 0 } else { 1 } + let del = prev[j2] + 1 + let ins = curr[j2 - 1] + 1 + let sub = prev[j2 - 1] + cost + curr[j2] = if del < ins { + if del < sub { + del + } else { + sub + } + } else if ins < sub { + ins + } else { + sub + } + j2 = j2 + 1 + } + let temp = prev + prev = curr + curr = temp + i = i + 1 + } + prev[n] +} diff --git a/argparse/parser_validate.mbt b/argparse/parser_validate.mbt new file mode 100644 index 000000000..81943a99c --- /dev/null +++ b/argparse/parser_validate.mbt @@ -0,0 +1,518 @@ +// 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. + +///| +priv struct ValidationCtx { + inherited_global_names : @set.Set[String] + seen_names : @set.Set[String] + seen_long : @set.Set[String] + seen_short : @set.Set[Char] + seen_positional_indices : @set.Set[Int] + args : Array[Arg] +} + +///| +fn ValidationCtx::new( + inherited_global_names? : @set.Set[String] = @set.new(), +) -> ValidationCtx { + ValidationCtx::{ + inherited_global_names: inherited_global_names.copy(), + seen_names: @set.new(), + seen_long: @set.new(), + seen_short: @set.new(), + seen_positional_indices: @set.new(), + args: [], + } +} + +///| +fn ValidationCtx::record_arg( + self : ValidationCtx, + arg : Arg, +) -> Unit raise ArgBuildError { + if !self.seen_names.add_and_check(arg.name) { + raise ArgBuildError::Unsupported("duplicate arg name: \{arg.name}") + } + if !arg.global && self.inherited_global_names.contains(arg.name) { + raise ArgBuildError::Unsupported( + "arg '\{arg.name}' shadows an inherited global; rename the arg or mark it global", + ) + } + if arg.long is Some(name) { + if !self.seen_long.add_and_check(name) { + raise ArgBuildError::Unsupported("duplicate long option: --\{name}") + } + if arg.negatable && !self.seen_long.add_and_check("no-\{name}") { + raise ArgBuildError::Unsupported("duplicate long option: --no-\{name}") + } + } + if arg.short is Some(short) && !self.seen_short.add_and_check(short) { + raise ArgBuildError::Unsupported("duplicate short option: -\{short}") + } + if arg.is_positional && + arg.index is Some(index) && + !self.seen_positional_indices.add_and_check(index) { + raise ArgBuildError::Unsupported("duplicate positional index: \{index}") + } + self.args.push(arg) +} + +///| +fn ValidationCtx::finalize(self : ValidationCtx) -> Unit raise ArgBuildError { + validate_requires_conflicts_targets(self.args, self.seen_names) + validate_indexed_positional_num_args(self.args) +} + +///| +fn validate_command( + cmd : Command, + args : Array[Arg], + groups : Array[ArgGroup], + inherited_global_names : @set.Set[String], +) -> Unit raise ArgBuildError { + match cmd.build_error { + Some(err) => raise err + None => () + } + validate_inherited_global_shadowing(args, inherited_global_names) + validate_group_defs(args, groups) + validate_group_refs(args, groups) + validate_subcommand_defs(cmd.subcommands) + validate_subcommand_required_policy(cmd) + validate_help_subcommand(cmd) + validate_version_actions(cmd) + let child_inherited_global_names = inherited_global_names.copy() + for global in collect_globals(args) { + child_inherited_global_names.add(global.name) + } + for sub in cmd.subcommands { + validate_command(sub, sub.args, sub.groups, child_inherited_global_names) + } +} + +///| +fn validate_inherited_global_shadowing( + args : Array[Arg], + inherited_global_names : @set.Set[String], +) -> Unit raise ArgBuildError { + for arg in args { + if arg.global { + continue + } + if inherited_global_names.contains(arg.name) { + raise ArgBuildError::Unsupported( + "arg '\{arg.name}' shadows an inherited global; rename the arg or mark it global", + ) + } + } +} + +///| +fn validate_indexed_positional_num_args( + args : Array[Arg], +) -> Unit raise ArgBuildError { + let positionals = positional_args(args) + if positionals.length() <= 1 { + return + } + let mut idx = 0 + while idx + 1 < positionals.length() { + let arg = positionals[idx] + if arg.index is Some(_) { + match arg.num_args { + Some(range) => + if !(range.lower == 1 && range.upper is Some(1)) { + raise ArgBuildError::Unsupported( + "indexed positional '\{arg.name}' cannot set num_args unless it is the last positional or exactly 1..1", + ) + } + None => () + } + } + idx = idx + 1 + } +} + +///| +fn validate_flag_arg( + arg : Arg, + ctx : ValidationCtx, +) -> Unit raise ArgBuildError { + validate_named_option_arg(arg) + guard arg.index is None && + !arg.last && + arg.num_args is None && + arg.default_values is None + if arg.flag_action is (Help | Version) { + guard !arg.negatable else { + raise ArgBuildError::Unsupported( + "help/version actions do not support negatable", + ) + } + guard arg.env is None else { + raise ArgBuildError::Unsupported( + "help/version actions do not support env/defaults", + ) + } + guard !arg.multiple else { + raise ArgBuildError::Unsupported( + "help/version actions do not support multiple values", + ) + } + } + ctx.record_arg(arg) +} + +///| +fn validate_option_arg( + arg : Arg, + ctx : ValidationCtx, +) -> Unit raise ArgBuildError { + validate_named_option_arg(arg) + guard arg.index is None && !arg.last && !arg.negatable && arg.num_args is None + validate_default_values(arg) + ctx.record_arg(arg) +} + +///| +fn validate_positional_arg( + arg : Arg, + ctx : ValidationCtx, +) -> Unit raise ArgBuildError { + guard arg.long is None && arg.short is None && !arg.negatable + let (min, max) = arg_min_max_for_validate(arg) + if (min > 1 || (max is Some(m) && m > 1)) && !arg.multiple { + raise ArgBuildError::Unsupported( + "multiple values require action=Append or num_args allowing >1", + ) + } + validate_default_values(arg) + ctx.record_arg(arg) +} + +///| +fn validate_named_option_arg(arg : Arg) -> Unit raise ArgBuildError { + guard arg.long is Some(_) || arg.short is Some(_) else { + raise ArgBuildError::Unsupported("flag/option args require short/long") + } +} + +///| +fn validate_default_values(arg : Arg) -> Unit raise ArgBuildError { + if arg.default_values is Some(values) && + values.length() > 1 && + !arg.multiple && + arg_action(arg) != ArgAction::Append { + raise ArgBuildError::Unsupported( + "default_values with multiple entries require action=Append", + ) + } +} + +///| +fn validate_group_defs( + args : Array[Arg], + groups : Array[ArgGroup], +) -> Unit raise ArgBuildError { + let seen : @set.Set[String] = @set.new() + let arg_seen : @set.Set[String] = @set.new() + for arg in args { + arg_seen.add(arg.name) + } + for group in groups { + if !seen.add_and_check(group.name) { + raise ArgBuildError::Unsupported("duplicate group: \{group.name}") + } + } + for group in groups { + for required in group.requires { + if required == group.name { + raise ArgBuildError::Unsupported( + "group cannot require itself: \{group.name}", + ) + } + if !seen.contains(required) && !arg_seen.contains(required) { + raise ArgBuildError::Unsupported( + "unknown group requires target: \{group.name} -> \{required}", + ) + } + } + for conflict in group.conflicts_with { + if conflict == group.name { + raise ArgBuildError::Unsupported( + "group cannot conflict with itself: \{group.name}", + ) + } + if !seen.contains(conflict) && !arg_seen.contains(conflict) { + raise ArgBuildError::Unsupported( + "unknown group conflicts_with target: \{group.name} -> \{conflict}", + ) + } + } + } +} + +///| +fn validate_group_refs( + args : Array[Arg], + groups : Array[ArgGroup], +) -> Unit raise ArgBuildError { + if groups.length() == 0 { + return + } + let arg_index : @set.Set[String] = @set.new() + for arg in args { + arg_index.add(arg.name) + } + for group in groups { + for name in group.args { + if !arg_index.contains(name) { + raise ArgBuildError::Unsupported( + "unknown group arg: \{group.name} -> \{name}", + ) + } + } + } +} + +///| +fn validate_requires_conflicts_targets( + args : Array[Arg], + seen_names : @set.Set[String], +) -> Unit raise ArgBuildError { + for arg in args { + for required in arg.requires { + if required == arg.name { + raise ArgBuildError::Unsupported( + "arg cannot require itself: \{arg.name}", + ) + } + if !seen_names.contains(required) { + raise ArgBuildError::Unsupported( + "unknown requires target: \{arg.name} -> \{required}", + ) + } + } + for conflict in arg.conflicts_with { + if conflict == arg.name { + raise ArgBuildError::Unsupported( + "arg cannot conflict with itself: \{arg.name}", + ) + } + if !seen_names.contains(conflict) { + raise ArgBuildError::Unsupported( + "unknown conflicts_with target: \{arg.name} -> \{conflict}", + ) + } + } + } +} + +///| +fn validate_subcommand_defs(subs : Array[Command]) -> Unit raise ArgBuildError { + if subs.length() == 0 { + return + } + let seen : @set.Set[String] = @set.new() + for sub in subs { + if !seen.add_and_check(sub.name) { + raise ArgBuildError::Unsupported("duplicate subcommand: \{sub.name}") + } + } +} + +///| +fn validate_subcommand_required_policy( + cmd : Command, +) -> Unit raise ArgBuildError { + if cmd.subcommand_required && cmd.subcommands.length() == 0 { + raise ArgBuildError::Unsupported( + "subcommand_required requires at least one subcommand", + ) + } +} + +///| +fn validate_help_subcommand(cmd : Command) -> Unit raise ArgBuildError { + if help_subcommand_enabled(cmd) && + cmd.subcommands.any(cmd => cmd.name == "help") { + raise ArgBuildError::Unsupported( + "subcommand name reserved for built-in help: help (disable with disable_help_subcommand)", + ) + } +} + +///| +fn validate_version_actions(cmd : Command) -> Unit raise ArgBuildError { + if cmd.version is None && + cmd.args.any(arg => arg_action(arg) is ArgAction::Version) { + raise ArgBuildError::Unsupported( + "version action requires command version text", + ) + } +} + +///| +fn validate_command_policies( + cmd : Command, + matches : Matches, +) -> Unit raise ArgParseError { + if cmd.subcommand_required && + cmd.subcommands.length() > 0 && + matches.parsed_subcommand is None { + raise ArgParseError::MissingRequired("subcommand") + } +} + +///| +fn validate_groups( + args : Array[Arg], + groups : Array[ArgGroup], + matches : Matches, +) -> Unit raise ArgParseError { + if groups.length() == 0 { + return + } + let group_presence : Map[String, Int] = {} + let group_seen : @set.Set[String] = @set.new() + let arg_seen : @set.Set[String] = @set.new() + for group in groups { + group_seen.add(group.name) + } + for arg in args { + arg_seen.add(arg.name) + } + for group in groups { + let mut count = 0 + for arg in args { + if !arg_in_group(arg, group) { + continue + } + if matches_has_value_or_flag(matches, arg.name) { + count = count + 1 + } + } + group_presence[group.name] = count + if group.required && count == 0 { + raise ArgParseError::MissingGroup(group.name) + } + if !group.multiple && count > 1 { + raise ArgParseError::GroupConflict(group.name) + } + } + for group in groups { + let count = group_presence[group.name] + if count == 0 { + continue + } + for required in group.requires { + if group_seen.contains(required) { + if group_presence.get(required).unwrap_or(0) == 0 { + raise ArgParseError::MissingGroup(required) + } + } else if arg_seen.contains(required) { + if !matches_has_value_or_flag(matches, required) { + raise ArgParseError::MissingRequired(required) + } + } + } + for conflict in group.conflicts_with { + if group_seen.contains(conflict) { + if group_presence.get(conflict).unwrap_or(0) > 0 { + raise ArgParseError::GroupConflict( + "\{group.name} conflicts with \{conflict}", + ) + } + } else if arg_seen.contains(conflict) { + if matches_has_value_or_flag(matches, conflict) { + raise ArgParseError::GroupConflict( + "\{group.name} conflicts with \{conflict}", + ) + } + } + } + } +} + +///| +fn arg_in_group(arg : Arg, group : ArgGroup) -> Bool { + group.args.contains(arg.name) +} + +///| +fn validate_values( + args : Array[Arg], + matches : Matches, +) -> Unit raise ArgParseError { + for arg in args { + let present = matches_has_value_or_flag(matches, arg.name) + if arg.required && !present { + raise ArgParseError::MissingRequired(arg.name) + } + if !arg_takes_value(arg) { + continue + } + if !present { + if is_positional_arg(arg) { + let (min, _) = arg_min_max(arg) + if min > 0 { + raise ArgParseError::TooFewValues(arg.name, 0, min) + } + } + continue + } + let values = matches.values.get(arg.name).unwrap_or([]) + let count = values.length() + let (min, max) = arg_min_max(arg) + if count < min { + raise ArgParseError::TooFewValues(arg.name, count, min) + } + if arg_action(arg) != ArgAction::Append { + match max { + Some(max) if count > max => + raise ArgParseError::TooManyValues(arg.name, count, max) + _ => () + } + } + } +} + +///| +fn validate_relationships( + matches : Matches, + args : Array[Arg], +) -> Unit raise ArgParseError { + for arg in args { + if !matches_has_value_or_flag(matches, arg.name) { + continue + } + for required in arg.requires { + if !matches_has_value_or_flag(matches, required) { + raise ArgParseError::MissingRequired(required) + } + } + for conflict in arg.conflicts_with { + if matches_has_value_or_flag(matches, conflict) { + raise ArgParseError::InvalidArgument( + "conflicting arguments: \{arg.name} and \{conflict}", + ) + } + } + } +} + +///| +fn is_positional_arg(arg : Arg) -> Bool { + arg.is_positional +} diff --git a/argparse/parser_values.mbt b/argparse/parser_values.mbt new file mode 100644 index 000000000..eaa95ff86 --- /dev/null +++ b/argparse/parser_values.mbt @@ -0,0 +1,317 @@ +// 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. + +///| +fn assign_positionals( + matches : Matches, + positionals : Array[Arg], + values : Array[String], +) -> Unit raise ArgParseError { + let mut cursor = 0 + for idx in 0.. max_count => take = max_count + _ => () + } + if take < min { + take = min + } + if take > remaining { + take = remaining + } + let mut taken = 0 + while taken < take { + add_value( + matches, + arg.name, + values[cursor + taken], + arg, + ValueSource::Argv, + ) + taken = taken + 1 + } + cursor = cursor + taken + continue + } + if remaining > 0 { + add_value(matches, arg.name, values[cursor], arg, ValueSource::Argv) + cursor = cursor + 1 + } + } + if cursor < values.length() { + raise ArgParseError::TooManyPositionals + } +} + +///| +fn positional_min_required(arg : Arg) -> Int { + let (min, _) = arg_min_max(arg) + if min > 0 { + min + } else if arg.required { + 1 + } else { + 0 + } +} + +///| +fn remaining_positional_min(positionals : Array[Arg], start : Int) -> Int { + let mut total = 0 + let mut idx = start + while idx < positionals.length() { + total = total + positional_min_required(positionals[idx]) + idx = idx + 1 + } + total +} + +///| +fn add_value( + matches : Matches, + name : String, + value : String, + arg : Arg, + source : ValueSource, +) -> Unit { + if arg.multiple || arg_action(arg) == ArgAction::Append { + let arr = matches.values.get(name).unwrap_or([]) + arr.push(value) + matches.values[name] = arr + let srcs = matches.value_sources.get(name).unwrap_or([]) + srcs.push(source) + matches.value_sources[name] = srcs + } else { + matches.values[name] = [value] + matches.value_sources[name] = [source] + } +} + +///| +fn assign_value( + matches : Matches, + arg : Arg, + value : String, + source : ValueSource, +) -> Unit raise ArgParseError { + match arg_action(arg) { + ArgAction::Append => add_value(matches, arg.name, value, arg, source) + ArgAction::Set => add_value(matches, arg.name, value, arg, source) + ArgAction::SetTrue => { + let flag = parse_bool(value) + matches.flags[arg.name] = flag + matches.flag_sources[arg.name] = source + } + ArgAction::SetFalse => { + let flag = parse_bool(value) + matches.flags[arg.name] = !flag + matches.flag_sources[arg.name] = source + } + ArgAction::Count => { + let count = parse_count(value) + matches.counts[arg.name] = count + matches.flags[arg.name] = count > 0 + matches.flag_sources[arg.name] = source + } + ArgAction::Help => + raise ArgParseError::InvalidArgument("help action does not take values") + ArgAction::Version => + raise ArgParseError::InvalidArgument( + "version action does not take values", + ) + } +} + +///| +fn option_conflict_label(arg : Arg) -> String { + match arg.long { + Some(name) => "--\{name}" + None => + match arg.short { + Some(short) => "-\{short}" + None => arg.name + } + } +} + +///| +fn check_duplicate_set_occurrence( + matches : Matches, + arg : Arg, +) -> Unit raise ArgParseError { + if arg_action(arg) != ArgAction::Set { + return + } + if matches.values.get(arg.name) is Some(_) { + raise ArgParseError::InvalidArgument( + "argument '\{option_conflict_label(arg)}' cannot be used multiple times", + ) + } +} + +///| +fn should_stop_option_value( + value : String, + arg : Arg, + _long_index : Map[String, Arg], + _short_index : Map[Char, Arg], +) -> Bool { + if !value.has_prefix("-") || value == "-" { + return false + } + if arg.allow_hyphen_values { + // Rust clap parity: + // - `clap_builder/src/parser/parser.rs`: `parse_long_arg` / `parse_short_arg` + // return `ParseResult::MaybeHyphenValue` when the pending arg in + // `ParseState::Opt` or `ParseState::Pos` has `allow_hyphen_values`. + // - `clap_builder/src/builder/arg.rs` (`Arg::allow_hyphen_values` docs): + // prior args with this setting take precedence over known flags/options. + // - `tests/builder/opts.rs` (`leading_hyphen_with_flag_after`): + // a pending option consumes `-f` as a value rather than parsing flag `-f`. + // This also means `--` is consumed as a value while the option remains pending. + return false + } + true +} + +///| +fn apply_env( + matches : Matches, + args : Array[Arg], + env : Map[String, String], +) -> Unit raise ArgParseError { + for arg in args { + let name = arg.name + if matches_has_value_or_flag(matches, name) { + continue + } + let env_name = match arg.env { + Some(value) => value + None => continue + } + let value = match env.get(env_name) { + Some(v) => v + None => continue + } + if arg_takes_value(arg) { + assign_value(matches, arg, value, ValueSource::Env) + continue + } + match arg_action(arg) { + ArgAction::Count => { + let count = parse_count(value) + matches.counts[name] = count + matches.flags[name] = count > 0 + matches.flag_sources[name] = ValueSource::Env + } + ArgAction::SetFalse => { + let flag = parse_bool(value) + matches.flags[name] = !flag + matches.flag_sources[name] = ValueSource::Env + } + ArgAction::SetTrue => { + let flag = parse_bool(value) + matches.flags[name] = flag + matches.flag_sources[name] = ValueSource::Env + } + ArgAction::Set => { + let flag = parse_bool(value) + matches.flags[name] = flag + matches.flag_sources[name] = ValueSource::Env + } + ArgAction::Append => () + ArgAction::Help => () + ArgAction::Version => () + } + } +} + +///| +fn apply_defaults(matches : Matches, args : Array[Arg]) -> Unit { + for arg in args { + if !arg_takes_value(arg) { + continue + } + if matches_has_value_or_flag(matches, arg.name) { + continue + } + match arg.default_values { + Some(values) if values.length() > 0 => + for value in values { + let _ = add_value(matches, arg.name, value, arg, ValueSource::Default) + } + _ => () + } + } +} + +///| +fn matches_has_value_or_flag(matches : Matches, name : String) -> Bool { + matches.flags.get(name) is Some(_) || matches.values.get(name) is Some(_) +} + +///| +fn apply_flag(matches : Matches, arg : Arg, source : ValueSource) -> Unit { + match arg_action(arg) { + ArgAction::SetTrue => matches.flags[arg.name] = true + ArgAction::SetFalse => matches.flags[arg.name] = false + ArgAction::Count => { + let current = matches.counts.get(arg.name).unwrap_or(0) + matches.counts[arg.name] = current + 1 + matches.flags[arg.name] = true + } + ArgAction::Help => () + ArgAction::Version => () + _ => matches.flags[arg.name] = true + } + matches.flag_sources[arg.name] = source +} + +///| +fn parse_bool(value : String) -> Bool raise ArgParseError { + if value == "1" || value == "true" || value == "yes" || value == "on" { + true + } else if value == "0" || value == "false" || value == "no" || value == "off" { + false + } else { + raise ArgParseError::InvalidValue( + "invalid value '\{value}' for boolean flag; expected one of: 1, 0, true, false, yes, no, on, off", + ) + } +} + +///| +fn parse_count(value : String) -> Int raise ArgParseError { + try @strconv.parse_int(value) catch { + _ => + raise ArgParseError::InvalidValue( + "invalid value '\{value}' for count; expected a non-negative integer", + ) + } noraise { + _..<0 => + raise ArgParseError::InvalidValue( + "invalid value '\{value}' for count; expected a non-negative integer", + ) + v => v + } +} diff --git a/argparse/pkg.generated.mbti b/argparse/pkg.generated.mbti index 6dc9ea171..53e30798a 100644 --- a/argparse/pkg.generated.mbti +++ b/argparse/pkg.generated.mbti @@ -64,9 +64,9 @@ pub impl Show for FlagAction pub struct FlagArg { // private fields - fn new(String, short? : Char, long? : String, about? : String, action? : FlagAction, env? : String, requires? : Array[String], conflicts_with? : Array[String], group? : String, required? : Bool, global? : Bool, negatable? : Bool, hidden? : Bool) -> FlagArg + fn new(String, short? : Char, long? : String, about? : String, action? : FlagAction, env? : String, requires? : Array[String], conflicts_with? : Array[String], required? : Bool, global? : Bool, negatable? : Bool, hidden? : Bool) -> FlagArg } -pub fn FlagArg::new(String, short? : Char, long? : String, about? : String, action? : FlagAction, env? : String, requires? : Array[String], conflicts_with? : Array[String], group? : String, required? : Bool, global? : Bool, negatable? : Bool, hidden? : Bool) -> Self +pub fn FlagArg::new(String, short? : Char, long? : String, about? : String, action? : FlagAction, env? : String, requires? : Array[String], conflicts_with? : Array[String], required? : Bool, global? : Bool, negatable? : Bool, hidden? : Bool) -> Self pub impl ArgLike for FlagArg pub struct Matches { @@ -88,26 +88,25 @@ pub impl Show for OptionAction pub struct OptionArg { // private fields - fn new(String, short? : Char, long? : String, about? : String, action? : OptionAction, env? : String, default_values? : Array[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : Array[String], conflicts_with? : Array[String], group? : String, required? : Bool, global? : Bool, hidden? : Bool) -> OptionArg + fn new(String, short? : Char, long? : String, about? : String, action? : OptionAction, env? : String, default_values? : Array[String], allow_hyphen_values? : Bool, last? : Bool, requires? : Array[String], conflicts_with? : Array[String], required? : Bool, global? : Bool, hidden? : Bool) -> OptionArg } -pub fn OptionArg::new(String, short? : Char, long? : String, about? : String, action? : OptionAction, env? : String, default_values? : Array[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : Array[String], conflicts_with? : Array[String], group? : String, required? : Bool, global? : Bool, hidden? : Bool) -> Self +pub fn OptionArg::new(String, short? : Char, long? : String, about? : String, action? : OptionAction, env? : String, default_values? : Array[String], allow_hyphen_values? : Bool, last? : Bool, requires? : Array[String], conflicts_with? : Array[String], required? : Bool, global? : Bool, hidden? : Bool) -> Self pub impl ArgLike for OptionArg pub struct PositionalArg { // private fields - fn new(String, index? : Int, about? : String, env? : String, default_values? : Array[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : Array[String], conflicts_with? : Array[String], group? : String, required? : Bool, global? : Bool, hidden? : Bool) -> PositionalArg + fn new(String, index? : Int, about? : String, env? : String, default_values? : Array[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : Array[String], conflicts_with? : Array[String], required? : Bool, global? : Bool, hidden? : Bool) -> PositionalArg } -pub fn PositionalArg::new(String, index? : Int, about? : String, env? : String, default_values? : Array[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : Array[String], conflicts_with? : Array[String], group? : String, required? : Bool, global? : Bool, hidden? : Bool) -> Self +pub fn PositionalArg::new(String, index? : Int, about? : String, env? : String, default_values? : Array[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : Array[String], conflicts_with? : Array[String], required? : Bool, global? : Bool, hidden? : Bool) -> Self pub impl ArgLike for PositionalArg pub struct ValueRange { // private fields - fn new(lower? : Int, upper? : Int, lower_inclusive? : Bool, upper_inclusive? : Bool) -> ValueRange + fn new(lower? : Int, upper? : Int) -> ValueRange } -pub fn ValueRange::empty() -> Self -pub fn ValueRange::new(lower? : Int, upper? : Int, lower_inclusive? : Bool, upper_inclusive? : Bool) -> Self +pub fn ValueRange::new(lower? : Int, upper? : Int) -> Self pub fn ValueRange::single() -> Self pub impl Eq for ValueRange pub impl Show for ValueRange diff --git a/argparse/value_range.mbt b/argparse/value_range.mbt index 91e8f9d35..4fba32977 100644 --- a/argparse/value_range.mbt +++ b/argparse/value_range.mbt @@ -18,38 +18,22 @@ pub struct ValueRange { priv lower : Int priv upper : Int? - fn new( - lower? : Int, - upper? : Int, - lower_inclusive? : Bool, - upper_inclusive? : Bool, - ) -> ValueRange + /// Create a value-count range. + fn new(lower? : Int, upper? : Int) -> ValueRange } derive(Eq, Show) ///| -pub fn ValueRange::empty() -> ValueRange { - ValueRange(lower=0, upper=0) -} - -///| +/// Exact single-value range (`1..1`). pub fn ValueRange::single() -> ValueRange { ValueRange(lower=1, upper=1) } ///| -pub fn ValueRange::new( - lower? : Int, - upper? : Int, - lower_inclusive? : Bool = true, - upper_inclusive? : Bool = true, -) -> ValueRange { - let lower = match lower { - None => 0 - Some(lower) => if lower_inclusive { lower } else { lower + 1 } - } - let upper = match upper { - None => None - Some(upper) => Some(if upper_inclusive { upper } else { upper - 1 }) - } +/// Create a value-count range. +/// +/// Examples: +/// - `ValueRange(lower=0)` means `0..`. +/// - `ValueRange(lower=1, upper=3)` means `1..=3`. +pub fn ValueRange::new(lower? : Int = 0, upper? : Int) -> ValueRange { ValueRange::{ lower, upper } } From ce94f8652148f8a0c3bf2dc3f5d486db47ccc23a Mon Sep 17 00:00:00 2001 From: zihang Date: Fri, 13 Feb 2026 17:45:09 +0800 Subject: [PATCH 04/40] refactor: one source per match --- argparse/command.mbt | 2 +- argparse/matches.mbt | 29 +----------------- argparse/parser.mbt | 2 +- argparse/parser_globals_merge.mbt | 49 ++++++++++--------------------- argparse/parser_positionals.mbt | 33 +-------------------- argparse/parser_values.mbt | 6 ++-- 6 files changed, 22 insertions(+), 99 deletions(-) diff --git a/argparse/command.mbt b/argparse/command.mbt index 37a25918e..37141072c 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -143,7 +143,7 @@ fn build_matches( Some(v) => Some(v) None => match raw.value_sources.get(name) { - Some(vs) => highest_source(vs) + Some(v) => Some(v) None => None } } diff --git a/argparse/matches.mbt b/argparse/matches.mbt index 1ebd04108..886e87c6b 100644 --- a/argparse/matches.mbt +++ b/argparse/matches.mbt @@ -30,7 +30,7 @@ pub struct Matches { subcommand : (String, Matches)? priv counts : Map[String, Int] priv flag_sources : Map[String, ValueSource] - priv value_sources : Map[String, Array[ValueSource]] + priv value_sources : Map[String, ValueSource] priv mut parsed_subcommand : (String, Matches)? } @@ -49,33 +49,6 @@ fn new_matches_parse_state() -> Matches { } } -///| -fn highest_source(sources : Array[ValueSource]) -> ValueSource? { - if sources.length() == 0 { - return None - } - let mut saw_env = false - let mut saw_default = false - for s in sources { - if s == ValueSource::Argv { - return Some(ValueSource::Argv) - } - if s == ValueSource::Env { - saw_env = true - } - if s == ValueSource::Default { - saw_default = true - } - } - if saw_env { - Some(ValueSource::Env) - } else if saw_default { - Some(ValueSource::Default) - } else { - None - } -} - ///| /// Decode a full argument struct/enum from `Matches`. pub(open) trait FromMatches { diff --git a/argparse/parser.mbt b/argparse/parser.mbt index 95ce7c2e9..557149122 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -124,7 +124,7 @@ fn parse_command( long_index.get("version") is None let positionals = positional_args(args) let positional_values = [] - let last_pos_idx = last_positional_index(positionals) + let last_pos_idx = positionals.search_by(arg => arg.last) let mut i = 0 let mut positional_arg_found = false while i < argv.length() { diff --git a/argparse/parser_globals_merge.mbt b/argparse/parser_globals_merge.mbt index a65710c8d..0e7716231 100644 --- a/argparse/parser_globals_merge.mbt +++ b/argparse/parser_globals_merge.mbt @@ -53,14 +53,6 @@ fn strongest_source( } } -///| -fn source_from_values(sources : Array[ValueSource]?) -> ValueSource? { - match sources { - Some(items) if items.length() > 0 => highest_source(items) - _ => None - } -} - ///| fn merge_global_value_from_child( parent : Matches, @@ -70,44 +62,31 @@ fn merge_global_value_from_child( ) -> Unit { let parent_vals = parent.values.get(name) let child_vals = child.values.get(name) - let parent_srcs = parent.value_sources.get(name) - let child_srcs = child.value_sources.get(name) + let parent_source = parent.value_sources.get(name) + let child_source = child.value_sources.get(name) let has_parent = parent_vals is Some(pv) && pv.length() > 0 let has_child = child_vals is Some(cv) && cv.length() > 0 if !has_parent && !has_child { return } - let parent_source = source_from_values(parent_srcs) - let child_source = source_from_values(child_srcs) if arg.multiple || arg_action(arg) == ArgAction::Append { let both_argv = parent_source is Some(ValueSource::Argv) && child_source is Some(ValueSource::Argv) if both_argv { let merged = [] - let merged_srcs = [] if parent_vals is Some(pv) { for v in pv { merged.push(v) } } - if parent_srcs is Some(ps) { - for s in ps { - merged_srcs.push(s) - } - } if child_vals is Some(cv) { for v in cv { merged.push(v) } } - if child_srcs is Some(cs) { - for s in cs { - merged_srcs.push(s) - } - } if merged.length() > 0 { parent.values[name] = merged - parent.value_sources[name] = merged_srcs + parent.value_sources[name] = ValueSource::Argv } } else { let choose_child = has_child && @@ -116,13 +95,15 @@ fn merge_global_value_from_child( if child_vals is Some(cv) && cv.length() > 0 { parent.values[name] = cv.copy() } - if child_srcs is Some(cs) && cs.length() > 0 { - parent.value_sources[name] = cs.copy() + match child_source { + Some(src) => parent.value_sources[name] = src + None => () } } else if parent_vals is Some(pv) && pv.length() > 0 { parent.values[name] = pv.copy() - if parent_srcs is Some(ps) && ps.length() > 0 { - parent.value_sources[name] = ps.copy() + match parent_source { + Some(src) => parent.value_sources[name] = src + None => () } } } @@ -133,13 +114,15 @@ fn merge_global_value_from_child( if child_vals is Some(cv) && cv.length() > 0 { parent.values[name] = cv.copy() } - if child_srcs is Some(cs) && cs.length() > 0 { - parent.value_sources[name] = cs.copy() + match child_source { + Some(src) => parent.value_sources[name] = src + None => () } } else if parent_vals is Some(pv) && pv.length() > 0 { parent.values[name] = pv.copy() - if parent_srcs is Some(ps) && ps.length() > 0 { - parent.value_sources[name] = ps.copy() + match parent_source { + Some(src) => parent.value_sources[name] = src + None => () } } } @@ -238,7 +221,7 @@ fn propagate_globals_to_child( Some(values) => { child.values[name] = values.copy() match parent.value_sources.get(name) { - Some(srcs) => child.value_sources[name] = srcs.copy() + Some(src) => child.value_sources[name] = src None => () } } diff --git a/argparse/parser_positionals.mbt b/argparse/parser_positionals.mbt index 1559ab3a1..3dc5aee5e 100644 --- a/argparse/parser_positionals.mbt +++ b/argparse/parser_positionals.mbt @@ -25,7 +25,7 @@ fn positional_args(args : Array[Arg]) -> Array[Arg] { } } } - sort_positionals(with_index) + with_index.sort_by_key(pair => pair.0) let ordered = [] for item in with_index { let (_, arg) = item @@ -37,18 +37,6 @@ fn positional_args(args : Array[Arg]) -> Array[Arg] { ordered } -///| -fn last_positional_index(positionals : Array[Arg]) -> Int? { - let mut i = 0 - while i < positionals.length() { - if positionals[i].last { - return Some(i) - } - i = i + 1 - } - None -} - ///| fn next_positional(positionals : Array[Arg], collected : Array[String]) -> Arg? { let target = collected.length() @@ -137,22 +125,3 @@ fn is_negative_number(arg : String) -> Bool { } true } - -///| -fn sort_positionals(items : Array[(Int, Arg)]) -> Unit { - let mut i = 1 - while i < items.length() { - let key = items[i] - let mut j = i - 1 - while j >= 0 && items[j].0 > key.0 { - items[j + 1] = items[j] - if j == 0 { - j = -1 - } else { - j = j - 1 - } - } - items[j + 1] = key - i = i + 1 - } -} diff --git a/argparse/parser_values.mbt b/argparse/parser_values.mbt index eaa95ff86..1e6964bfd 100644 --- a/argparse/parser_values.mbt +++ b/argparse/parser_values.mbt @@ -98,12 +98,10 @@ fn add_value( let arr = matches.values.get(name).unwrap_or([]) arr.push(value) matches.values[name] = arr - let srcs = matches.value_sources.get(name).unwrap_or([]) - srcs.push(source) - matches.value_sources[name] = srcs + matches.value_sources[name] = source } else { matches.values[name] = [value] - matches.value_sources[name] = [source] + matches.value_sources[name] = source } } From 8e16b0c1c60bb9b7c6acd2c9c88b87a1c5fbf63a Mon Sep 17 00:00:00 2001 From: zihang Date: Sat, 14 Feb 2026 17:51:56 +0800 Subject: [PATCH 05/40] fix: validation --- argparse/parser_validate.mbt | 42 ++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/argparse/parser_validate.mbt b/argparse/parser_validate.mbt index 81943a99c..be8344e3f 100644 --- a/argparse/parser_validate.mbt +++ b/argparse/parser_validate.mbt @@ -150,10 +150,16 @@ fn validate_flag_arg( ctx : ValidationCtx, ) -> Unit raise ArgBuildError { validate_named_option_arg(arg) - guard arg.index is None && - !arg.last && - arg.num_args is None && - arg.default_values is None + if arg.index is Some(_) || arg.last { + raise ArgBuildError::Unsupported( + "positional-only settings require no short/long", + ) + } + if arg.num_args is Some(_) { + raise ArgBuildError::Unsupported( + "min/max values require value-taking arguments", + ) + } if arg.flag_action is (Help | Version) { guard !arg.negatable else { raise ArgBuildError::Unsupported( @@ -171,6 +177,11 @@ fn validate_flag_arg( ) } } + if arg.default_values is Some(_) { + raise ArgBuildError::Unsupported( + "default values require value-taking arguments", + ) + } ctx.record_arg(arg) } @@ -180,7 +191,19 @@ fn validate_option_arg( ctx : ValidationCtx, ) -> Unit raise ArgBuildError { validate_named_option_arg(arg) - guard arg.index is None && !arg.last && !arg.negatable && arg.num_args is None + if arg.index is Some(_) || arg.last { + raise ArgBuildError::Unsupported( + "positional-only settings require no short/long", + ) + } + guard !arg.negatable else { + raise ArgBuildError::Unsupported("negatable is only supported for flags") + } + guard arg.num_args is None else { + raise ArgBuildError::Unsupported( + "min/max values require value-taking arguments", + ) + } validate_default_values(arg) ctx.record_arg(arg) } @@ -190,7 +213,14 @@ fn validate_positional_arg( arg : Arg, ctx : ValidationCtx, ) -> Unit raise ArgBuildError { - guard arg.long is None && arg.short is None && !arg.negatable + if arg.long is Some(_) || arg.short is Some(_) { + raise ArgBuildError::Unsupported( + "positional args do not support short/long", + ) + } + guard !arg.negatable else { + raise ArgBuildError::Unsupported("negatable is only supported for flags") + } let (min, max) = arg_min_max_for_validate(arg) if (min > 1 || (max is Some(m) && m > 1)) && !arg.multiple { raise ArgBuildError::Unsupported( From 7de9550b59b886002c5502f1c164ab898ee5a3a2 Mon Sep 17 00:00:00 2001 From: flycloudc Date: Wed, 25 Feb 2026 15:07:15 +0800 Subject: [PATCH 06/40] refactor: reorganize Arg struct fields for clarity --- argparse/arg_spec.mbt | 128 +++++++++++++++++++++++++----------------- 1 file changed, 76 insertions(+), 52 deletions(-) diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt index c00949976..8d30b68b1 100644 --- a/argparse/arg_spec.mbt +++ b/argparse/arg_spec.mbt @@ -32,27 +32,34 @@ pub(all) enum OptionAction { ///| /// Unified argument model used by the parser internals. priv struct Arg { + // All name : String - short : Char? - long : String? - index : Int? about : String? - is_flag : Bool - is_positional : Bool - flag_action : FlagAction - option_action : OptionAction env : String? - default_values : Array[String]? - num_args : ValueRange? - multiple : Bool - allow_hyphen_values : Bool - last : Bool requires : Array[String] conflicts_with : Array[String] required : Bool global : Bool - negatable : Bool hidden : Bool + // Flag and Option + short : Char? + long : String? + // Flag + flag_action : FlagAction + negatable : Bool + // Option + option_action : OptionAction + // Positional + index : Int? + num_args : ValueRange? + // Option and Positional + default_values : Array[String]? + allow_hyphen_values : Bool + last : Bool + // other + is_flag : Bool + is_positional : Bool + multiple : Bool } ///| @@ -119,26 +126,31 @@ pub fn FlagArg::new( FlagArg::{ arg: Arg::{ name, + about, + env, + global, + hidden, + requires: requires.copy(), + conflicts_with: conflicts_with.copy(), + required, + // short, long, - index: None, - about, - is_flag: true, - is_positional: false, flag_action: action, + negatable, + // option_action: OptionAction::Set, - env, - default_values: None, + // + index: None, num_args: None, - multiple: false, + // + default_values: None, allow_hyphen_values: false, last: false, - requires: requires.copy(), - conflicts_with: conflicts_with.copy(), - required, - global, - negatable, - hidden, + // + is_flag: true, + is_positional: false, + multiple: false, }, } } @@ -204,26 +216,32 @@ pub fn OptionArg::new( OptionArg::{ arg: Arg::{ name, - short, - long, - index: None, about, - is_flag: false, - is_positional: false, - flag_action: FlagAction::SetTrue, - option_action: action, env, - default_values: default_values.map(Array::copy), - num_args: None, - multiple: allows_multiple_values(action), - allow_hyphen_values, - last, requires: requires.copy(), conflicts_with: conflicts_with.copy(), required, global, - negatable: false, hidden, + // + short, + long, + // + flag_action: FlagAction::SetTrue, + negatable: false, + // + option_action: action, + // + index: None, + num_args: None, + // + default_values: default_values.map(Array::copy), + allow_hyphen_values, + last, + // + is_flag: false, + is_positional: false, + multiple: allows_multiple_values(action), }, } } @@ -292,26 +310,32 @@ pub fn PositionalArg::new( PositionalArg::{ arg: Arg::{ name, - short: None, - long: None, - index, about, - is_flag: false, - is_positional: true, - flag_action: FlagAction::SetTrue, - option_action: OptionAction::Set, env, - default_values: default_values.map(Array::copy), - num_args, - multiple: range_allows_multiple(num_args), - allow_hyphen_values, - last, requires: requires.copy(), conflicts_with: conflicts_with.copy(), required, global, - negatable: false, hidden, + // + short: None, + long: None, + // + flag_action: FlagAction::SetTrue, + negatable: false, + // + option_action: OptionAction::Set, + // + index, + num_args, + // + default_values: default_values.map(Array::copy), + allow_hyphen_values, + last, + // + is_flag: false, + is_positional: true, + multiple: range_allows_multiple(num_args), }, } } From fd9fb13d0976bd87b57cc7299b1065eb6f02dad1 Mon Sep 17 00:00:00 2001 From: flycloudc Date: Wed, 25 Feb 2026 19:35:35 +0800 Subject: [PATCH 07/40] refactor: unify Arg struct fields under ArgInfo for improved clarity and maintainability --- argparse/arg_action.mbt | 38 +++++----- argparse/arg_spec.mbt | 114 ++++++++++++++-------------- argparse/argparse_blackbox_test.mbt | 17 ----- argparse/command.mbt | 4 +- argparse/help_render.mbt | 34 ++++++--- argparse/parser.mbt | 20 +++-- argparse/parser_lookup.mbt | 16 ++-- argparse/parser_positionals.mbt | 11 ++- argparse/parser_validate.mbt | 104 +++++++++++-------------- argparse/parser_values.mbt | 28 ++++--- 10 files changed, 191 insertions(+), 195 deletions(-) diff --git a/argparse/arg_action.mbt b/argparse/arg_action.mbt index 3f09e86e2..4cafaac22 100644 --- a/argparse/arg_action.mbt +++ b/argparse/arg_action.mbt @@ -26,30 +26,32 @@ priv enum ArgAction { ///| fn arg_takes_value(arg : Arg) -> Bool { - !arg.is_flag + !(arg.info 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 - } + match arg.info { + Flag(action~, ..) => + match action { + FlagAction::SetTrue => ArgAction::SetTrue + FlagAction::SetFalse => ArgAction::SetFalse + FlagAction::Count => ArgAction::Count + FlagAction::Help => ArgAction::Help + FlagAction::Version => ArgAction::Version + } + Option(action~, ..) => + match action { + OptionAction::Set => ArgAction::Set + OptionAction::Append => ArgAction::Append + } + Positional(_) => ArgAction::Set } } ///| fn arg_min_max_for_validate(arg : Arg) -> (Int, Int?) raise ArgBuildError { - if arg.num_args is Some(range) { + if arg.info is Positional(num_args=Some(range), ..) { if range.lower < 0 { raise ArgBuildError::Unsupported("min values must be >= 0") } @@ -74,8 +76,8 @@ fn arg_min_max_for_validate(arg : Arg) -> (Int, Int?) raise ArgBuildError { ///| fn arg_min_max(arg : Arg) -> (Int, Int?) { - match arg.num_args { - Some(range) => (range.lower, range.upper) - None => (0, None) + match arg.info { + Positional(num_args=Some(range), ..) => (range.lower, range.upper) + _ => (0, None) } } diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt index 8d30b68b1..b30c0ca45 100644 --- a/argparse/arg_spec.mbt +++ b/argparse/arg_spec.mbt @@ -41,27 +41,30 @@ priv struct Arg { required : Bool global : Bool hidden : Bool - // Flag and Option - short : Char? - long : String? - // Flag - flag_action : FlagAction - negatable : Bool - // Option - option_action : OptionAction - // Positional - index : Int? - num_args : ValueRange? - // Option and Positional - default_values : Array[String]? - allow_hyphen_values : Bool - last : Bool - // other - is_flag : Bool - is_positional : Bool + info : ArgInfo multiple : Bool } +///| +priv enum ArgInfo { + Flag(short~ : Char?, long~ : String?, action~ : FlagAction, negatable~ : Bool) + Option( + short~ : Char?, + long~ : String?, + action~ : OptionAction, + default_values~ : Array[String]?, + allow_hyphen_values~ : Bool, + last~ : Bool + ) + Positional( + index~ : Int?, + num_args~ : ValueRange?, + default_values~ : Array[String]?, + allow_hyphen_values~ : Bool, + last~ : Bool + ) +} + ///| /// Trait for declarative arg constructors. trait ArgLike { @@ -134,22 +137,17 @@ pub fn FlagArg::new( conflicts_with: conflicts_with.copy(), required, // - short, - long, - flag_action: action, - negatable, + info: Flag(short~, long~, action~, negatable~), // - option_action: OptionAction::Set, + // option_action: OptionAction::Set, // - index: None, - num_args: None, + // index: None, + // num_args: None, // - default_values: None, - allow_hyphen_values: false, - last: false, + // default_values: None, + // allow_hyphen_values: false, + // last: false, // - is_flag: true, - is_positional: false, multiple: false, }, } @@ -224,23 +222,23 @@ pub fn OptionArg::new( global, hidden, // - short, - long, + info: Option( + short~, + long~, + action~, + default_values=default_values.map(Array::copy), + allow_hyphen_values~, + last~, + ), // - flag_action: FlagAction::SetTrue, - negatable: false, + // flag_action: FlagAction::SetTrue, + // negatable: false, // - option_action: action, + // option_action: action, // - index: None, - num_args: None, + // index: None, + // num_args: None, // - default_values: default_values.map(Array::copy), - allow_hyphen_values, - last, - // - is_flag: false, - is_positional: false, multiple: allows_multiple_values(action), }, } @@ -318,23 +316,21 @@ pub fn PositionalArg::new( global, hidden, // - short: None, - long: None, - // - flag_action: FlagAction::SetTrue, - negatable: false, - // - option_action: OptionAction::Set, + info: Positional( + index~, + num_args~, + default_values=default_values.map(Array::copy), + allow_hyphen_values~, + last~, + ), + // short: None, + // long: None, // - index, - num_args, + // flag_action: FlagAction::SetTrue, + // negatable: false, // - default_values: default_values.map(Array::copy), - allow_hyphen_values, - last, + // option_action: OptionAction::Set, // - is_flag: false, - is_positional: true, multiple: range_allows_multiple(num_args), }, } @@ -347,12 +343,12 @@ fn arg_name(arg : Arg) -> String { ///| fn is_flag_spec(arg : Arg) -> Bool { - arg.is_flag + arg.info is Flag(_) } ///| fn is_count_flag_spec(arg : Arg) -> Bool { - arg.is_flag && arg.flag_action == FlagAction::Count + arg.info is Flag(action=Count, ..) } ///| diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 262bbd422..4cc59542b 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -1136,23 +1136,6 @@ test "default argv path is reachable" { ///| test "validation branches exposed through parse" { - try - @argparse.Command("demo", args=[ - @argparse.OptionArg("x", long="x", last=true), - ]).parse(argv=[], env=empty_env()) - catch { - @argparse.ArgBuildError::Unsupported(msg) => - inspect( - msg, - content=( - #|positional-only settings require no short/long - ), - ) - _ => panic() - } noraise { - _ => panic() - } - try @argparse.Command("demo", args=[ @argparse.FlagArg("f", action=@argparse.FlagAction::Help), diff --git a/argparse/command.mbt b/argparse/command.mbt index 37141072c..d7314d66a 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -163,7 +163,9 @@ fn build_matches( } let child_globals = inherited_globals + cmd.args.filter(arg => { - arg.global && (arg.long is Some(_) || arg.short is Some(_)) + arg.global && + arg.info is (Option(long~, short~, ..) | Flag(long~, short~, ..)) && + (long is Some(_) || short is Some(_)) }) let subcommand = match raw.parsed_subcommand { diff --git a/argparse/help_render.mbt b/argparse/help_render.mbt index 592b8e3b2..2b00decc4 100644 --- a/argparse/help_render.mbt +++ b/argparse/help_render.mbt @@ -75,7 +75,8 @@ fn has_options(cmd : Command) -> Bool { if arg.hidden { continue } - if arg.long is Some(_) || arg.short is Some(_) { + if arg.info is (Option(long~, short~, ..) | Flag(long~, short~, ..)) && + (long is Some(_) || short is Some(_)) { return true } } @@ -130,7 +131,8 @@ fn option_entries(cmd : Command) -> Array[String] { display.push((label, "Show version information.")) } for arg in args { - if arg.long is None && arg.short is None { + if !(arg.info is (Option(long~, short~, ..) | Flag(long~, short~, ..)) && + (long is Some(_) || short is Some(_))) { continue } if arg.hidden { @@ -149,7 +151,9 @@ fn option_entries(cmd : Command) -> Array[String] { ///| fn has_long_option(args : Array[Arg], name : String) -> Bool { for arg in args { - if arg.long is Some(long) && long == name { + if arg.info is (Option(long~, ..) | Flag(long~, ..)) && + long is Some(long) && + long == name { return true } } @@ -159,7 +163,9 @@ fn has_long_option(args : Array[Arg], name : String) -> Bool { ///| fn has_short_option(args : Array[Arg], value : Char) -> Bool { for arg in args { - if arg.short is Some(short) && short == value { + if arg.info is (Option(short~, ..) | Flag(short~, ..)) && + short is Some(short) && + short == value { return true } } @@ -272,11 +278,16 @@ fn format_entries(display : Array[(String, String)]) -> Array[String] { ///| fn arg_display(arg : Arg) -> String { let parts = Array::new(capacity=2) - if arg.short is Some(short) { + let (short, long) = match arg.info { + Option(short~, long~, ..) => (short, long) + Flag(short~, long~, ..) => (short, long) + Positional(_) => (None, None) + } + if short is Some(short) { parts.push("-\{short}") } - if arg.long is Some(long) { - if arg.negatable && !arg_takes_value(arg) { + if long is Some(long) { + if arg.info is Flag(negatable=true, ..) { parts.push("--[no-]\{long}") } else { parts.push("--\{long}") @@ -305,13 +316,14 @@ fn arg_doc(arg : Arg) -> String { Some(env_name) => notes.push("env: \{env_name}") None => () } - match arg.default_values { - Some(values) if values.length() > 1 => { + if arg.info is (Option(default_values~, ..) | Positional(default_values~, ..)) && + default_values is Some(values) { + if values.length() > 1 { let defaults = values.join(", ") notes.push("defaults: \{defaults}") + } else if values.length() == 1 { + notes.push("default: \{values[0]}") } - Some(values) if values.length() == 1 => notes.push("default: \{values[0]}") - _ => () } if is_required_arg(arg) { notes.push("required") diff --git a/argparse/parser.mbt b/argparse/parser.mbt index 557149122..3165402ef 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -103,11 +103,11 @@ fn parse_command( let child_version_long = inherited_version_long.copy() let child_version_short = inherited_version_short.copy() for global in globals_here { - if arg_action(global) == ArgAction::Version { - if global.long is Some(name) { + if global.info is Flag(long~, short~, action=Version, ..) { + if long is Some(name) { child_version_long[name] = command_version(cmd) } - if global.short is Some(short) { + if short is Some(short) { child_version_short[short] = command_version(cmd) } } @@ -124,7 +124,9 @@ fn parse_command( long_index.get("version") is None let positionals = positional_args(args) let positional_values = [] - let last_pos_idx = positionals.search_by(arg => arg.last) + let last_pos_idx = positionals.search_by(arg => { + arg.info is Positional(last=true, ..) + }) let mut i = 0 let mut positional_arg_found = false while i < argv.length() { @@ -193,7 +195,7 @@ fn parse_command( match long_index.get(target) { None => raise_unknown_long(name, long_index) Some(spec) => { - if !spec.negatable || arg_takes_value(spec) { + if !(spec.info is Flag(negatable=true, ..)) { raise_unknown_long(name, long_index) } if inline is Some(_) { @@ -412,9 +414,7 @@ fn version_text_for_long_action( inherited_version_long : Map[String, String], ) -> String { for arg in cmd.args { - if arg.long is Some(name) && - name == long && - arg_action(arg) == ArgAction::Version { + if arg.info is Flag(long=Some(name), action=Version, ..) && name == long { return command_version(cmd) } } @@ -428,9 +428,7 @@ fn version_text_for_short_action( inherited_version_short : Map[Char, String], ) -> String { for arg in cmd.args { - if arg.short is Some(value) && - value == short && - arg_action(arg) == ArgAction::Version { + if arg.info is Flag(short=Some(value), action=Version, ..) && value == short { return command_version(cmd) } } diff --git a/argparse/parser_lookup.mbt b/argparse/parser_lookup.mbt index 256998815..adef1310d 100644 --- a/argparse/parser_lookup.mbt +++ b/argparse/parser_lookup.mbt @@ -19,12 +19,12 @@ fn build_long_index( ) -> Map[String, Arg] { let index : Map[String, Arg] = {} for arg in globals { - if arg.long is Some(name) { + if arg.info is (Flag(long~, ..) | Option(long~, ..)) && long is Some(name) { index[name] = arg } } for arg in args { - if arg.long is Some(name) { + if arg.info is (Flag(long~, ..) | Option(long~, ..)) && long is Some(name) { index[name] = arg } } @@ -35,12 +35,14 @@ fn build_long_index( fn build_short_index(globals : Array[Arg], args : Array[Arg]) -> Map[Char, Arg] { let index : Map[Char, Arg] = {} for arg in globals { - if arg.short is Some(value) { + if arg.info is (Flag(short~, ..) | Option(short~, ..)) && + short is Some(value) { index[value] = arg } } for arg in args { - if arg.short is Some(value) { + if arg.info is (Flag(short~, ..) | Option(short~, ..)) && + short is Some(value) { index[value] = arg } } @@ -49,7 +51,11 @@ fn build_short_index(globals : Array[Arg], args : Array[Arg]) -> Map[Char, Arg] ///| fn collect_globals(args : Array[Arg]) -> Array[Arg] { - args.filter(arg => arg.global && (arg.long is Some(_) || arg.short is Some(_))) + args.filter(arg => { + arg.global && + arg.info is (Flag(long~, short~, ..) | Option(long~, short~, ..)) && + (long is Some(_) || short is Some(_)) + }) } ///| diff --git a/argparse/parser_positionals.mbt b/argparse/parser_positionals.mbt index 3dc5aee5e..616071d16 100644 --- a/argparse/parser_positionals.mbt +++ b/argparse/parser_positionals.mbt @@ -17,8 +17,8 @@ fn positional_args(args : Array[Arg]) -> Array[Arg] { let with_index = [] let without_index = [] for arg in args { - if is_positional_arg(arg) { - if arg.index is Some(idx) { + if arg.info is Positional(index~, ..) { + if index is Some(idx) { with_index.push((idx, arg)) } else { without_index.push(arg) @@ -94,7 +94,12 @@ fn should_parse_as_positional( Some(v) => v None => return false } - let allow = next.allow_hyphen_values || is_negative_number(arg) + let next_allow = match next.info { + Flag(_) => false + Option(allow_hyphen_values~, ..) | Positional(allow_hyphen_values~, ..) => + allow_hyphen_values + } + let allow = next_allow || is_negative_number(arg) if !allow { return false } diff --git a/argparse/parser_validate.mbt b/argparse/parser_validate.mbt index be8344e3f..6ce89ac14 100644 --- a/argparse/parser_validate.mbt +++ b/argparse/parser_validate.mbt @@ -49,21 +49,41 @@ fn ValidationCtx::record_arg( "arg '\{arg.name}' shadows an inherited global; rename the arg or mark it global", ) } - if arg.long is Some(name) { + fn check_long(name) raise _ { if !self.seen_long.add_and_check(name) { raise ArgBuildError::Unsupported("duplicate long option: --\{name}") } - if arg.negatable && !self.seen_long.add_and_check("no-\{name}") { - raise ArgBuildError::Unsupported("duplicate long option: --no-\{name}") - } } - if arg.short is Some(short) && !self.seen_short.add_and_check(short) { - raise ArgBuildError::Unsupported("duplicate short option: -\{short}") + fn check_short(short) raise _ { + if !self.seen_short.add_and_check(short) { + raise ArgBuildError::Unsupported("duplicate short option: -\{short}") + } } - if arg.is_positional && - arg.index is Some(index) && - !self.seen_positional_indices.add_and_check(index) { - raise ArgBuildError::Unsupported("duplicate positional index: \{index}") + match arg.info { + Flag(long~, short~, negatable~, ..) => { + if long is Some(name) { + check_long(name) + if negatable { + check_long("no-\{name}") + } + } + if short is Some(short) { + check_short(short) + } + } + Option(long~, short~, ..) => { + if long is Some(name) { + check_long(name) + } + if short is Some(short) { + check_short(short) + } + } + Positional(index~, ..) => + if index is Some(index) && + !self.seen_positional_indices.add_and_check(index) { + raise ArgBuildError::Unsupported("duplicate positional index: \{index}") + } } self.args.push(arg) } @@ -129,15 +149,12 @@ fn validate_indexed_positional_num_args( let mut idx = 0 while idx + 1 < positionals.length() { let arg = positionals[idx] - if arg.index is Some(_) { - match arg.num_args { - Some(range) => - if !(range.lower == 1 && range.upper is Some(1)) { - raise ArgBuildError::Unsupported( - "indexed positional '\{arg.name}' cannot set num_args unless it is the last positional or exactly 1..1", - ) - } - None => () + guard arg.info is Positional(index~, num_args~, ..) + if index is Some(_) && num_args is Some(range) { + if !(range is { lower: 1, upper: Some(1) }) { + raise ArgBuildError::Unsupported( + "indexed positional '\{arg.name}' cannot set num_args unless it is the last positional or exactly 1..1", + ) } } idx = idx + 1 @@ -150,18 +167,9 @@ fn validate_flag_arg( ctx : ValidationCtx, ) -> Unit raise ArgBuildError { validate_named_option_arg(arg) - if arg.index is Some(_) || arg.last { - raise ArgBuildError::Unsupported( - "positional-only settings require no short/long", - ) - } - if arg.num_args is Some(_) { - raise ArgBuildError::Unsupported( - "min/max values require value-taking arguments", - ) - } - if arg.flag_action is (Help | Version) { - guard !arg.negatable else { + guard arg.info is Flag(action~, negatable~, ..) + if action is (Help | Version) { + guard !negatable else { raise ArgBuildError::Unsupported( "help/version actions do not support negatable", ) @@ -177,11 +185,6 @@ fn validate_flag_arg( ) } } - if arg.default_values is Some(_) { - raise ArgBuildError::Unsupported( - "default values require value-taking arguments", - ) - } ctx.record_arg(arg) } @@ -191,19 +194,6 @@ fn validate_option_arg( ctx : ValidationCtx, ) -> Unit raise ArgBuildError { validate_named_option_arg(arg) - if arg.index is Some(_) || arg.last { - raise ArgBuildError::Unsupported( - "positional-only settings require no short/long", - ) - } - guard !arg.negatable else { - raise ArgBuildError::Unsupported("negatable is only supported for flags") - } - guard arg.num_args is None else { - raise ArgBuildError::Unsupported( - "min/max values require value-taking arguments", - ) - } validate_default_values(arg) ctx.record_arg(arg) } @@ -213,14 +203,6 @@ fn validate_positional_arg( arg : Arg, ctx : ValidationCtx, ) -> Unit raise ArgBuildError { - if arg.long is Some(_) || arg.short is Some(_) { - raise ArgBuildError::Unsupported( - "positional args do not support short/long", - ) - } - guard !arg.negatable else { - raise ArgBuildError::Unsupported("negatable is only supported for flags") - } let (min, max) = arg_min_max_for_validate(arg) if (min > 1 || (max is Some(m) && m > 1)) && !arg.multiple { raise ArgBuildError::Unsupported( @@ -233,14 +215,16 @@ fn validate_positional_arg( ///| fn validate_named_option_arg(arg : Arg) -> Unit raise ArgBuildError { - guard arg.long is Some(_) || arg.short is Some(_) else { + guard arg.info is (Flag(long~, short~, ..) | Option(long~, short~, ..)) + guard long is Some(_) || short is Some(_) else { raise ArgBuildError::Unsupported("flag/option args require short/long") } } ///| fn validate_default_values(arg : Arg) -> Unit raise ArgBuildError { - if arg.default_values is Some(values) && + if arg.info is (Option(default_values~, ..) | Positional(default_values~, ..)) && + default_values is Some(values) && values.length() > 1 && !arg.multiple && arg_action(arg) != ArgAction::Append { @@ -544,5 +528,5 @@ fn validate_relationships( ///| fn is_positional_arg(arg : Arg) -> Bool { - arg.is_positional + arg.info is Positional(_) } diff --git a/argparse/parser_values.mbt b/argparse/parser_values.mbt index 1e6964bfd..b27562535 100644 --- a/argparse/parser_values.mbt +++ b/argparse/parser_values.mbt @@ -142,13 +142,17 @@ fn assign_value( ///| fn option_conflict_label(arg : Arg) -> String { - match arg.long { - Some(name) => "--\{name}" - None => - match arg.short { - Some(short) => "-\{short}" - None => arg.name + match arg.info { + Flag(long~, short~, ..) | Option(long~, short~, ..) => + match long { + Some(name) => "--\{name}" + None => + match short { + Some(short) => "-\{short}" + None => arg.name + } } + Positional(_) => arg.name } } @@ -177,7 +181,9 @@ fn should_stop_option_value( if !value.has_prefix("-") || value == "-" { return false } - if arg.allow_hyphen_values { + if arg.info + is (Option(allow_hyphen_values~, ..) | Positional(allow_hyphen_values~, ..)) && + allow_hyphen_values { // Rust clap parity: // - `clap_builder/src/parser/parser.rs`: `parse_long_arg` / `parse_short_arg` // return `ParseResult::MaybeHyphenValue` when the pending arg in @@ -247,13 +253,15 @@ fn apply_env( ///| fn apply_defaults(matches : Matches, args : Array[Arg]) -> Unit { for arg in args { - if !arg_takes_value(arg) { - continue + let default_values = match arg.info { + Flag(_) => continue + Option(default_values~, ..) | Positional(default_values~, ..) => + default_values } if matches_has_value_or_flag(matches, arg.name) { continue } - match arg.default_values { + match default_values { Some(values) if values.length() > 0 => for value in values { let _ = add_value(matches, arg.name, value, arg, ValueSource::Default) From 1e67004ea9670e5b16d916cf6c7575bbe7bf53ea Mon Sep 17 00:00:00 2001 From: flycloudc Date: Wed, 25 Feb 2026 20:06:44 +0800 Subject: [PATCH 08/40] refactor: direct checks on Arg info --- argparse/arg_action.mbt | 5 ----- argparse/help_render.mbt | 16 +++++++--------- argparse/parser.mbt | 4 ++-- argparse/parser_globals_merge.mbt | 10 +++++----- argparse/parser_validate.mbt | 11 ++--------- argparse/parser_values.mbt | 2 +- 6 files changed, 17 insertions(+), 31 deletions(-) diff --git a/argparse/arg_action.mbt b/argparse/arg_action.mbt index 4cafaac22..cf05fc009 100644 --- a/argparse/arg_action.mbt +++ b/argparse/arg_action.mbt @@ -24,11 +24,6 @@ priv enum ArgAction { Version } derive(Eq) -///| -fn arg_takes_value(arg : Arg) -> Bool { - !(arg.info is Flag(_)) -} - ///| fn arg_action(arg : Arg) -> ArgAction { match arg.info { diff --git a/argparse/help_render.mbt b/argparse/help_render.mbt index 2b00decc4..4de1c581d 100644 --- a/argparse/help_render.mbt +++ b/argparse/help_render.mbt @@ -131,14 +131,14 @@ fn option_entries(cmd : Command) -> Array[String] { display.push((label, "Show version information.")) } for arg in args { - if !(arg.info is (Option(long~, short~, ..) | Flag(long~, short~, ..)) && - (long is Some(_) || short is Some(_))) { + guard arg.info is (Option(long~, short~, ..) | Flag(long~, short~, ..)) && + (long is Some(_) || short is Some(_)) else { continue } if arg.hidden { continue } - let name = if arg_takes_value(arg) { + let name = if arg.info is Option(_) { "\{arg_display(arg)} <\{arg.name}>" } else { arg_display(arg) @@ -396,11 +396,9 @@ fn group_members(cmd : Command, group : ArgGroup) -> String { ///| fn group_member_display(arg : Arg) -> String { let base = arg_display(arg) - if is_positional_arg(arg) { - base - } else if arg_takes_value(arg) { - "\{base} <\{arg.name}>" - } else { - base + match arg.info { + Flag(_) => base + Option(_) => "\{base} <\{arg.name}>" + Positional(_) => base } } diff --git a/argparse/parser.mbt b/argparse/parser.mbt index 3165402ef..dabae9033 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -216,7 +216,7 @@ fn parse_command( raise_unknown_long(name, long_index) } Some(spec) => - if arg_takes_value(spec) { + if spec.info is (Option(_) | Positional(_)) { check_duplicate_set_occurrence(matches, spec) if inline is Some(v) { assign_value(matches, spec, v, ValueSource::Argv) @@ -269,7 +269,7 @@ fn parse_command( Some(v) => v None => raise_unknown_short(short, short_index) } - if arg_takes_value(spec) { + if spec.info is (Option(_) | Positional(_)) { check_duplicate_set_occurrence(matches, spec) if pos + 1 < arg.length() { let rest = arg.unsafe_substring(start=pos + 1, end=arg.length()) diff --git a/argparse/parser_globals_merge.mbt b/argparse/parser_globals_merge.mbt index 0e7716231..fde80d7aa 100644 --- a/argparse/parser_globals_merge.mbt +++ b/argparse/parser_globals_merge.mbt @@ -196,10 +196,10 @@ fn merge_globals_from_child( if child_local_non_globals.contains(name) { continue } - if arg_takes_value(arg) { - merge_global_value_from_child(parent, child, arg, name) - } else { - merge_global_flag_from_child(parent, child, arg, name) + match arg.info { + Option(_) | Positional(_) => + merge_global_value_from_child(parent, child, arg, name) + Flag(_) => merge_global_flag_from_child(parent, child, arg, name) } } } @@ -216,7 +216,7 @@ fn propagate_globals_to_child( if child_local_non_globals.contains(name) { continue } - if arg_takes_value(arg) { + if arg.info is (Option(_) | Positional(_)) { match parent.values.get(name) { Some(values) => { child.values[name] = values.copy() diff --git a/argparse/parser_validate.mbt b/argparse/parser_validate.mbt index 6ce89ac14..88b5c7925 100644 --- a/argparse/parser_validate.mbt +++ b/argparse/parser_validate.mbt @@ -474,11 +474,9 @@ fn validate_values( if arg.required && !present { raise ArgParseError::MissingRequired(arg.name) } - if !arg_takes_value(arg) { - continue - } + guard arg.info is (Option(_) | Positional(_)) else { continue } if !present { - if is_positional_arg(arg) { + if arg.info is Positional(_) { let (min, _) = arg_min_max(arg) if min > 0 { raise ArgParseError::TooFewValues(arg.name, 0, min) @@ -525,8 +523,3 @@ fn validate_relationships( } } } - -///| -fn is_positional_arg(arg : Arg) -> Bool { - arg.info is Positional(_) -} diff --git a/argparse/parser_values.mbt b/argparse/parser_values.mbt index b27562535..cfc93ad3c 100644 --- a/argparse/parser_values.mbt +++ b/argparse/parser_values.mbt @@ -217,7 +217,7 @@ fn apply_env( Some(v) => v None => continue } - if arg_takes_value(arg) { + if arg.info is (Option(_) | Positional(_)) { assign_value(matches, arg, value, ValueSource::Env) continue } From 6cc3681f0cfa0ef7bd22f17f9a0c3ce76fa8dcf2 Mon Sep 17 00:00:00 2001 From: flycloudc Date: Wed, 25 Feb 2026 20:28:40 +0800 Subject: [PATCH 09/40] refactor: replace arg_action usage with direct checks on Arg info for improved clarity --- argparse/arg_action.mbt | 32 -------------------- argparse/parser.mbt | 49 +++++++++++++----------------- argparse/parser_globals_merge.mbt | 6 ++-- argparse/parser_validate.mbt | 6 ++-- argparse/parser_values.mbt | 50 +++++++++++++++---------------- 5 files changed, 50 insertions(+), 93 deletions(-) diff --git a/argparse/arg_action.mbt b/argparse/arg_action.mbt index cf05fc009..5246b52a0 100644 --- a/argparse/arg_action.mbt +++ b/argparse/arg_action.mbt @@ -12,38 +12,6 @@ // 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_action(arg : Arg) -> ArgAction { - match arg.info { - Flag(action~, ..) => - match action { - FlagAction::SetTrue => ArgAction::SetTrue - FlagAction::SetFalse => ArgAction::SetFalse - FlagAction::Count => ArgAction::Count - FlagAction::Help => ArgAction::Help - FlagAction::Version => ArgAction::Version - } - Option(action~, ..) => - match action { - OptionAction::Set => ArgAction::Set - OptionAction::Append => ArgAction::Append - } - Positional(_) => ArgAction::Set - } -} - ///| fn arg_min_max_for_validate(arg : Arg) -> (Int, Int?) raise ArgBuildError { if arg.info is Positional(num_args=Some(range), ..) { diff --git a/argparse/parser.mbt b/argparse/parser.mbt index dabae9033..f1f974900 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -187,31 +187,21 @@ fn parse_command( match long_index.get(name) { None => // Support `--no-` when the underlying flag is marked `negatable`. - if name.has_prefix("no-") { - let target = match name.strip_prefix("no-") { - Some(view) => view.to_string() - None => "" + if name is [.. "no-", .. target] && + long_index.get(target.to_string()) + is Some({ info: Flag(negatable=true, action~, ..), .. } as spec) { + if inline is Some(_) { + raise ArgParseError::InvalidArgument(arg) } - match long_index.get(target) { - None => raise_unknown_long(name, long_index) - Some(spec) => { - if !(spec.info is Flag(negatable=true, ..)) { - raise_unknown_long(name, long_index) - } - if inline is Some(_) { - raise ArgParseError::InvalidArgument(arg) - } - let value = match arg_action(spec) { - ArgAction::SetFalse => true - _ => false - } - if arg_action(spec) == ArgAction::Count { - matches.counts[spec.name] = 0 - } - matches.flags[spec.name] = value - matches.flag_sources[spec.name] = ValueSource::Argv - } + let value = match action { + SetFalse => true + _ => false + } + if action is Count { + matches.counts[spec.name] = 0 } + matches.flags[spec.name] = value + matches.flag_sources[spec.name] = ValueSource::Argv } else { raise_unknown_long(name, long_index) } @@ -239,9 +229,10 @@ fn parse_command( if inline is Some(_) { raise ArgParseError::InvalidArgument(arg) } - match arg_action(spec) { - ArgAction::Help => raise_context_help(cmd, inherited_globals) - ArgAction::Version => + match spec.info { + Flag(action=Help, ..) => + raise_context_help(cmd, inherited_globals) + Flag(action=Version, ..) => raise_version( version_text_for_long_action( cmd, name, inherited_version_long, @@ -295,9 +286,9 @@ fn parse_command( } break } else { - match arg_action(spec) { - ArgAction::Help => raise_context_help(cmd, inherited_globals) - ArgAction::Version => + match spec.info { + Flag(action=Help, ..) => raise_context_help(cmd, inherited_globals) + Flag(action=Version, ..) => raise_version( version_text_for_short_action( cmd, short, inherited_version_short, diff --git a/argparse/parser_globals_merge.mbt b/argparse/parser_globals_merge.mbt index fde80d7aa..f72b84339 100644 --- a/argparse/parser_globals_merge.mbt +++ b/argparse/parser_globals_merge.mbt @@ -69,7 +69,7 @@ fn merge_global_value_from_child( if !has_parent && !has_child { return } - if arg.multiple || arg_action(arg) == ArgAction::Append { + if arg.multiple || arg.info is Option(action=Append, ..) { let both_argv = parent_source is Some(ValueSource::Argv) && child_source is Some(ValueSource::Argv) if both_argv { @@ -137,7 +137,7 @@ fn merge_global_flag_from_child( ) -> Unit { match child.flags.get(name) { Some(v) => - if arg_action(arg) == ArgAction::Count { + if arg.info is Flag(action=Count, ..) { let has_parent = parent.flags.get(name) is Some(_) let parent_source = parent.flag_sources.get(name) let child_source = child.flag_sources.get(name) @@ -235,7 +235,7 @@ fn propagate_globals_to_child( Some(src) => child.flag_sources[name] = src None => () } - if arg_action(arg) == ArgAction::Count { + if arg.info is Flag(action=Count, ..) { match parent.counts.get(name) { Some(c) => child.counts[name] = c None => () diff --git a/argparse/parser_validate.mbt b/argparse/parser_validate.mbt index 88b5c7925..b3985f7f3 100644 --- a/argparse/parser_validate.mbt +++ b/argparse/parser_validate.mbt @@ -227,7 +227,7 @@ fn validate_default_values(arg : Arg) -> Unit raise ArgBuildError { default_values is Some(values) && values.length() > 1 && !arg.multiple && - arg_action(arg) != ArgAction::Append { + !(arg.info is Option(action=Append, ..)) { raise ArgBuildError::Unsupported( "default_values with multiple entries require action=Append", ) @@ -370,7 +370,7 @@ fn validate_help_subcommand(cmd : Command) -> Unit raise ArgBuildError { ///| fn validate_version_actions(cmd : Command) -> Unit raise ArgBuildError { if cmd.version is None && - cmd.args.any(arg => arg_action(arg) is ArgAction::Version) { + cmd.args.any(arg => arg.info is Flag(action=Version, ..)) { raise ArgBuildError::Unsupported( "version action requires command version text", ) @@ -490,7 +490,7 @@ fn validate_values( if count < min { raise ArgParseError::TooFewValues(arg.name, count, min) } - if arg_action(arg) != ArgAction::Append { + if !(arg.info is Option(action=Append, ..)) { match max { Some(max) if count > max => raise ArgParseError::TooManyValues(arg.name, count, max) diff --git a/argparse/parser_values.mbt b/argparse/parser_values.mbt index cfc93ad3c..e881947f9 100644 --- a/argparse/parser_values.mbt +++ b/argparse/parser_values.mbt @@ -94,7 +94,7 @@ fn add_value( arg : Arg, source : ValueSource, ) -> Unit { - if arg.multiple || arg_action(arg) == ArgAction::Append { + if arg.multiple || arg.info is Option(action=Append, ..) { let arr = matches.values.get(name).unwrap_or([]) arr.push(value) matches.values[name] = arr @@ -112,28 +112,30 @@ fn assign_value( value : String, source : ValueSource, ) -> Unit raise ArgParseError { - match arg_action(arg) { - ArgAction::Append => add_value(matches, arg.name, value, arg, source) - ArgAction::Set => add_value(matches, arg.name, value, arg, source) - ArgAction::SetTrue => { + match arg.info { + Option(action=Append, ..) => + add_value(matches, arg.name, value, arg, source) + Option(action=Set, ..) | Positional(_) => + add_value(matches, arg.name, value, arg, source) + Flag(action=SetTrue, ..) => { let flag = parse_bool(value) matches.flags[arg.name] = flag matches.flag_sources[arg.name] = source } - ArgAction::SetFalse => { + Flag(action=SetFalse, ..) => { let flag = parse_bool(value) matches.flags[arg.name] = !flag matches.flag_sources[arg.name] = source } - ArgAction::Count => { + Flag(action=Count, ..) => { let count = parse_count(value) matches.counts[arg.name] = count matches.flags[arg.name] = count > 0 matches.flag_sources[arg.name] = source } - ArgAction::Help => + Flag(action=Help, ..) => raise ArgParseError::InvalidArgument("help action does not take values") - ArgAction::Version => + Flag(action=Version, ..) => raise ArgParseError::InvalidArgument( "version action does not take values", ) @@ -161,9 +163,7 @@ fn check_duplicate_set_occurrence( matches : Matches, arg : Arg, ) -> Unit raise ArgParseError { - if arg_action(arg) != ArgAction::Set { - return - } + guard arg.info is (Option(action=Set, ..) | Positional(_)) else { return } if matches.values.get(arg.name) is Some(_) { raise ArgParseError::InvalidArgument( "argument '\{option_conflict_label(arg)}' cannot be used multiple times", @@ -221,31 +221,29 @@ fn apply_env( assign_value(matches, arg, value, ValueSource::Env) continue } - match arg_action(arg) { - ArgAction::Count => { + match arg.info { + Flag(action=Count, ..) => { let count = parse_count(value) matches.counts[name] = count matches.flags[name] = count > 0 matches.flag_sources[name] = ValueSource::Env } - ArgAction::SetFalse => { + Flag(action=SetFalse, ..) => { let flag = parse_bool(value) matches.flags[name] = !flag matches.flag_sources[name] = ValueSource::Env } - ArgAction::SetTrue => { + Flag(action=SetTrue, ..) => { let flag = parse_bool(value) matches.flags[name] = flag matches.flag_sources[name] = ValueSource::Env } - ArgAction::Set => { + Option(action=Set, ..) | Positional(_) => { let flag = parse_bool(value) matches.flags[name] = flag matches.flag_sources[name] = ValueSource::Env } - ArgAction::Append => () - ArgAction::Help => () - ArgAction::Version => () + Flag(action=Help | Version, ..) | Option(action=Append, ..) => () } } } @@ -278,16 +276,16 @@ fn matches_has_value_or_flag(matches : Matches, name : String) -> Bool { ///| fn apply_flag(matches : Matches, arg : Arg, source : ValueSource) -> Unit { - match arg_action(arg) { - ArgAction::SetTrue => matches.flags[arg.name] = true - ArgAction::SetFalse => matches.flags[arg.name] = false - ArgAction::Count => { + match arg.info { + Flag(action=SetTrue, ..) => matches.flags[arg.name] = true + Flag(action=SetFalse, ..) => matches.flags[arg.name] = false + Flag(action=Count, ..) => { let current = matches.counts.get(arg.name).unwrap_or(0) matches.counts[arg.name] = current + 1 matches.flags[arg.name] = true } - ArgAction::Help => () - ArgAction::Version => () + Flag(action=Help, ..) => () + Flag(action=Version, ..) => () _ => matches.flags[arg.name] = true } matches.flag_sources[arg.name] = source From 55ada3b90910225fc6d2813181355cdbfb142ac5 Mon Sep 17 00:00:00 2001 From: flycloudc Date: Thu, 26 Feb 2026 04:29:27 +0800 Subject: [PATCH 10/40] simplify --- argparse/arg_spec.mbt | 17 +---------------- argparse/command.mbt | 30 +++++++++++------------------- argparse/parser_lookup.mbt | 6 +----- 3 files changed, 13 insertions(+), 40 deletions(-) diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt index b30c0ca45..a9ff5baa0 100644 --- a/argparse/arg_spec.mbt +++ b/argparse/arg_spec.mbt @@ -239,7 +239,7 @@ pub fn OptionArg::new( // index: None, // num_args: None, // - multiple: allows_multiple_values(action), + multiple: action is Append, }, } } @@ -341,21 +341,6 @@ fn arg_name(arg : Arg) -> String { arg.name } -///| -fn is_flag_spec(arg : Arg) -> Bool { - arg.info is Flag(_) -} - -///| -fn is_count_flag_spec(arg : Arg) -> Bool { - arg.info is Flag(action=Count, ..) -} - -///| -fn allows_multiple_values(action : OptionAction) -> Bool { - action == OptionAction::Append -} - ///| fn range_allows_multiple(range : ValueRange?) -> Bool { match range { diff --git a/argparse/command.mbt b/argparse/command.mbt index d7314d66a..311b3ef75 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -131,34 +131,26 @@ fn build_matches( for spec in specs { let name = arg_name(spec) - match raw.values.get(name) { - Some(vs) => values[name] = vs.copy() - None => () + if raw.values.get(name) is Some(vs) { + values[name] = vs.copy() } - let count = raw.counts.get(name).unwrap_or(0) + let count = raw.counts.get_or_default(name, 0) if count > 0 { flag_counts[name] = count } let source = match raw.flag_sources.get(name) { Some(v) => Some(v) - None => - match raw.value_sources.get(name) { - Some(v) => Some(v) - None => None - } + None => raw.value_sources.get(name) } - match source { - Some(source) => { - sources[name] = source - if is_flag_spec(spec) { - if is_count_flag_spec(spec) { - flags[name] = count > 0 - } else { - flags[name] = raw.flags.get(name).unwrap_or(false) - } + if source is Some(source) { + sources[name] = source + if spec.info is Flag(action~, ..) { + if action is Count { + flags[name] = count > 0 + } else { + flags[name] = raw.flags.get(name).unwrap_or(false) } } - None => () } } let child_globals = inherited_globals + diff --git a/argparse/parser_lookup.mbt b/argparse/parser_lookup.mbt index adef1310d..2a96326aa 100644 --- a/argparse/parser_lookup.mbt +++ b/argparse/parser_lookup.mbt @@ -51,11 +51,7 @@ fn build_short_index(globals : Array[Arg], args : Array[Arg]) -> Map[Char, Arg] ///| fn collect_globals(args : Array[Arg]) -> Array[Arg] { - args.filter(arg => { - arg.global && - arg.info is (Flag(long~, short~, ..) | Option(long~, short~, ..)) && - (long is Some(_) || short is Some(_)) - }) + args.filter(arg => arg.global && arg.info is (Flag(_) | Option(_))) } ///| From 03195d3fa8fcb469613e0082aa9ddc43bdfac04c Mon Sep 17 00:00:00 2001 From: flycloudc Date: Thu, 26 Feb 2026 06:02:40 +0800 Subject: [PATCH 11/40] refactor: remove last field from OptionArg --- argparse/arg_spec.mbt | 12 ++++-------- argparse/argparse_blackbox_test.mbt | 1 - argparse/pkg.generated.mbti | 4 ++-- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt index a9ff5baa0..94c3e6f4a 100644 --- a/argparse/arg_spec.mbt +++ b/argparse/arg_spec.mbt @@ -53,15 +53,14 @@ priv enum ArgInfo { long~ : String?, action~ : OptionAction, default_values~ : Array[String]?, - allow_hyphen_values~ : Bool, - last~ : Bool + allow_hyphen_values~ : Bool ) Positional( index~ : Int?, num_args~ : ValueRange?, + last~ : Bool, default_values~ : Array[String]?, - allow_hyphen_values~ : Bool, - last~ : Bool + allow_hyphen_values~ : Bool ) } @@ -168,7 +167,6 @@ pub struct OptionArg { env? : String, default_values? : Array[String], allow_hyphen_values? : Bool, - last? : Bool, requires? : Array[String], conflicts_with? : Array[String], required? : Bool, @@ -204,7 +202,6 @@ pub fn OptionArg::new( env? : String, default_values? : Array[String], allow_hyphen_values? : Bool = false, - last? : Bool = false, requires? : Array[String] = [], conflicts_with? : Array[String] = [], required? : Bool = false, @@ -228,7 +225,6 @@ pub fn OptionArg::new( action~, default_values=default_values.map(Array::copy), allow_hyphen_values~, - last~, ), // // flag_action: FlagAction::SetTrue, @@ -319,9 +315,9 @@ pub fn PositionalArg::new( info: Positional( index~, num_args~, + last~, default_values=default_values.map(Array::copy), allow_hyphen_values~, - last~, ), // short: None, // long: None, diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 4cc59542b..298534dc6 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -110,7 +110,6 @@ test "render help conversion coverage snapshot" { default_values=["x", "y"], env="OPT_ENV", allow_hyphen_values=true, - last=true, required=true, global=true, hidden=true, diff --git a/argparse/pkg.generated.mbti b/argparse/pkg.generated.mbti index 53e30798a..3bb22bc48 100644 --- a/argparse/pkg.generated.mbti +++ b/argparse/pkg.generated.mbti @@ -88,9 +88,9 @@ pub impl Show for OptionAction pub struct OptionArg { // private fields - fn new(String, short? : Char, long? : String, about? : String, action? : OptionAction, env? : String, default_values? : Array[String], allow_hyphen_values? : Bool, last? : Bool, requires? : Array[String], conflicts_with? : Array[String], required? : Bool, global? : Bool, hidden? : Bool) -> OptionArg + fn new(String, short? : Char, long? : String, about? : String, action? : OptionAction, env? : String, default_values? : Array[String], allow_hyphen_values? : Bool, requires? : Array[String], conflicts_with? : Array[String], required? : Bool, global? : Bool, hidden? : Bool) -> OptionArg } -pub fn OptionArg::new(String, short? : Char, long? : String, about? : String, action? : OptionAction, env? : String, default_values? : Array[String], allow_hyphen_values? : Bool, last? : Bool, requires? : Array[String], conflicts_with? : Array[String], required? : Bool, global? : Bool, hidden? : Bool) -> Self +pub fn OptionArg::new(String, short? : Char, long? : String, about? : String, action? : OptionAction, env? : String, default_values? : Array[String], allow_hyphen_values? : Bool, requires? : Array[String], conflicts_with? : Array[String], required? : Bool, global? : Bool, hidden? : Bool) -> Self pub impl ArgLike for OptionArg pub struct PositionalArg { From ab87ca7f0ef36993d153fc529b0a7d147f676b86 Mon Sep 17 00:00:00 2001 From: zihang Date: Thu, 26 Feb 2026 14:15:54 +0800 Subject: [PATCH 12/40] chore: cleanup code --- argparse/arg_action.mbt | 10 +- argparse/arg_group.mbt | 2 +- argparse/arg_spec.mbt | 16 +- argparse/argparse_blackbox_test.mbt | 339 ++++++++++------------------ argparse/argparse_test.mbt | 40 ++-- argparse/command.mbt | 4 +- argparse/error.mbt | 24 +- argparse/matches.mbt | 2 +- argparse/moon.pkg | 2 + argparse/parser.mbt | 26 +-- argparse/parser_globals_merge.mbt | 17 +- argparse/parser_lookup.mbt | 4 +- argparse/parser_validate.mbt | 105 ++++----- argparse/parser_values.mbt | 38 ++-- argparse/value_range.mbt | 2 +- 15 files changed, 240 insertions(+), 391 deletions(-) diff --git a/argparse/arg_action.mbt b/argparse/arg_action.mbt index 5246b52a0..751eba4d4 100644 --- a/argparse/arg_action.mbt +++ b/argparse/arg_action.mbt @@ -16,19 +16,17 @@ fn arg_min_max_for_validate(arg : Arg) -> (Int, Int?) raise ArgBuildError { if arg.info is Positional(num_args=Some(range), ..) { if range.lower < 0 { - raise ArgBuildError::Unsupported("min values must be >= 0") + raise 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") + raise Unsupported("max values must be >= 0") } if max_value < range.lower { - raise ArgBuildError::Unsupported("max values must be >= min values") + raise Unsupported("max values must be >= min values") } if range.lower == 0 && max_value == 0 { - raise ArgBuildError::Unsupported( - "empty value range (0..0) is unsupported", - ) + raise Unsupported("empty value range (0..0) is unsupported") } } (range.lower, range.upper) diff --git a/argparse/arg_group.mbt b/argparse/arg_group.mbt index 5147ae5fd..82884b314 100644 --- a/argparse/arg_group.mbt +++ b/argparse/arg_group.mbt @@ -48,7 +48,7 @@ pub fn ArgGroup::new( requires? : Array[String] = [], conflicts_with? : Array[String] = [], ) -> ArgGroup { - ArgGroup::{ + { name, required, multiple, diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt index 94c3e6f4a..ea864c019 100644 --- a/argparse/arg_spec.mbt +++ b/argparse/arg_spec.mbt @@ -116,7 +116,7 @@ pub fn FlagArg::new( short? : Char, long? : String, about? : String, - action? : FlagAction = FlagAction::SetTrue, + action? : FlagAction = SetTrue, env? : String, requires? : Array[String] = [], conflicts_with? : Array[String] = [], @@ -125,8 +125,8 @@ pub fn FlagArg::new( negatable? : Bool = false, hidden? : Bool = false, ) -> FlagArg { - FlagArg::{ - arg: Arg::{ + { + arg: { name, about, env, @@ -198,7 +198,7 @@ pub fn OptionArg::new( short? : Char, long? : String, about? : String, - action? : OptionAction = OptionAction::Set, + action? : OptionAction = Set, env? : String, default_values? : Array[String], allow_hyphen_values? : Bool = false, @@ -208,8 +208,8 @@ pub fn OptionArg::new( global? : Bool = false, hidden? : Bool = false, ) -> OptionArg { - OptionArg::{ - arg: Arg::{ + { + arg: { name, about, env, @@ -301,8 +301,8 @@ pub fn PositionalArg::new( global? : Bool = false, hidden? : Bool = false, ) -> PositionalArg { - PositionalArg::{ - arg: Arg::{ + { + arg: { name, about, env, diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 298534dc6..ec8a8a2ce 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -22,8 +22,8 @@ impl @argparse.FromMatches for DecodeName with from_matches( matches : @argparse.Matches, ) { match matches.values.get("name") { - Some(values) if values.length() > 0 => DecodeName::{ name: values[0] } - _ => raise @argparse.ArgParseError::MissingRequired("name") + Some(values) if values.length() > 0 => { name: values[0] } + _ => raise MissingRequired("name") } } @@ -32,13 +32,13 @@ test "render help snapshot with groups and hidden entries" { let cmd = @argparse.Command( "render", groups=[ - @argparse.ArgGroup("mode", required=true, multiple=false, args=[ + ArgGroup("mode", required=true, multiple=false, args=[ "fast", "slow", "path", ]), ], subcommands=[ - @argparse.Command("run", about="run"), - @argparse.Command("hidden", about="hidden", hidden=true), + Command("run", about="run"), + Command("hidden", about="hidden", hidden=true), ], args=[ @argparse.FlagArg("fast", short='f', long="fast"), @@ -53,11 +53,7 @@ test "render help snapshot with groups and hidden entries" { required=true, ), @argparse.PositionalArg("target", index=0, required=true), - @argparse.PositionalArg( - "rest", - index=1, - num_args=@argparse.ValueRange(lower=0), - ), + @argparse.PositionalArg("rest", index=1, num_args=ValueRange(lower=0)), @argparse.PositionalArg("secret", index=2, hidden=true), ], ) @@ -91,7 +87,7 @@ test "render help snapshot with groups and hidden entries" { test "render help conversion coverage snapshot" { let cmd = @argparse.Command( "shape", - groups=[@argparse.ArgGroup("grp", args=["f", "opt", "pos"])], + groups=[ArgGroup("grp", args=["f", "opt", "pos"])], args=[ @argparse.FlagArg( "f", @@ -120,7 +116,7 @@ test "render help conversion coverage snapshot" { about="pos", env="POS_ENV", default_values=["p1", "p2"], - num_args=@argparse.ValueRange(lower=0, upper=2), + num_args=ValueRange(lower=0, upper=2), allow_hyphen_values=true, last=true, requires=["opt"], @@ -146,19 +142,14 @@ test "render help conversion coverage snapshot" { ///| test "count flags and sources with pattern matching" { let cmd = @argparse.Command("demo", args=[ - @argparse.FlagArg( - "verbose", - short='v', - long="verbose", - action=@argparse.FlagAction::Count, - ), + @argparse.FlagArg("verbose", short='v', long="verbose", action=Count), ]) let matches = cmd.parse(argv=["-v", "-v", "-v"], env=empty_env()) catch { _ => panic() } assert_true(matches.flags is { "verbose": true, .. }) assert_true(matches.flag_counts is { "verbose": 3, .. }) - assert_true(matches.sources is { "verbose": @argparse.ValueSource::Argv, .. }) + assert_true(matches.sources is { "verbose": Argv, .. }) } ///| @@ -171,7 +162,7 @@ test "global option merges parent and child values" { "profile", short='p', long="profile", - action=@argparse.OptionAction::Append, + action=Append, global=true, ), ], @@ -185,7 +176,7 @@ test "global option merges parent and child values" { _ => panic() } assert_true(matches.values is { "profile": ["parent", "child"], .. }) - assert_true(matches.sources is { "profile": @argparse.ValueSource::Argv, .. }) + assert_true(matches.sources is { "profile": Argv, .. }) assert_true( matches.subcommand is Some(("run", sub)) && sub.values is { "profile": ["parent", "child"], .. }, @@ -200,7 +191,7 @@ test "global requires is validated after parent-child merge" { @argparse.OptionArg("mode", long="mode", requires=["config"], global=true), @argparse.OptionArg("config", long="config", global=true), ], - subcommands=[@argparse.Command("run")], + subcommands=[Command("run")], ) let parsed = cmd.parse( @@ -225,7 +216,7 @@ test "global append keeps parent argv over child env/default" { @argparse.OptionArg( "profile", long="profile", - action=@argparse.OptionAction::Append, + action=Append, env="PROFILE", default_values=["def"], global=true, @@ -240,11 +231,11 @@ test "global append keeps parent argv over child env/default" { _ => panic() } assert_true(matches.values is { "profile": ["parent"], .. }) - assert_true(matches.sources is { "profile": @argparse.ValueSource::Argv, .. }) + assert_true(matches.sources is { "profile": Argv, .. }) assert_true( matches.subcommand is Some(("run", sub)) && sub.values is { "profile": ["parent"], .. } && - sub.sources is { "profile": @argparse.ValueSource::Argv, .. }, + sub.sources is { "profile": Argv, .. }, ) } @@ -271,11 +262,11 @@ test "global scalar keeps parent argv over child env/default" { _ => panic() } assert_true(matches.values is { "profile": ["parent"], .. }) - assert_true(matches.sources is { "profile": @argparse.ValueSource::Argv, .. }) + assert_true(matches.sources is { "profile": Argv, .. }) assert_true( matches.subcommand is Some(("run", sub)) && sub.values is { "profile": ["parent"], .. } && - sub.sources is { "profile": @argparse.ValueSource::Argv, .. }, + sub.sources is { "profile": Argv, .. }, ) } @@ -289,7 +280,7 @@ test "global count merges parent and child occurrences" { "verbose", short='v', long="verbose", - action=@argparse.FlagAction::Count, + action=Count, global=true, ), ], @@ -316,7 +307,7 @@ test "global count keeps parent argv over child env fallback" { "verbose", short='v', long="verbose", - action=@argparse.FlagAction::Count, + action=Count, env="VERBOSE", global=true, ), @@ -328,11 +319,11 @@ test "global count keeps parent argv over child env fallback" { _ => panic() } assert_true(matches.flag_counts is { "verbose": 1, .. }) - assert_true(matches.sources is { "verbose": @argparse.ValueSource::Argv, .. }) + assert_true(matches.sources is { "verbose": Argv, .. }) assert_true( matches.subcommand is Some(("run", sub)) && sub.flag_counts is { "verbose": 1, .. } && - sub.sources is { "verbose": @argparse.ValueSource::Argv, .. }, + sub.sources is { "verbose": Argv, .. }, ) } @@ -351,11 +342,11 @@ test "global flag keeps parent argv over child env fallback" { _ => panic() } assert_true(matches.flags is { "verbose": true, .. }) - assert_true(matches.sources is { "verbose": @argparse.ValueSource::Argv, .. }) + assert_true(matches.sources is { "verbose": Argv, .. }) assert_true( matches.subcommand is Some(("run", sub)) && sub.flags is { "verbose": true, .. } && - sub.sources is { "verbose": @argparse.ValueSource::Argv, .. }, + sub.sources is { "verbose": Argv, .. }, ) } @@ -364,7 +355,7 @@ test "subcommand cannot follow positional arguments" { let cmd = @argparse.Command( "demo", args=[@argparse.PositionalArg("input", index=0)], - subcommands=[@argparse.Command("run")], + subcommands=[Command("run")], ) try cmd.parse(argv=["raw", "run"], env=empty_env()) catch { @argparse.ArgParseError::InvalidArgument(msg) => @@ -390,7 +381,7 @@ test "global count source keeps env across subcommand merge" { "verbose", short='v', long="verbose", - action=@argparse.FlagAction::Count, + action=Count, env="VERBOSE", global=true, ), @@ -403,11 +394,11 @@ test "global count source keeps env across subcommand merge" { } assert_true(matches.flags is { "verbose": true, .. }) assert_true(matches.flag_counts is { "verbose": 1, .. }) - assert_true(matches.sources is { "verbose": @argparse.ValueSource::Env, .. }) + assert_true(matches.sources is { "verbose": Env, .. }) assert_true( matches.subcommand is Some(("run", sub)) && sub.flag_counts is { "verbose": 1, .. } && - sub.sources is { "verbose": @argparse.ValueSource::Env, .. }, + sub.sources is { "verbose": Env, .. }, ) } @@ -617,17 +608,13 @@ test "long and short value parsing branches" { ///| test "append option action is publicly selectable" { let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg( - "tag", - long="tag", - action=@argparse.OptionAction::Append, - ), + @argparse.OptionArg("tag", long="tag", action=Append), ]) let appended = cmd.parse(argv=["--tag", "a", "--tag", "b"], env=empty_env()) catch { _ => panic() } assert_true(appended.values is { "tag": ["a", "b"], .. }) - assert_true(appended.sources is { "tag": @argparse.ValueSource::Argv, .. }) + assert_true(appended.sources is { "tag": Argv, .. }) } ///| @@ -641,7 +628,7 @@ test "negation parsing and invalid negation forms" { _ => panic() } assert_true(off.flags is { "cache": false, .. }) - assert_true(off.sources is { "cache": @argparse.ValueSource::Argv, .. }) + assert_true(off.sources is { "cache": Argv, .. }) try cmd.parse(argv=["--no-path"], env=empty_env()) catch { @argparse.ArgParseError::UnknownArgument(arg, _) => @@ -668,12 +655,7 @@ test "negation parsing and invalid negation forms" { } let count_cmd = @argparse.Command("demo", args=[ - @argparse.FlagArg( - "verbose", - long="verbose", - action=@argparse.FlagAction::Count, - negatable=true, - ), + @argparse.FlagArg("verbose", long="verbose", action=Count, negatable=true), ]) let reset = count_cmd.parse( argv=["--verbose", "--no-verbose"], @@ -683,7 +665,7 @@ test "negation parsing and invalid negation forms" { } assert_true(reset.flags is { "verbose": false, .. }) assert_true(reset.flag_counts is { "verbose"? : None, .. }) - assert_true(reset.sources is { "verbose": @argparse.ValueSource::Argv, .. }) + assert_true(reset.sources is { "verbose": Argv, .. }) } ///| @@ -692,7 +674,7 @@ test "positionals force mode and dash handling" { @argparse.PositionalArg( "tail", index=0, - num_args=@argparse.ValueRange(lower=0), + num_args=ValueRange(lower=0), last=true, allow_hyphen_values=true, ), @@ -729,7 +711,7 @@ test "variadic positional keeps accepting hyphen values after first token" { @argparse.PositionalArg( "tail", index=0, - num_args=@argparse.ValueRange(lower=0), + num_args=ValueRange(lower=0), allow_hyphen_values=true, ), ]) @@ -742,10 +724,7 @@ test "variadic positional keeps accepting hyphen values after first token" { ///| test "bounded positional does not greedily consume later required values" { let cmd = @argparse.Command("demo", args=[ - @argparse.PositionalArg( - "first", - num_args=@argparse.ValueRange(lower=1, upper=2), - ), + @argparse.PositionalArg("first", num_args=ValueRange(lower=1, upper=2)), @argparse.PositionalArg("second", required=true), ]) @@ -782,7 +761,7 @@ test "empty positional value range is rejected at build time" { @argparse.PositionalArg( "skip", index=0, - num_args=@argparse.ValueRange(lower=0, upper=0), + num_args=ValueRange(lower=0, upper=0), ), @argparse.PositionalArg("name", index=1, required=true), ]).parse(argv=["alice"], env=empty_env()) @@ -798,24 +777,9 @@ test "empty positional value range is rejected at build time" { ///| test "env parsing for settrue setfalse count and invalid values" { let cmd = @argparse.Command("demo", args=[ - @argparse.FlagArg( - "on", - long="on", - action=@argparse.FlagAction::SetTrue, - env="ON", - ), - @argparse.FlagArg( - "off", - long="off", - action=@argparse.FlagAction::SetFalse, - env="OFF", - ), - @argparse.FlagArg( - "v", - long="v", - action=@argparse.FlagAction::Count, - env="V", - ), + @argparse.FlagArg("on", long="on", action=SetTrue, env="ON"), + @argparse.FlagArg("off", long="off", action=SetFalse, env="OFF"), + @argparse.FlagArg("v", long="v", action=Count, env="V"), ]) let parsed = cmd.parse(argv=[], env={ "ON": "true", "OFF": "true", "V": "3" }) catch { @@ -823,15 +787,7 @@ test "env parsing for settrue setfalse count and invalid values" { } assert_true(parsed.flags is { "on": true, "off": false, "v": true, .. }) assert_true(parsed.flag_counts is { "v": 3, .. }) - assert_true( - parsed.sources - is { - "on": @argparse.ValueSource::Env, - "off": @argparse.ValueSource::Env, - "v": @argparse.ValueSource::Env, - .. - }, - ) + assert_true(parsed.sources is { "on": Env, "off": Env, "v": Env, .. }) try cmd.parse(argv=[], env={ "ON": "bad" }) catch { @argparse.ArgParseError::InvalidValue(msg) => @@ -889,33 +845,19 @@ test "env parsing for settrue setfalse count and invalid values" { ///| test "defaults and value range helpers through public API" { let defaults = @argparse.Command("demo", args=[ - @argparse.OptionArg( - "mode", - long="mode", - action=@argparse.OptionAction::Append, - default_values=["a", "b"], - ), + @argparse.OptionArg("mode", long="mode", action=Append, default_values=[ + "a", "b", + ]), @argparse.OptionArg("one", long="one", default_values=["x"]), ]) let by_default = defaults.parse(argv=[], env=empty_env()) catch { _ => panic() } assert_true(by_default.values is { "mode": ["a", "b"], "one": ["x"], .. }) - assert_true( - by_default.sources - is { - "mode": @argparse.ValueSource::Default, - "one": @argparse.ValueSource::Default, - .. - }, - ) + assert_true(by_default.sources is { "mode": Default, "one": Default, .. }) let upper_only = @argparse.Command("demo", args=[ - @argparse.OptionArg( - "tag", - long="tag", - action=@argparse.OptionAction::Append, - ), + @argparse.OptionArg("tag", long="tag", action=Append), ]) let upper_parsed = upper_only.parse( argv=["--tag", "a", "--tag", "b", "--tag", "c"], @@ -958,7 +900,7 @@ test "options consume exactly one value per occurrence" { _ => panic() } assert_true(parsed.values is { "tag": ["a"], .. }) - assert_true(parsed.sources is { "tag": @argparse.ValueSource::Argv, .. }) + assert_true(parsed.sources is { "tag": Argv, .. }) try cmd.parse(argv=["--tag", "a", "b"], env=empty_env()) catch { @argparse.ArgParseError::TooManyPositionals => () @@ -1014,17 +956,13 @@ test "flag and option args require short or long names" { ///| test "append options collect values across repeated occurrences" { let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg( - "arg", - long="arg", - action=@argparse.OptionAction::Append, - ), + @argparse.OptionArg("arg", long="arg", action=Append), ]) let parsed = cmd.parse(argv=["--arg", "x", "--arg", "y"], env=empty_env()) catch { _ => panic() } assert_true(parsed.values is { "arg": ["x", "y"], .. }) - assert_true(parsed.sources is { "arg": @argparse.ValueSource::Argv, .. }) + assert_true(parsed.sources is { "arg": Argv, .. }) } ///| @@ -1104,7 +1042,7 @@ test "option values reject hyphen tokens unless allow_hyphen_values is enabled" _ => panic() } assert_true(parsed.values is { "pattern": ["-file"], .. }) - assert_true(parsed.sources is { "pattern": @argparse.ValueSource::Argv, .. }) + assert_true(parsed.sources is { "pattern": Argv, .. }) } ///| @@ -1126,7 +1064,7 @@ test "default argv path is reachable" { let cmd = @argparse.Command("demo", args=[ @argparse.PositionalArg( "rest", - num_args=@argparse.ValueRange(lower=0), + num_args=ValueRange(lower=0), allow_hyphen_values=true, ), ]) @@ -1136,9 +1074,10 @@ test "default argv path is reachable" { ///| test "validation branches exposed through parse" { try - @argparse.Command("demo", args=[ - @argparse.FlagArg("f", action=@argparse.FlagAction::Help), - ]).parse(argv=[], env=empty_env()) + @argparse.Command("demo", args=[@argparse.FlagArg("f", action=Help)]).parse( + argv=[], + env=empty_env(), + ) catch { @argparse.ArgBuildError::Unsupported(msg) => inspect( @@ -1154,12 +1093,7 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", args=[ - @argparse.FlagArg( - "f", - long="f", - action=@argparse.FlagAction::Help, - negatable=true, - ), + @argparse.FlagArg("f", long="f", action=Help, negatable=true), ]).parse(argv=[], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1176,12 +1110,7 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", args=[ - @argparse.FlagArg( - "f", - long="f", - action=@argparse.FlagAction::Help, - env="F", - ), + @argparse.FlagArg("f", long="f", action=Help, env="F"), ]).parse(argv=[], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1227,10 +1156,7 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", args=[ - @argparse.PositionalArg( - "x", - num_args=@argparse.ValueRange(lower=3, upper=2), - ), + @argparse.PositionalArg("x", num_args=ValueRange(lower=3, upper=2)), ]).parse(argv=[], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1247,10 +1173,7 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", args=[ - @argparse.PositionalArg( - "x", - num_args=@argparse.ValueRange(lower=-1, upper=2), - ), + @argparse.PositionalArg("x", num_args=ValueRange(lower=-1, upper=2)), ]).parse(argv=[], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1267,10 +1190,7 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", args=[ - @argparse.PositionalArg( - "x", - num_args=@argparse.ValueRange(lower=0, upper=-1), - ), + @argparse.PositionalArg("x", num_args=ValueRange(lower=0, upper=-1)), ]).parse(argv=[], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1290,7 +1210,7 @@ test "validation branches exposed through parse" { @argparse.PositionalArg( "x", index=0, - num_args=@argparse.ValueRange(lower=0, upper=2), + num_args=ValueRange(lower=0, upper=2), ), @argparse.PositionalArg("y", index=1), ]).parse(argv=["a"], env=empty_env()) @@ -1326,10 +1246,10 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", groups=[ - @argparse.ArgGroup("g"), - @argparse.ArgGroup("g"), - ]).parse(argv=[], env=empty_env()) + @argparse.Command("demo", groups=[ArgGroup("g"), ArgGroup("g")]).parse( + argv=[], + env=empty_env(), + ) catch { @argparse.ArgBuildError::Unsupported(msg) => inspect( @@ -1344,7 +1264,7 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", groups=[@argparse.ArgGroup("g", requires=["g"])]).parse( + @argparse.Command("demo", groups=[ArgGroup("g", requires=["g"])]).parse( argv=[], env=empty_env(), ) @@ -1362,9 +1282,10 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", groups=[ - @argparse.ArgGroup("g", conflicts_with=["g"]), - ]).parse(argv=[], env=empty_env()) + @argparse.Command("demo", groups=[ArgGroup("g", conflicts_with=["g"])]).parse( + argv=[], + env=empty_env(), + ) catch { @argparse.ArgBuildError::Unsupported(msg) => inspect( @@ -1379,7 +1300,7 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", groups=[@argparse.ArgGroup("g", args=["missing"])]).parse( + @argparse.Command("demo", groups=[ArgGroup("g", args=["missing"])]).parse( argv=[], env=empty_env(), ) @@ -1503,10 +1424,10 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", subcommands=[ - @argparse.Command("x"), - @argparse.Command("x"), - ]).parse(argv=[], env=empty_env()) + @argparse.Command("demo", subcommands=[Command("x"), Command("x")]).parse( + argv=[], + env=empty_env(), + ) catch { @argparse.ArgBuildError::Unsupported(msg) => inspect( @@ -1539,7 +1460,7 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", subcommands=[@argparse.Command("help")]).parse( + @argparse.Command("demo", subcommands=[Command("help")]).parse( argv=[], env=empty_env(), ) @@ -1613,7 +1534,7 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", args=[ - @argparse.FlagArg("v", long="v", action=@argparse.FlagAction::Version), + @argparse.FlagArg("v", long="v", action=Version), ]).parse(argv=[], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1662,11 +1583,7 @@ test "builtin and custom help/version dispatch edge paths" { } let long_help = @argparse.Command("demo", args=[ - @argparse.FlagArg( - "assist", - long="assist", - action=@argparse.FlagAction::Help, - ), + @argparse.FlagArg("assist", long="assist", action=Help), ]) try long_help.parse(argv=["--assist"], env=empty_env()) catch { @argparse.DisplayHelp::Message(text) => @@ -1677,7 +1594,7 @@ test "builtin and custom help/version dispatch edge paths" { } let short_help = @argparse.Command("demo", args=[ - @argparse.FlagArg("assist", short='?', action=@argparse.FlagAction::Help), + @argparse.FlagArg("assist", short='?', action=Help), ]) try short_help.parse(argv=["-?"], env=empty_env()) catch { @argparse.DisplayHelp::Message(text) => @@ -1693,7 +1610,7 @@ test "subcommand lookup falls back to positional value" { let cmd = @argparse.Command( "demo", args=[@argparse.PositionalArg("input", index=0)], - subcommands=[@argparse.Command("run")], + subcommands=[Command("run")], ) let parsed = cmd.parse(argv=["raw"], env=empty_env()) catch { _ => panic() } assert_true(parsed.values is { "input": ["raw"], .. }) @@ -1703,9 +1620,10 @@ test "subcommand lookup falls back to positional value" { ///| test "group validation catches unknown requires target" { try - @argparse.Command("demo", groups=[ - @argparse.ArgGroup("g", requires=["missing"]), - ]).parse(argv=[], env=empty_env()) + @argparse.Command("demo", groups=[ArgGroup("g", requires=["missing"])]).parse( + argv=[], + env=empty_env(), + ) catch { @argparse.ArgBuildError::Unsupported(msg) => inspect(msg, content="unknown group requires target: g -> missing") @@ -1718,9 +1636,10 @@ test "group validation catches unknown requires target" { ///| test "group validation catches unknown conflicts_with target" { try - @argparse.Command("demo", groups=[ - @argparse.ArgGroup("g", conflicts_with=["missing"]), - ]).parse(argv=[], env=empty_env()) + @argparse.Command("demo", groups=[ArgGroup("g", conflicts_with=["missing"])]).parse( + argv=[], + env=empty_env(), + ) catch { @argparse.ArgBuildError::Unsupported(msg) => inspect(msg, content="unknown group conflicts_with target: g -> missing") @@ -1734,7 +1653,7 @@ test "group validation catches unknown conflicts_with target" { test "group requires/conflicts can target argument names" { let requires_cmd = @argparse.Command( "demo", - groups=[@argparse.ArgGroup("mode", args=["fast"], requires=["config"])], + groups=[ArgGroup("mode", args=["fast"], requires=["config"])], args=[ @argparse.FlagArg("fast", long="fast"), @argparse.OptionArg("config", long="config"), @@ -1761,9 +1680,7 @@ test "group requires/conflicts can target argument names" { let conflicts_cmd = @argparse.Command( "demo", - groups=[ - @argparse.ArgGroup("mode", args=["fast"], conflicts_with=["config"]), - ], + groups=[ArgGroup("mode", args=["fast"], conflicts_with=["config"])], args=[ @argparse.FlagArg("fast", long="fast"), @argparse.OptionArg("config", long="config"), @@ -1786,7 +1703,7 @@ test "group requires/conflicts can target argument names" { ///| test "group without members has no parse effect" { - let cmd = @argparse.Command("demo", groups=[@argparse.ArgGroup("known")], args=[ + let cmd = @argparse.Command("demo", groups=[ArgGroup("known")], args=[ @argparse.FlagArg("x", long="x"), ]) let parsed = cmd.parse(argv=["--x"], env=empty_env()) catch { _ => panic() } @@ -1829,10 +1746,7 @@ test "arg validation catches unknown conflicts_with target" { test "empty groups without presence do not fail" { let grouped_ok = @argparse.Command( "demo", - groups=[ - @argparse.ArgGroup("left", args=["l"]), - @argparse.ArgGroup("right", args=["r"]), - ], + groups=[ArgGroup("left", args=["l"]), ArgGroup("right", args=["r"])], args=[ @argparse.FlagArg("l", long="left"), @argparse.FlagArg("r", long="right"), @@ -1851,7 +1765,7 @@ test "help rendering edge paths stay stable" { "files", index=0, required=true, - num_args=@argparse.ValueRange(lower=1), + num_args=ValueRange(lower=1), ), ]) let required_help = required_many.render_help() @@ -1906,7 +1820,7 @@ test "help rendering edge paths stay stable" { assert_true(implicit_group_help.has_prefix("Usage: demo [item]")) let sub_visible = @argparse.Command("demo", disable_help_subcommand=true, subcommands=[ - @argparse.Command("run"), + Command("run"), ]) let sub_help = sub_visible.render_help() assert_true(sub_help.has_prefix("Usage: demo [command]")) @@ -2001,16 +1915,8 @@ test "short options require one value before next option token" { ///| test "version action dispatches on custom long and short flags" { let cmd = @argparse.Command("demo", version="2.0.0", args=[ - @argparse.FlagArg( - "show_long", - long="show-version", - action=@argparse.FlagAction::Version, - ), - @argparse.FlagArg( - "show_short", - short='S', - action=@argparse.FlagAction::Version, - ), + @argparse.FlagArg("show_long", long="show-version", action=Version), + @argparse.FlagArg("show_short", short='S', action=Version), ]) try cmd.parse(argv=["--show-version"], env=empty_env()) catch { @@ -2038,11 +1944,11 @@ test "global version action keeps parent version text in subcommand context" { "show_version", short='S', long="show-version", - action=@argparse.FlagAction::Version, + action=Version, global=true, ), ], - subcommands=[@argparse.Command("run")], + subcommands=[Command("run")], ) try cmd.parse(argv=["--show-version"], env=empty_env()) catch { @@ -2087,7 +1993,7 @@ test "required and env-fed ranged values validate after parsing" { _ => panic() } assert_true(env_value.values is { "pair": ["one"], .. }) - assert_true(env_value.sources is { "pair": @argparse.ValueSource::Env, .. }) + assert_true(env_value.sources is { "pair": Env, .. }) } ///| @@ -2096,7 +2002,7 @@ test "positionals honor explicit index sorting with last ranged positional" { @argparse.PositionalArg( "late", index=2, - num_args=@argparse.ValueRange(lower=2, upper=2), + num_args=ValueRange(lower=2, upper=2), ), @argparse.PositionalArg("first", index=0), @argparse.PositionalArg("mid", index=1), @@ -2116,7 +2022,7 @@ test "positional num_args lower bound rejects missing argv values" { @argparse.PositionalArg( "first", index=0, - num_args=@argparse.ValueRange(lower=2, upper=3), + num_args=ValueRange(lower=2, upper=3), ), ]) @@ -2135,10 +2041,7 @@ test "positional num_args lower bound rejects missing argv values" { ///| test "positional max clamp leaves trailing value for next positional" { let cmd = @argparse.Command("demo", args=[ - @argparse.PositionalArg( - "items", - num_args=@argparse.ValueRange(lower=0, upper=2), - ), + @argparse.PositionalArg("items", num_args=ValueRange(lower=0, upper=2)), @argparse.PositionalArg("tail"), ]) @@ -2188,7 +2091,7 @@ test "options with allow_hyphen_values accept option-like single values" { @argparse.PositionalArg( "rest", index=0, - num_args=@argparse.ValueRange(lower=0), + num_args=ValueRange(lower=0), allow_hyphen_values=true, ), ]) @@ -2270,17 +2173,13 @@ test "unknown short suggestion can be absent" { ///| test "setfalse flags apply false when present" { let cmd = @argparse.Command("demo", args=[ - @argparse.FlagArg( - "failfast", - long="failfast", - action=@argparse.FlagAction::SetFalse, - ), + @argparse.FlagArg("failfast", long="failfast", action=SetFalse), ]) let parsed = cmd.parse(argv=["--failfast"], env=empty_env()) catch { _ => panic() } assert_true(parsed.flags is { "failfast": false, .. }) - assert_true(parsed.sources is { "failfast": @argparse.ValueSource::Argv, .. }) + assert_true(parsed.sources is { "failfast": Argv, .. }) } ///| @@ -2308,16 +2207,16 @@ test "global value from child default is merged back to parent" { ), @argparse.OptionArg("unused", long="unused", global=true), ], - subcommands=[@argparse.Command("run")], + subcommands=[Command("run")], ) let parsed = cmd.parse(argv=["run"], env=empty_env()) catch { _ => panic() } assert_true(parsed.values is { "mode": ["safe"], "unused"? : None, .. }) - assert_true(parsed.sources is { "mode": @argparse.ValueSource::Default, .. }) + assert_true(parsed.sources is { "mode": Default, .. }) assert_true( parsed.subcommand is Some(("run", sub)) && sub.values is { "mode": ["safe"], .. } && - sub.sources is { "mode": @argparse.ValueSource::Default, .. }, + sub.sources is { "mode": Default, .. }, ) } @@ -2334,7 +2233,7 @@ test "child global arg with inherited global name updates parent global" { ), ], subcommands=[ - @argparse.Command("run", args=[ + Command("run", args=[ @argparse.OptionArg("mode", long="mode", global=true), ]), ], @@ -2344,11 +2243,11 @@ test "child global arg with inherited global name updates parent global" { _ => panic() } assert_true(parsed.values is { "mode": ["fast"], .. }) - assert_true(parsed.sources is { "mode": @argparse.ValueSource::Argv, .. }) + assert_true(parsed.sources is { "mode": Argv, .. }) assert_true( parsed.subcommand is Some(("run", sub)) && sub.values is { "mode": ["fast"], .. } && - sub.sources is { "mode": @argparse.ValueSource::Argv, .. }, + sub.sources is { "mode": Argv, .. }, ) } @@ -2367,7 +2266,7 @@ test "child local arg shadowing inherited global is rejected at build time" { ), ], subcommands=[ - @argparse.Command("run", args=[@argparse.OptionArg("mode", long="mode")]), + Command("run", args=[@argparse.OptionArg("mode", long="mode")]), ], ).parse(argv=["run"], env=empty_env()) catch { @@ -2387,23 +2286,23 @@ test "global append env value from child is merged back to parent" { @argparse.OptionArg( "tag", long="tag", - action=@argparse.OptionAction::Append, + action=Append, env="TAG", global=true, ), ], - subcommands=[@argparse.Command("run")], + subcommands=[Command("run")], ) let parsed = cmd.parse(argv=["run"], env={ "TAG": "env-tag" }) catch { _ => panic() } assert_true(parsed.values is { "tag": ["env-tag"], .. }) - assert_true(parsed.sources is { "tag": @argparse.ValueSource::Env, .. }) + assert_true(parsed.sources is { "tag": Env, .. }) assert_true( parsed.subcommand is Some(("run", sub)) && sub.values is { "tag": ["env-tag"], .. } && - sub.sources is { "tag": @argparse.ValueSource::Env, .. }, + sub.sources is { "tag": Env, .. }, ) } @@ -2412,17 +2311,17 @@ test "global flag set in child argv is merged back to parent" { let cmd = @argparse.Command( "demo", args=[@argparse.FlagArg("verbose", long="verbose", global=true)], - subcommands=[@argparse.Command("run")], + subcommands=[Command("run")], ) let parsed = cmd.parse(argv=["run", "--verbose"], env=empty_env()) catch { _ => panic() } assert_true(parsed.flags is { "verbose": true, .. }) - assert_true(parsed.sources is { "verbose": @argparse.ValueSource::Argv, .. }) + assert_true(parsed.sources is { "verbose": Argv, .. }) assert_true( parsed.subcommand is Some(("run", sub)) && sub.flags is { "verbose": true, .. } && - sub.sources is { "verbose": @argparse.ValueSource::Argv, .. }, + sub.sources is { "verbose": Argv, .. }, ) } diff --git a/argparse/argparse_test.mbt b/argparse/argparse_test.mbt index 63e82a27b..fca3e2dc9 100644 --- a/argparse/argparse_test.mbt +++ b/argparse/argparse_test.mbt @@ -99,11 +99,7 @@ test "relationships and num args" { } let appended = @argparse.Command("demo", args=[ - @argparse.OptionArg( - "tag", - long="tag", - action=@argparse.OptionAction::Append, - ), + @argparse.OptionArg("tag", long="tag", action=Append), ]).parse(argv=["--tag", "a", "--tag", "b", "--tag", "c"], env=empty_env()) catch { _ => panic() } @@ -115,9 +111,7 @@ test "arg groups required and multiple" { let cmd = @argparse.Command( "demo", groups=[ - @argparse.ArgGroup("mode", required=true, multiple=false, args=[ - "fast", "slow", - ]), + ArgGroup("mode", required=true, multiple=false, args=["fast", "slow"]), ], args=[ @argparse.FlagArg("fast", long="fast"), @@ -146,8 +140,8 @@ test "arg groups requires and conflicts" { let requires_cmd = @argparse.Command( "demo", groups=[ - @argparse.ArgGroup("mode", args=["fast"], requires=["output"]), - @argparse.ArgGroup("output", args=["json"]), + ArgGroup("mode", args=["fast"], requires=["output"]), + ArgGroup("output", args=["json"]), ], args=[ @argparse.FlagArg("fast", long="fast"), @@ -166,8 +160,8 @@ test "arg groups requires and conflicts" { let conflict_cmd = @argparse.Command( "demo", groups=[ - @argparse.ArgGroup("mode", args=["fast"], conflicts_with=["output"]), - @argparse.ArgGroup("output", args=["json"]), + ArgGroup("mode", args=["fast"], conflicts_with=["output"]), + ArgGroup("output", args=["json"]), ], args=[ @argparse.FlagArg("fast", long="fast"), @@ -222,7 +216,7 @@ test "full help snapshot" { ]), @argparse.PositionalArg("name", index=0, about="Target name"), ], - subcommands=[@argparse.Command("echo", about="Echo a message")], + subcommands=[Command("echo", about="Echo a message")], ) inspect( cmd.render_help(), @@ -255,19 +249,17 @@ test "value source precedence argv env default" { let from_default = cmd.parse(argv=[], env=empty_env()) catch { _ => panic() } assert_true(from_default.values is { "level": ["1"], .. }) - assert_true( - from_default.sources is { "level": @argparse.ValueSource::Default, .. }, - ) + assert_true(from_default.sources is { "level": Default, .. }) let from_env = cmd.parse(argv=[], env={ "LEVEL": "2" }) catch { _ => panic() } assert_true(from_env.values is { "level": ["2"], .. }) - assert_true(from_env.sources is { "level": @argparse.ValueSource::Env, .. }) + assert_true(from_env.sources is { "level": Env, .. }) let from_argv = cmd.parse(argv=["--level", "3"], env={ "LEVEL": "2" }) catch { _ => panic() } assert_true(from_argv.values is { "level": ["3"], .. }) - assert_true(from_argv.sources is { "level": @argparse.ValueSource::Argv, .. }) + assert_true(from_argv.sources is { "level": Argv, .. }) } ///| @@ -287,11 +279,7 @@ test "options and multiple values" { "demo", args=[ @argparse.OptionArg("count", short='c', long="count"), - @argparse.OptionArg( - "tag", - long="tag", - action=@argparse.OptionAction::Append, - ), + @argparse.OptionArg("tag", long="tag", action=Append), ], subcommands=[serve], ) @@ -324,7 +312,7 @@ test "negatable and conflicts" { @argparse.FlagArg( "failfast", long="failfast", - action=@argparse.FlagAction::SetFalse, + action=SetFalse, negatable=true, ), @argparse.FlagArg("verbose", long="verbose", conflicts_with=["quiet"]), @@ -335,7 +323,7 @@ test "negatable and conflicts" { _ => panic() } assert_true(no_cache.flags is { "cache": false, .. }) - assert_true(no_cache.sources is { "cache": @argparse.ValueSource::Argv, .. }) + assert_true(no_cache.sources is { "cache": Argv, .. }) let no_failfast = cmd.parse(argv=["--no-failfast"], env=empty_env()) catch { _ => panic() @@ -412,7 +400,7 @@ test "command policies" { } let sub_cmd = @argparse.Command("demo", subcommand_required=true, subcommands=[ - @argparse.Command("echo"), + Command("echo"), ]) assert_true(sub_cmd.render_help().has_prefix("Usage: demo ")) try sub_cmd.parse(argv=[], env=empty_env()) catch { diff --git a/argparse/command.mbt b/argparse/command.mbt index 311b3ef75..a71bcb340 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -168,7 +168,7 @@ fn build_matches( Some( ( name, - Matches::{ + { flags: {}, values: {}, flag_counts: {}, @@ -185,7 +185,7 @@ fn build_matches( None => None } - Matches::{ + { flags, values, flag_counts, diff --git a/argparse/error.mbt b/argparse/error.mbt index fcdf17ba4..af67cae31 100644 --- a/argparse/error.mbt +++ b/argparse/error.mbt @@ -35,34 +35,32 @@ pub impl Show for ArgParseError with output(self : ArgParseError, logger) { ///| fn ArgParseError::arg_parse_error_message(self : ArgParseError) -> String { match self { - ArgParseError::UnknownArgument(arg, Some(hint)) => + UnknownArgument(arg, Some(hint)) => ( $|error: unexpected argument '\{arg}' found $| $| tip: a similar argument exists: '\{hint}' ) - ArgParseError::UnknownArgument(arg, None) => - "error: unexpected argument '\{arg}' found" - ArgParseError::InvalidArgument(arg) => + UnknownArgument(arg, None) => "error: unexpected argument '\{arg}' found" + InvalidArgument(arg) => if arg.has_prefix("-") { "error: unexpected argument '\{arg}' found" } else { "error: \{arg}" } - ArgParseError::MissingValue(arg) => + MissingValue(arg) => "error: a value is required for '\{arg}' but none was supplied" - ArgParseError::MissingRequired(name) => + MissingRequired(name) => "error: the following required argument was not provided: '\{name}'" - ArgParseError::TooFewValues(name, got, min) => + TooFewValues(name, got, min) => "error: '\{name}' requires at least \{min} values but only \{got} were provided" - ArgParseError::TooManyValues(name, got, max) => + TooManyValues(name, got, max) => "error: '\{name}' allows at most \{max} values but \{got} were provided" - ArgParseError::TooManyPositionals => - "error: too many positional arguments were provided" - ArgParseError::InvalidValue(msg) => "error: \{msg}" - ArgParseError::MissingGroup(name) => + TooManyPositionals => "error: too many positional arguments were provided" + InvalidValue(msg) => "error: \{msg}" + MissingGroup(name) => "error: the following required argument group was not provided: '\{name}'" - ArgParseError::GroupConflict(name) => "error: group conflict \{name}" + GroupConflict(name) => "error: group conflict \{name}" } } diff --git a/argparse/matches.mbt b/argparse/matches.mbt index 886e87c6b..e64954f0a 100644 --- a/argparse/matches.mbt +++ b/argparse/matches.mbt @@ -36,7 +36,7 @@ pub struct Matches { ///| fn new_matches_parse_state() -> Matches { - Matches::{ + { flags: {}, values: {}, flag_counts: {}, diff --git a/argparse/moon.pkg b/argparse/moon.pkg index 101bb7edf..1a2c08f0e 100644 --- a/argparse/moon.pkg +++ b/argparse/moon.pkg @@ -4,3 +4,5 @@ import { "moonbitlang/core/strconv", "moonbitlang/core/set", } + +warnings = "+unnecessary_annotation" diff --git a/argparse/parser.mbt b/argparse/parser.mbt index f1f974900..6519b28d9 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -14,12 +14,12 @@ ///| fn raise_help(text : String) -> Unit raise DisplayHelp { - raise DisplayHelp::Message(text) + raise Message(text) } ///| fn raise_version(text : String) -> Unit raise DisplayVersion { - raise DisplayVersion::Message(text) + raise Message(text) } ///| @@ -28,7 +28,7 @@ fn[T] raise_unknown_long( long_index : Map[String, Arg], ) -> T raise ArgParseError { let hint = suggest_long(name, long_index) - raise ArgParseError::UnknownArgument("--\{name}", hint) + raise UnknownArgument("--\{name}", hint) } ///| @@ -37,12 +37,12 @@ fn[T] raise_unknown_short( short_index : Map[Char, Arg], ) -> T raise ArgParseError { let hint = suggest_short(short, short_index) - raise ArgParseError::UnknownArgument("-\{short}", hint) + raise UnknownArgument("-\{short}", hint) } ///| fn[T] raise_subcommand_conflict(name : String) -> T raise ArgParseError { - raise ArgParseError::InvalidArgument( + raise InvalidArgument( "subcommand '\{name}' cannot be used with positional arguments", ) } @@ -55,7 +55,7 @@ fn render_help_for_context( let help_cmd = if inherited_globals.length() == 0 { cmd } else { - Command::{ ..cmd, args: inherited_globals + cmd.args } + { ..cmd, args: inherited_globals + cmd.args } } render_help(help_cmd) } @@ -201,7 +201,7 @@ fn parse_command( matches.counts[spec.name] = 0 } matches.flags[spec.name] = value - matches.flag_sources[spec.name] = ValueSource::Argv + matches.flag_sources[spec.name] = Argv } else { raise_unknown_long(name, long_index) } @@ -209,7 +209,7 @@ fn parse_command( if spec.info is (Option(_) | Positional(_)) { check_duplicate_set_occurrence(matches, spec) if inline is Some(v) { - assign_value(matches, spec, v, ValueSource::Argv) + assign_value(matches, spec, v, Argv) } else { let can_take_next = i + 1 < argv.length() && !should_stop_option_value( @@ -220,7 +220,7 @@ fn parse_command( ) if can_take_next { i = i + 1 - assign_value(matches, spec, argv[i], ValueSource::Argv) + assign_value(matches, spec, argv[i], Argv) } else { raise ArgParseError::MissingValue("--\{name}") } @@ -238,7 +238,7 @@ fn parse_command( cmd, name, inherited_version_long, ), ) - _ => apply_flag(matches, spec, ValueSource::Argv) + _ => apply_flag(matches, spec, Argv) } } } @@ -268,7 +268,7 @@ fn parse_command( Some(view) => view.to_string() None => rest } - assign_value(matches, spec, inline, ValueSource::Argv) + assign_value(matches, spec, inline, Argv) } else { let can_take_next = i + 1 < argv.length() && !should_stop_option_value( @@ -279,7 +279,7 @@ fn parse_command( ) if can_take_next { i = i + 1 - assign_value(matches, spec, argv[i], ValueSource::Argv) + assign_value(matches, spec, argv[i], Argv) } else { raise ArgParseError::MissingValue("-\{short}") } @@ -294,7 +294,7 @@ fn parse_command( cmd, short, inherited_version_short, ), ) - _ => apply_flag(matches, spec, ValueSource::Argv) + _ => apply_flag(matches, spec, Argv) } } pos = pos + 1 diff --git a/argparse/parser_globals_merge.mbt b/argparse/parser_globals_merge.mbt index f72b84339..5091ad46d 100644 --- a/argparse/parser_globals_merge.mbt +++ b/argparse/parser_globals_merge.mbt @@ -15,9 +15,9 @@ ///| fn source_priority(source : ValueSource?) -> Int { match source { - Some(ValueSource::Argv) => 3 - Some(ValueSource::Env) => 2 - Some(ValueSource::Default) => 1 + Some(Argv) => 3 + Some(Env) => 2 + Some(Default) => 1 None => 0 } } @@ -34,7 +34,7 @@ fn prefer_child_source( } else if child_priority < parent_priority { false } else { - child_source is Some(ValueSource::Argv) + child_source is Some(Argv) } } @@ -70,8 +70,7 @@ fn merge_global_value_from_child( return } if arg.multiple || arg.info is Option(action=Append, ..) { - let both_argv = parent_source is Some(ValueSource::Argv) && - child_source is Some(ValueSource::Argv) + let both_argv = parent_source is Some(Argv) && child_source is Some(Argv) if both_argv { let merged = [] if parent_vals is Some(pv) { @@ -86,7 +85,7 @@ fn merge_global_value_from_child( } if merged.length() > 0 { parent.values[name] = merged - parent.value_sources[name] = ValueSource::Argv + parent.value_sources[name] = Argv } } else { let choose_child = has_child && @@ -141,8 +140,8 @@ fn merge_global_flag_from_child( let has_parent = parent.flags.get(name) is Some(_) let parent_source = parent.flag_sources.get(name) let child_source = child.flag_sources.get(name) - let both_argv = parent_source is Some(ValueSource::Argv) && - child_source is Some(ValueSource::Argv) + let both_argv = parent_source is Some(Argv) && + child_source is Some(Argv) if both_argv { let parent_count = parent.counts.get(name).unwrap_or(0) let child_count = child.counts.get(name).unwrap_or(0) diff --git a/argparse/parser_lookup.mbt b/argparse/parser_lookup.mbt index 2a96326aa..ca8082b08 100644 --- a/argparse/parser_lookup.mbt +++ b/argparse/parser_lookup.mbt @@ -83,10 +83,10 @@ fn resolve_help_target( let mut subs = cmd.subcommands for name in targets { if name.has_prefix("-") { - raise ArgParseError::InvalidArgument("unexpected help argument: \{name}") + raise InvalidArgument("unexpected help argument: \{name}") } guard subs.iter().find_first(sub => sub.name == name) is Some(sub) else { - raise ArgParseError::InvalidArgument("unknown subcommand: \{name}") + raise InvalidArgument("unknown subcommand: \{name}") } current_globals = current_globals + collect_globals(current.args) current = sub diff --git a/argparse/parser_validate.mbt b/argparse/parser_validate.mbt index b3985f7f3..9c1d94023 100644 --- a/argparse/parser_validate.mbt +++ b/argparse/parser_validate.mbt @@ -26,7 +26,7 @@ priv struct ValidationCtx { fn ValidationCtx::new( inherited_global_names? : @set.Set[String] = @set.new(), ) -> ValidationCtx { - ValidationCtx::{ + { inherited_global_names: inherited_global_names.copy(), seen_names: @set.new(), seen_long: @set.new(), @@ -42,10 +42,10 @@ fn ValidationCtx::record_arg( arg : Arg, ) -> Unit raise ArgBuildError { if !self.seen_names.add_and_check(arg.name) { - raise ArgBuildError::Unsupported("duplicate arg name: \{arg.name}") + raise Unsupported("duplicate arg name: \{arg.name}") } if !arg.global && self.inherited_global_names.contains(arg.name) { - raise ArgBuildError::Unsupported( + raise Unsupported( "arg '\{arg.name}' shadows an inherited global; rename the arg or mark it global", ) } @@ -82,7 +82,7 @@ fn ValidationCtx::record_arg( Positional(index~, ..) => if index is Some(index) && !self.seen_positional_indices.add_and_check(index) { - raise ArgBuildError::Unsupported("duplicate positional index: \{index}") + raise Unsupported("duplicate positional index: \{index}") } } self.args.push(arg) @@ -131,7 +131,7 @@ fn validate_inherited_global_shadowing( continue } if inherited_global_names.contains(arg.name) { - raise ArgBuildError::Unsupported( + raise Unsupported( "arg '\{arg.name}' shadows an inherited global; rename the arg or mark it global", ) } @@ -152,7 +152,7 @@ fn validate_indexed_positional_num_args( guard arg.info is Positional(index~, num_args~, ..) if index is Some(_) && num_args is Some(range) { if !(range is { lower: 1, upper: Some(1) }) { - raise ArgBuildError::Unsupported( + raise Unsupported( "indexed positional '\{arg.name}' cannot set num_args unless it is the last positional or exactly 1..1", ) } @@ -170,19 +170,13 @@ fn validate_flag_arg( guard arg.info is Flag(action~, negatable~, ..) if action is (Help | Version) { guard !negatable else { - raise ArgBuildError::Unsupported( - "help/version actions do not support negatable", - ) + raise Unsupported("help/version actions do not support negatable") } guard arg.env is None else { - raise ArgBuildError::Unsupported( - "help/version actions do not support env/defaults", - ) + raise Unsupported("help/version actions do not support env/defaults") } guard !arg.multiple else { - raise ArgBuildError::Unsupported( - "help/version actions do not support multiple values", - ) + raise Unsupported("help/version actions do not support multiple values") } } ctx.record_arg(arg) @@ -205,7 +199,7 @@ fn validate_positional_arg( ) -> Unit raise ArgBuildError { let (min, max) = arg_min_max_for_validate(arg) if (min > 1 || (max is Some(m) && m > 1)) && !arg.multiple { - raise ArgBuildError::Unsupported( + raise Unsupported( "multiple values require action=Append or num_args allowing >1", ) } @@ -217,7 +211,7 @@ fn validate_positional_arg( fn validate_named_option_arg(arg : Arg) -> Unit raise ArgBuildError { guard arg.info is (Flag(long~, short~, ..) | Option(long~, short~, ..)) guard long is Some(_) || short is Some(_) else { - raise ArgBuildError::Unsupported("flag/option args require short/long") + raise Unsupported("flag/option args require short/long") } } @@ -228,7 +222,7 @@ fn validate_default_values(arg : Arg) -> Unit raise ArgBuildError { values.length() > 1 && !arg.multiple && !(arg.info is Option(action=Append, ..)) { - raise ArgBuildError::Unsupported( + raise Unsupported( "default_values with multiple entries require action=Append", ) } @@ -246,30 +240,26 @@ fn validate_group_defs( } for group in groups { if !seen.add_and_check(group.name) { - raise ArgBuildError::Unsupported("duplicate group: \{group.name}") + raise Unsupported("duplicate group: \{group.name}") } } for group in groups { for required in group.requires { if required == group.name { - raise ArgBuildError::Unsupported( - "group cannot require itself: \{group.name}", - ) + raise Unsupported("group cannot require itself: \{group.name}") } if !seen.contains(required) && !arg_seen.contains(required) { - raise ArgBuildError::Unsupported( + raise Unsupported( "unknown group requires target: \{group.name} -> \{required}", ) } } for conflict in group.conflicts_with { if conflict == group.name { - raise ArgBuildError::Unsupported( - "group cannot conflict with itself: \{group.name}", - ) + raise Unsupported("group cannot conflict with itself: \{group.name}") } if !seen.contains(conflict) && !arg_seen.contains(conflict) { - raise ArgBuildError::Unsupported( + raise Unsupported( "unknown group conflicts_with target: \{group.name} -> \{conflict}", ) } @@ -292,9 +282,7 @@ fn validate_group_refs( for group in groups { for name in group.args { if !arg_index.contains(name) { - raise ArgBuildError::Unsupported( - "unknown group arg: \{group.name} -> \{name}", - ) + raise Unsupported("unknown group arg: \{group.name} -> \{name}") } } } @@ -308,24 +296,18 @@ fn validate_requires_conflicts_targets( for arg in args { for required in arg.requires { if required == arg.name { - raise ArgBuildError::Unsupported( - "arg cannot require itself: \{arg.name}", - ) + raise Unsupported("arg cannot require itself: \{arg.name}") } if !seen_names.contains(required) { - raise ArgBuildError::Unsupported( - "unknown requires target: \{arg.name} -> \{required}", - ) + raise Unsupported("unknown requires target: \{arg.name} -> \{required}") } } for conflict in arg.conflicts_with { if conflict == arg.name { - raise ArgBuildError::Unsupported( - "arg cannot conflict with itself: \{arg.name}", - ) + raise Unsupported("arg cannot conflict with itself: \{arg.name}") } if !seen_names.contains(conflict) { - raise ArgBuildError::Unsupported( + raise Unsupported( "unknown conflicts_with target: \{arg.name} -> \{conflict}", ) } @@ -341,7 +323,7 @@ fn validate_subcommand_defs(subs : Array[Command]) -> Unit raise ArgBuildError { let seen : @set.Set[String] = @set.new() for sub in subs { if !seen.add_and_check(sub.name) { - raise ArgBuildError::Unsupported("duplicate subcommand: \{sub.name}") + raise Unsupported("duplicate subcommand: \{sub.name}") } } } @@ -351,9 +333,7 @@ fn validate_subcommand_required_policy( cmd : Command, ) -> Unit raise ArgBuildError { if cmd.subcommand_required && cmd.subcommands.length() == 0 { - raise ArgBuildError::Unsupported( - "subcommand_required requires at least one subcommand", - ) + raise Unsupported("subcommand_required requires at least one subcommand") } } @@ -361,7 +341,7 @@ fn validate_subcommand_required_policy( fn validate_help_subcommand(cmd : Command) -> Unit raise ArgBuildError { if help_subcommand_enabled(cmd) && cmd.subcommands.any(cmd => cmd.name == "help") { - raise ArgBuildError::Unsupported( + raise Unsupported( "subcommand name reserved for built-in help: help (disable with disable_help_subcommand)", ) } @@ -371,9 +351,7 @@ fn validate_help_subcommand(cmd : Command) -> Unit raise ArgBuildError { fn validate_version_actions(cmd : Command) -> Unit raise ArgBuildError { if cmd.version is None && cmd.args.any(arg => arg.info is Flag(action=Version, ..)) { - raise ArgBuildError::Unsupported( - "version action requires command version text", - ) + raise Unsupported("version action requires command version text") } } @@ -385,7 +363,7 @@ fn validate_command_policies( if cmd.subcommand_required && cmd.subcommands.length() > 0 && matches.parsed_subcommand is None { - raise ArgParseError::MissingRequired("subcommand") + raise MissingRequired("subcommand") } } @@ -419,10 +397,10 @@ fn validate_groups( } group_presence[group.name] = count if group.required && count == 0 { - raise ArgParseError::MissingGroup(group.name) + raise MissingGroup(group.name) } if !group.multiple && count > 1 { - raise ArgParseError::GroupConflict(group.name) + raise GroupConflict(group.name) } } for group in groups { @@ -433,26 +411,22 @@ fn validate_groups( for required in group.requires { if group_seen.contains(required) { if group_presence.get(required).unwrap_or(0) == 0 { - raise ArgParseError::MissingGroup(required) + raise MissingGroup(required) } } else if arg_seen.contains(required) { if !matches_has_value_or_flag(matches, required) { - raise ArgParseError::MissingRequired(required) + raise MissingRequired(required) } } } for conflict in group.conflicts_with { if group_seen.contains(conflict) { if group_presence.get(conflict).unwrap_or(0) > 0 { - raise ArgParseError::GroupConflict( - "\{group.name} conflicts with \{conflict}", - ) + raise GroupConflict("\{group.name} conflicts with \{conflict}") } } else if arg_seen.contains(conflict) { if matches_has_value_or_flag(matches, conflict) { - raise ArgParseError::GroupConflict( - "\{group.name} conflicts with \{conflict}", - ) + raise GroupConflict("\{group.name} conflicts with \{conflict}") } } } @@ -472,14 +446,14 @@ fn validate_values( for arg in args { let present = matches_has_value_or_flag(matches, arg.name) if arg.required && !present { - raise ArgParseError::MissingRequired(arg.name) + raise MissingRequired(arg.name) } guard arg.info is (Option(_) | Positional(_)) else { continue } if !present { if arg.info is Positional(_) { let (min, _) = arg_min_max(arg) if min > 0 { - raise ArgParseError::TooFewValues(arg.name, 0, min) + raise TooFewValues(arg.name, 0, min) } } continue @@ -488,12 +462,11 @@ fn validate_values( let count = values.length() let (min, max) = arg_min_max(arg) if count < min { - raise ArgParseError::TooFewValues(arg.name, count, min) + raise TooFewValues(arg.name, count, min) } if !(arg.info is Option(action=Append, ..)) { match max { - Some(max) if count > max => - raise ArgParseError::TooManyValues(arg.name, count, max) + Some(max) if count > max => raise TooManyValues(arg.name, count, max) _ => () } } @@ -511,12 +484,12 @@ fn validate_relationships( } for required in arg.requires { if !matches_has_value_or_flag(matches, required) { - raise ArgParseError::MissingRequired(required) + raise MissingRequired(required) } } for conflict in arg.conflicts_with { if matches_has_value_or_flag(matches, conflict) { - raise ArgParseError::InvalidArgument( + raise InvalidArgument( "conflicting arguments: \{arg.name} and \{conflict}", ) } diff --git a/argparse/parser_values.mbt b/argparse/parser_values.mbt index e881947f9..320a65c9a 100644 --- a/argparse/parser_values.mbt +++ b/argparse/parser_values.mbt @@ -41,25 +41,19 @@ fn assign_positionals( } let mut taken = 0 while taken < take { - add_value( - matches, - arg.name, - values[cursor + taken], - arg, - ValueSource::Argv, - ) + add_value(matches, arg.name, values[cursor + taken], arg, Argv) taken = taken + 1 } cursor = cursor + taken continue } if remaining > 0 { - add_value(matches, arg.name, values[cursor], arg, ValueSource::Argv) + add_value(matches, arg.name, values[cursor], arg, Argv) cursor = cursor + 1 } } if cursor < values.length() { - raise ArgParseError::TooManyPositionals + raise TooManyPositionals } } @@ -134,11 +128,9 @@ fn assign_value( matches.flag_sources[arg.name] = source } Flag(action=Help, ..) => - raise ArgParseError::InvalidArgument("help action does not take values") + raise InvalidArgument("help action does not take values") Flag(action=Version, ..) => - raise ArgParseError::InvalidArgument( - "version action does not take values", - ) + raise InvalidArgument("version action does not take values") } } @@ -165,7 +157,7 @@ fn check_duplicate_set_occurrence( ) -> Unit raise ArgParseError { guard arg.info is (Option(action=Set, ..) | Positional(_)) else { return } if matches.values.get(arg.name) is Some(_) { - raise ArgParseError::InvalidArgument( + raise InvalidArgument( "argument '\{option_conflict_label(arg)}' cannot be used multiple times", ) } @@ -218,7 +210,7 @@ fn apply_env( None => continue } if arg.info is (Option(_) | Positional(_)) { - assign_value(matches, arg, value, ValueSource::Env) + assign_value(matches, arg, value, Env) continue } match arg.info { @@ -226,22 +218,22 @@ fn apply_env( let count = parse_count(value) matches.counts[name] = count matches.flags[name] = count > 0 - matches.flag_sources[name] = ValueSource::Env + matches.flag_sources[name] = Env } Flag(action=SetFalse, ..) => { let flag = parse_bool(value) matches.flags[name] = !flag - matches.flag_sources[name] = ValueSource::Env + matches.flag_sources[name] = Env } Flag(action=SetTrue, ..) => { let flag = parse_bool(value) matches.flags[name] = flag - matches.flag_sources[name] = ValueSource::Env + matches.flag_sources[name] = Env } Option(action=Set, ..) | Positional(_) => { let flag = parse_bool(value) matches.flags[name] = flag - matches.flag_sources[name] = ValueSource::Env + matches.flag_sources[name] = Env } Flag(action=Help | Version, ..) | Option(action=Append, ..) => () } @@ -262,7 +254,7 @@ fn apply_defaults(matches : Matches, args : Array[Arg]) -> Unit { match default_values { Some(values) if values.length() > 0 => for value in values { - let _ = add_value(matches, arg.name, value, arg, ValueSource::Default) + let _ = add_value(matches, arg.name, value, arg, Default) } _ => () } @@ -298,7 +290,7 @@ fn parse_bool(value : String) -> Bool raise ArgParseError { } else if value == "0" || value == "false" || value == "no" || value == "off" { false } else { - raise ArgParseError::InvalidValue( + raise InvalidValue( "invalid value '\{value}' for boolean flag; expected one of: 1, 0, true, false, yes, no, on, off", ) } @@ -308,12 +300,12 @@ fn parse_bool(value : String) -> Bool raise ArgParseError { fn parse_count(value : String) -> Int raise ArgParseError { try @strconv.parse_int(value) catch { _ => - raise ArgParseError::InvalidValue( + raise InvalidValue( "invalid value '\{value}' for count; expected a non-negative integer", ) } noraise { _..<0 => - raise ArgParseError::InvalidValue( + raise InvalidValue( "invalid value '\{value}' for count; expected a non-negative integer", ) v => v diff --git a/argparse/value_range.mbt b/argparse/value_range.mbt index 4fba32977..93965af45 100644 --- a/argparse/value_range.mbt +++ b/argparse/value_range.mbt @@ -35,5 +35,5 @@ pub fn ValueRange::single() -> ValueRange { /// - `ValueRange(lower=0)` means `0..`. /// - `ValueRange(lower=1, upper=3)` means `1..=3`. pub fn ValueRange::new(lower? : Int = 0, upper? : Int) -> ValueRange { - ValueRange::{ lower, upper } + { lower, upper } } From 9a052ca802519e26d0548f83beef133158d1a2ee Mon Sep 17 00:00:00 2001 From: zihang Date: Thu, 26 Feb 2026 14:21:06 +0800 Subject: [PATCH 13/40] fix: use `*view` types --- argparse/arg_group.mbt | 24 ++++----- argparse/arg_spec.mbt | 103 ++++++++++++++++++++---------------- argparse/command.mbt | 38 ++++++------- argparse/parser.mbt | 2 +- argparse/pkg.generated.mbti | 22 ++++---- 5 files changed, 100 insertions(+), 89 deletions(-) diff --git a/argparse/arg_group.mbt b/argparse/arg_group.mbt index 82884b314..2a505a7ea 100644 --- a/argparse/arg_group.mbt +++ b/argparse/arg_group.mbt @@ -24,12 +24,12 @@ pub struct ArgGroup { /// Create an argument group. fn new( - name : String, + name : StringView, required? : Bool, multiple? : Bool, - args? : Array[String], - requires? : Array[String], - conflicts_with? : Array[String], + args? : ArrayView[String], + requires? : ArrayView[String], + conflicts_with? : ArrayView[String], ) -> ArgGroup } @@ -41,19 +41,19 @@ pub struct ArgGroup { /// - `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, + name : StringView, required? : Bool = false, multiple? : Bool = true, - args? : Array[String] = [], - requires? : Array[String] = [], - conflicts_with? : Array[String] = [], + args? : ArrayView[String] = [], + requires? : ArrayView[String] = [], + conflicts_with? : ArrayView[String] = [], ) -> ArgGroup { { - name, + name: name.to_string(), required, multiple, - args: args.copy(), - requires: requires.copy(), - conflicts_with: conflicts_with.copy(), + args: args.to_array(), + requires: requires.to_array(), + conflicts_with: conflicts_with.to_array(), } } diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt index ea864c019..86b435e91 100644 --- a/argparse/arg_spec.mbt +++ b/argparse/arg_spec.mbt @@ -78,14 +78,14 @@ pub struct FlagArg { /// Create a flag argument. fn new( - name : String, + name : StringView, short? : Char, - long? : String, - about? : String, + long? : StringView, + about? : StringView, action? : FlagAction, - env? : String, - requires? : Array[String], - conflicts_with? : Array[String], + env? : StringView, + requires? : ArrayView[String], + conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, negatable? : Bool, @@ -112,19 +112,23 @@ pub impl ArgLike for FlagArg with validate(self, ctx) { /// /// If `negatable=true`, `--no-` is accepted for long flags. pub fn FlagArg::new( - name : String, + name : StringView, short? : Char, - long? : String, - about? : String, + long? : StringView, + about? : StringView, action? : FlagAction = SetTrue, - env? : String, - requires? : Array[String] = [], - conflicts_with? : Array[String] = [], + env? : StringView, + requires? : ArrayView[String] = [], + conflicts_with? : ArrayView[String] = [], required? : Bool = false, global? : Bool = false, negatable? : Bool = false, hidden? : Bool = false, ) -> FlagArg { + let name = name.to_string() + let long = long.map(v => v.to_string()) + let about = about.map(v => v.to_string()) + let env = env.map(v => v.to_string()) { arg: { name, @@ -132,8 +136,8 @@ pub fn FlagArg::new( env, global, hidden, - requires: requires.copy(), - conflicts_with: conflicts_with.copy(), + requires: requires.to_array(), + conflicts_with: conflicts_with.to_array(), required, // info: Flag(short~, long~, action~, negatable~), @@ -159,16 +163,16 @@ pub struct OptionArg { /// Create an option argument. fn new( - name : String, + name : StringView, short? : Char, - long? : String, - about? : String, + long? : StringView, + about? : StringView, action? : OptionAction, - env? : String, - default_values? : Array[String], + env? : StringView, + default_values? : ArrayView[String], allow_hyphen_values? : Bool, - requires? : Array[String], - conflicts_with? : Array[String], + requires? : ArrayView[String], + conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, hidden? : Bool, @@ -194,27 +198,31 @@ pub impl ArgLike for OptionArg with validate(self, ctx) { /// /// `global=true` makes the option available in subcommands. pub fn OptionArg::new( - name : String, + name : StringView, short? : Char, - long? : String, - about? : String, + long? : StringView, + about? : StringView, action? : OptionAction = Set, - env? : String, - default_values? : Array[String], + env? : StringView, + default_values? : ArrayView[String], allow_hyphen_values? : Bool = false, - requires? : Array[String] = [], - conflicts_with? : Array[String] = [], + requires? : ArrayView[String] = [], + conflicts_with? : ArrayView[String] = [], required? : Bool = false, global? : Bool = false, hidden? : Bool = false, ) -> OptionArg { + let name = name.to_string() + let long = long.map(v => v.to_string()) + let about = about.map(v => v.to_string()) + let env = env.map(v => v.to_string()) { arg: { name, about, env, - requires: requires.copy(), - conflicts_with: conflicts_with.copy(), + requires: requires.to_array(), + conflicts_with: conflicts_with.to_array(), required, global, hidden, @@ -223,7 +231,7 @@ pub fn OptionArg::new( short~, long~, action~, - default_values=default_values.map(Array::copy), + default_values=default_values.map(values => values.to_array()), allow_hyphen_values~, ), // @@ -247,16 +255,16 @@ pub struct PositionalArg { /// Create a positional argument. fn new( - name : String, + name : StringView, index? : Int, - about? : String, - env? : String, - default_values? : Array[String], + about? : StringView, + env? : StringView, + default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, - requires? : Array[String], - conflicts_with? : Array[String], + requires? : ArrayView[String], + conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, hidden? : Bool, @@ -287,27 +295,30 @@ pub impl ArgLike for PositionalArg with validate(self, ctx) { /// omitted or exactly `ValueRange::single()` (`1..1`); other ranges are rejected /// at build time. pub fn PositionalArg::new( - name : String, + name : StringView, index? : Int, - about? : String, - env? : String, - default_values? : Array[String], + about? : StringView, + env? : StringView, + default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool = false, last? : Bool = false, - requires? : Array[String] = [], - conflicts_with? : Array[String] = [], + requires? : ArrayView[String] = [], + conflicts_with? : ArrayView[String] = [], required? : Bool = false, global? : Bool = false, hidden? : Bool = false, ) -> PositionalArg { + let name = name.to_string() + let about = about.map(v => v.to_string()) + let env = env.map(v => v.to_string()) { arg: { name, about, env, - requires: requires.copy(), - conflicts_with: conflicts_with.copy(), + requires: requires.to_array(), + conflicts_with: conflicts_with.to_array(), required, global, hidden, @@ -316,7 +327,7 @@ pub fn PositionalArg::new( index~, num_args~, last~, - default_values=default_values.map(Array::copy), + default_values=default_values.map(values => values.to_array()), allow_hyphen_values~, ), // short: None, diff --git a/argparse/command.mbt b/argparse/command.mbt index a71bcb340..2314055e9 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -31,18 +31,18 @@ pub struct Command { /// Create a declarative command specification. fn new( - name : String, - args? : Array[&ArgLike], - subcommands? : Array[Command], - about? : String, - version? : String, + name : StringView, + args? : ArrayView[&ArgLike], + 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? : Array[ArgGroup], + groups? : ArrayView[ArgGroup], ) -> Command } @@ -54,28 +54,28 @@ pub struct Command { /// - `groups` explicitly declares all group memberships and policies. /// - Built-in `--help`/`--version` behavior can be disabled with the flags below. pub fn Command::new( - name : String, - args? : Array[&ArgLike] = [], - subcommands? : Array[Command] = [], - about? : String, - version? : String, + name : StringView, + args? : ArrayView[&ArgLike] = [], + subcommands? : ArrayView[Command] = [], + about? : StringView, + version? : StringView, disable_help_flag? : Bool = false, disable_version_flag? : Bool = false, disable_help_subcommand? : Bool = false, arg_required_else_help? : Bool = false, subcommand_required? : Bool = false, hidden? : Bool = false, - groups? : Array[ArgGroup] = [], + groups? : ArrayView[ArgGroup] = [], ) -> Command { let (parsed_args, arg_error) = collect_args(args) - let groups = groups.copy() + let groups = groups.to_array() let cmd = Command::{ - name, + name: name.to_string(), args: parsed_args, groups, - subcommands: subcommands.copy(), - about, - version, + subcommands: subcommands.to_array(), + about: about.map(v => v.to_string()), + version: version.map(v => v.to_string()), disable_help_flag, disable_version_flag, disable_help_subcommand, @@ -110,7 +110,7 @@ pub fn Command::render_help(self : Command) -> String { /// Value precedence is `argv > env > default_values`. pub fn Command::parse( self : Command, - argv? : Array[String] = default_argv(), + argv? : ArrayView[String] = default_argv(), env? : Map[String, String] = {}, ) -> Matches raise { let raw = parse_command(self, argv, env, [], {}, {}) @@ -209,7 +209,7 @@ fn find_decl_subcommand(subs : Array[Command], name : String) -> Command? { } ///| -fn collect_args(specs : Array[&ArgLike]) -> (Array[Arg], ArgBuildError?) { +fn collect_args(specs : ArrayView[&ArgLike]) -> (Array[Arg], ArgBuildError?) { let args = specs.map(spec => spec.to_arg()) let ctx = ValidationCtx::new() let mut first_error : ArgBuildError? = None diff --git a/argparse/parser.mbt b/argparse/parser.mbt index 6519b28d9..082214c81 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -81,7 +81,7 @@ fn default_argv() -> Array[String] { ///| fn parse_command( cmd : Command, - argv : Array[String], + argv : ArrayView[String], env : Map[String, String], inherited_globals : Array[Arg], inherited_version_long : Map[String, String], diff --git a/argparse/pkg.generated.mbti b/argparse/pkg.generated.mbti index 3bb22bc48..62f2c99a5 100644 --- a/argparse/pkg.generated.mbti +++ b/argparse/pkg.generated.mbti @@ -38,17 +38,17 @@ pub impl Show for DisplayVersion pub struct ArgGroup { // private fields - fn new(String, required? : Bool, multiple? : Bool, args? : Array[String], requires? : Array[String], conflicts_with? : Array[String]) -> ArgGroup + fn new(StringView, required? : Bool, multiple? : Bool, args? : ArrayView[String], requires? : ArrayView[String], conflicts_with? : ArrayView[String]) -> ArgGroup } -pub fn ArgGroup::new(String, required? : Bool, multiple? : Bool, args? : Array[String], requires? : Array[String], conflicts_with? : Array[String]) -> Self +pub fn ArgGroup::new(StringView, required? : Bool, multiple? : Bool, args? : ArrayView[String], requires? : ArrayView[String], conflicts_with? : ArrayView[String]) -> Self pub struct Command { // private fields - fn new(String, args? : Array[&ArgLike], subcommands? : Array[Command], about? : String, version? : String, disable_help_flag? : Bool, disable_version_flag? : Bool, disable_help_subcommand? : Bool, arg_required_else_help? : Bool, subcommand_required? : Bool, hidden? : Bool, groups? : Array[ArgGroup]) -> Command + fn new(StringView, args? : ArrayView[&ArgLike], 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]) -> Command } -pub fn Command::new(String, args? : Array[&ArgLike], subcommands? : Array[Self], about? : String, version? : String, disable_help_flag? : Bool, disable_version_flag? : Bool, disable_help_subcommand? : Bool, arg_required_else_help? : Bool, subcommand_required? : Bool, hidden? : Bool, groups? : Array[ArgGroup]) -> Self -pub fn Command::parse(Self, argv? : Array[String], env? : Map[String, String]) -> Matches raise +pub fn Command::new(StringView, args? : ArrayView[&ArgLike], subcommands? : ArrayView[Self], 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]) -> Self +pub fn Command::parse(Self, argv? : ArrayView[String], env? : Map[String, String]) -> Matches raise pub fn Command::render_help(Self) -> String pub(all) enum FlagAction { @@ -64,9 +64,9 @@ pub impl Show for FlagAction pub struct FlagArg { // private fields - fn new(String, short? : Char, long? : String, about? : String, action? : FlagAction, env? : String, requires? : Array[String], conflicts_with? : Array[String], required? : Bool, global? : Bool, negatable? : Bool, hidden? : Bool) -> FlagArg + fn new(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 } -pub fn FlagArg::new(String, short? : Char, long? : String, about? : String, action? : FlagAction, env? : String, requires? : Array[String], conflicts_with? : Array[String], required? : Bool, global? : Bool, negatable? : Bool, hidden? : Bool) -> Self +pub fn FlagArg::new(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) -> Self pub impl ArgLike for FlagArg pub struct Matches { @@ -88,17 +88,17 @@ pub impl Show for OptionAction pub struct OptionArg { // private fields - fn new(String, short? : Char, long? : String, about? : String, action? : OptionAction, env? : String, default_values? : Array[String], allow_hyphen_values? : Bool, requires? : Array[String], conflicts_with? : Array[String], required? : Bool, global? : Bool, hidden? : Bool) -> OptionArg + fn new(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 } -pub fn OptionArg::new(String, short? : Char, long? : String, about? : String, action? : OptionAction, env? : String, default_values? : Array[String], allow_hyphen_values? : Bool, requires? : Array[String], conflicts_with? : Array[String], required? : Bool, global? : Bool, hidden? : Bool) -> Self +pub fn OptionArg::new(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) -> Self pub impl ArgLike for OptionArg pub struct PositionalArg { // private fields - fn new(String, index? : Int, about? : String, env? : String, default_values? : Array[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : Array[String], conflicts_with? : Array[String], required? : Bool, global? : Bool, hidden? : Bool) -> PositionalArg + fn new(StringView, index? : Int, about? : StringView, env? : StringView, default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : ArrayView[String], conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, hidden? : Bool) -> PositionalArg } -pub fn PositionalArg::new(String, index? : Int, about? : String, env? : String, default_values? : Array[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : Array[String], conflicts_with? : Array[String], required? : Bool, global? : Bool, hidden? : Bool) -> Self +pub fn PositionalArg::new(StringView, index? : Int, about? : StringView, env? : StringView, default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : ArrayView[String], conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, hidden? : Bool) -> Self pub impl ArgLike for PositionalArg pub struct ValueRange { From 6dccaa195c5384258d99dda63bf211e1e3af3654 Mon Sep 17 00:00:00 2001 From: zihang Date: Thu, 26 Feb 2026 18:20:39 +0800 Subject: [PATCH 14/40] fix: handle review --- argparse/argparse_blackbox_test.mbt | 177 ++++++++++++++++++++++++++++ argparse/command.mbt | 2 +- argparse/parser.mbt | 2 +- argparse/parser_globals_merge.mbt | 48 +++++++- argparse/parser_positionals.mbt | 2 +- argparse/parser_validate.mbt | 168 ++++++++++++++++++++++++-- argparse/parser_values.mbt | 8 +- 7 files changed, 380 insertions(+), 27 deletions(-) diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index ec8a8a2ce..fad996558 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -2325,3 +2325,180 @@ test "global flag set in child argv is merged back to parent" { sub.sources is { "verbose": Argv, .. }, ) } + +///| +test "global count negation after subcommand resets merged state" { + let cmd = @argparse.Command( + "demo", + args=[ + @argparse.FlagArg( + "verbose", + long="verbose", + action=Count, + negatable=true, + global=true, + ), + ], + subcommands=[Command("run")], + ) + + let parsed = cmd.parse( + argv=["--verbose", "run", "--no-verbose"], + env=empty_env(), + ) catch { + _ => panic() + } + assert_true(parsed.flags is { "verbose": false, .. }) + assert_true(parsed.flag_counts.get("verbose") is None) + assert_true(parsed.sources is { "verbose": Argv, .. }) + assert_true( + parsed.subcommand is Some(("run", sub)) && + sub.flags is { "verbose": false, .. } && + sub.flag_counts.get("verbose") is None && + sub.sources is { "verbose": Argv, .. }, + ) +} + +///| +test "global set option rejects duplicate occurrences across subcommands" { + let cmd = @argparse.Command( + "demo", + args=[@argparse.OptionArg("mode", long="mode", global=true)], + subcommands=[Command("run")], + ) + try + cmd.parse(argv=["--mode", "a", "run", "--mode", "b"], env=empty_env()) + catch { + @argparse.ArgParseError::InvalidArgument(msg) => { + assert_true(msg.contains("--mode")) + assert_true(msg.contains("cannot be used multiple times")) + } + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "global override with incompatible inherited type is rejected" { + try + @argparse.Command( + "demo", + args=[ + @argparse.OptionArg("mode", long="mode", required=true, global=true), + ], + subcommands=[ + Command("run", args=[ + @argparse.FlagArg("mode", long="mode", global=true), + ]), + ], + ).parse(argv=["run", "--mode"], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + assert_true(msg.contains("incompatible")) + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "child local long alias collision with inherited global is rejected" { + try + @argparse.Command( + "demo", + args=[@argparse.FlagArg("verbose", long="verbose", global=true)], + subcommands=[ + Command("run", args=[@argparse.OptionArg("local", long="verbose")]), + ], + ).parse(argv=["run", "--verbose"], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => { + assert_true(msg.contains("long option")) + assert_true(msg.contains("inherited global")) + } + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "child local short alias collision with inherited global is rejected" { + try + @argparse.Command( + "demo", + args=[@argparse.FlagArg("verbose", short='v', global=true)], + subcommands=[ + Command("run", args=[@argparse.OptionArg("local", short='v')]), + ], + ).parse(argv=["run", "-v"], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => { + assert_true(msg.contains("short option")) + assert_true(msg.contains("inherited global")) + } + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "nested subcommands inherit finalized globals from ancestors" { + let leaf = @argparse.Command("leaf") + let mid = @argparse.Command("mid", subcommands=[leaf]) + let cmd = @argparse.Command( + "demo", + args=[@argparse.FlagArg("verbose", long="verbose", global=true)], + subcommands=[mid], + ) + + let parsed = cmd.parse(argv=["--verbose", "mid", "leaf"], env=empty_env()) catch { + _ => panic() + } + assert_true(parsed.flags is { "verbose": true, .. }) + assert_true( + parsed.subcommand is Some(("mid", mid_matches)) && + mid_matches.flags is { "verbose": true, .. } && + mid_matches.subcommand is Some(("leaf", leaf_matches)) && + leaf_matches.flags is { "verbose": true, .. } && + leaf_matches.sources is { "verbose": Argv, .. }, + ) +} + +///| +test "non-bmp short option token does not panic" { + let cmd = @argparse.Command("demo", args=[ + @argparse.FlagArg("party", short='🎉'), + ]) + let parsed = cmd.parse(argv=["-🎉"], env=empty_env()) catch { _ => panic() } + assert_true(parsed.flags is { "party": true, .. }) +} + +///| +test "non-bmp hyphen token reports unknown argument without panic" { + let cmd = @argparse.Command("demo", args=[ + @argparse.PositionalArg("value", index=0), + ]) + try cmd.parse(argv=["-🎉"], env=empty_env()) catch { + @argparse.ArgParseError::UnknownArgument(arg, hint) => { + assert_true(arg == "-🎉") + assert_true(hint is None) + } + _ => panic() + } noraise { + _ => panic() + } +} + +///| +test "option env values remain string values instead of flags" { + let cmd = @argparse.Command("demo", args=[ + @argparse.OptionArg("mode", long="mode", env="MODE"), + ]) + let parsed = cmd.parse(argv=[], env={ "MODE": "fast" }) catch { _ => panic() } + assert_true(parsed.values is { "mode": ["fast"], .. }) + assert_true(parsed.flags.get("mode") is None) + assert_true(parsed.sources is { "mode": Env, .. }) +} diff --git a/argparse/command.mbt b/argparse/command.mbt index 2314055e9..71290f682 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -85,7 +85,7 @@ pub fn Command::new( build_error: arg_error, } if cmd.build_error is None { - validate_command(cmd, parsed_args, groups, @set.new()) catch { + validate_command(cmd, parsed_args, groups, []) catch { err => cmd.build_error = Some(err) } } diff --git a/argparse/parser.mbt b/argparse/parser.mbt index 082214c81..eb1d670de 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -297,7 +297,7 @@ fn parse_command( _ => apply_flag(matches, spec, Argv) } } - pos = pos + 1 + pos = pos + short.utf16_len() } i = i + 1 continue diff --git a/argparse/parser_globals_merge.mbt b/argparse/parser_globals_merge.mbt index 5091ad46d..458e43d1b 100644 --- a/argparse/parser_globals_merge.mbt +++ b/argparse/parser_globals_merge.mbt @@ -59,7 +59,7 @@ fn merge_global_value_from_child( child : Matches, arg : Arg, name : String, -) -> Unit { +) -> Unit raise ArgParseError { let parent_vals = parent.values.get(name) let child_vals = child.values.get(name) let parent_source = parent.value_sources.get(name) @@ -107,6 +107,15 @@ fn merge_global_value_from_child( } } } else { + if has_parent && + has_child && + parent_source is Some(Argv) && + child_source is Some(Argv) && + arg.info is Option(action=Set, ..) { + raise InvalidArgument( + "argument '\{global_option_conflict_label(arg)}' cannot be used multiple times", + ) + } let choose_child = has_child && (!has_parent || prefer_child_source(parent_source, child_source)) if choose_child { @@ -145,9 +154,15 @@ fn merge_global_flag_from_child( if both_argv { let parent_count = parent.counts.get(name).unwrap_or(0) let child_count = child.counts.get(name).unwrap_or(0) - let total = parent_count + child_count - parent.counts[name] = total - parent.flags[name] = total > 0 + if child_count == 0 && !v { + // `--no-` after a subcommand should reset an earlier argv count. + parent.counts[name] = 0 + parent.flags[name] = false + } else { + let total = parent_count + child_count + parent.counts[name] = total + parent.flags[name] = total > 0 + } match strongest_source(parent_source, child_source) { Some(src) => parent.flag_sources[name] = src None => () @@ -189,7 +204,7 @@ fn merge_globals_from_child( child : Matches, globals : Array[Arg], child_local_non_globals : @set.Set[String], -) -> Unit { +) -> Unit raise ArgParseError { for arg in globals { let name = arg.name if child_local_non_globals.contains(name) { @@ -203,6 +218,22 @@ fn merge_globals_from_child( } } +///| +fn global_option_conflict_label(arg : Arg) -> String { + match arg.info { + Flag(long~, short~, ..) | Option(long~, short~, ..) => + match long { + Some(name) => "--\{name}" + None => + match short { + Some(short) => "-\{short}" + None => arg.name + } + } + Positional(_) => arg.name + } +} + ///| fn propagate_globals_to_child( parent : Matches, @@ -245,4 +276,11 @@ fn propagate_globals_to_child( } } } + match child.parsed_subcommand { + Some((sub_name, sub_m)) => { + propagate_globals_to_child(parent, sub_m, globals, @set.new()) + child.parsed_subcommand = Some((sub_name, sub_m)) + } + None => () + } } diff --git a/argparse/parser_positionals.mbt b/argparse/parser_positionals.mbt index 616071d16..9dc69c035 100644 --- a/argparse/parser_positionals.mbt +++ b/argparse/parser_positionals.mbt @@ -126,7 +126,7 @@ fn is_negative_number(arg : String) -> Bool { if ch < '0' || ch > '9' { return false } - i = i + 1 + i = i + ch.utf16_len() } true } diff --git a/argparse/parser_validate.mbt b/argparse/parser_validate.mbt index 9c1d94023..6effa2543 100644 --- a/argparse/parser_validate.mbt +++ b/argparse/parser_validate.mbt @@ -99,43 +99,185 @@ fn validate_command( cmd : Command, args : Array[Arg], groups : Array[ArgGroup], - inherited_global_names : @set.Set[String], + inherited_globals : Array[Arg], ) -> Unit raise ArgBuildError { match cmd.build_error { Some(err) => raise err None => () } - validate_inherited_global_shadowing(args, inherited_global_names) + validate_inherited_global_shadowing(args, inherited_globals) validate_group_defs(args, groups) validate_group_refs(args, groups) validate_subcommand_defs(cmd.subcommands) validate_subcommand_required_policy(cmd) validate_help_subcommand(cmd) validate_version_actions(cmd) - let child_inherited_global_names = inherited_global_names.copy() - for global in collect_globals(args) { - child_inherited_global_names.add(global.name) - } + let child_inherited_globals = merge_inherited_globals( + inherited_globals, + collect_globals(args), + ) for sub in cmd.subcommands { - validate_command(sub, sub.args, sub.groups, child_inherited_global_names) + validate_command(sub, sub.args, sub.groups, child_inherited_globals) } } ///| fn validate_inherited_global_shadowing( args : Array[Arg], - inherited_global_names : @set.Set[String], + inherited_globals : Array[Arg], ) -> Unit raise ArgBuildError { for arg in args { - if arg.global { + if inherited_globals.iter().find_first(g => g.name == arg.name) + is Some(inherited_arg) { + if !arg.global { + raise Unsupported( + "arg '\{arg.name}' shadows an inherited global; rename the arg or mark it global", + ) + } + if !global_override_compatible(inherited_arg, arg) { + raise Unsupported( + "global arg '\{arg.name}' is incompatible with inherited global definition", + ) + } + } + match arg.info { + Flag(long~, short~, negatable~, ..) => { + if long is Some(name) { + validate_inherited_global_long_collision(arg, name, inherited_globals) + if negatable { + validate_inherited_global_long_collision( + arg, + "no-\{name}", + inherited_globals, + ) + } + } + if short is Some(value) { + validate_inherited_global_short_collision( + arg, value, inherited_globals, + ) + } + } + Option(long~, short~, ..) => { + if long is Some(name) { + validate_inherited_global_long_collision(arg, name, inherited_globals) + } + if short is Some(value) { + validate_inherited_global_short_collision( + arg, value, inherited_globals, + ) + } + } + Positional(_) => () + } + } +} + +///| +fn merge_inherited_globals( + inherited_globals : Array[Arg], + globals_here : Array[Arg], +) -> Array[Arg] { + let merged = inherited_globals.copy() + for global in globals_here { + match merged.search_by(arg => arg.name == global.name) { + Some(idx) => merged[idx] = global + None => merged.push(global) + } + } + merged +} + +///| +fn global_override_compatible(inherited_arg : Arg, arg : Arg) -> Bool { + match inherited_arg.info { + Flag(action=inherited_action, ..) => + match arg.info { + Flag(action~, ..) => inherited_action == action + _ => false + } + Option(action=inherited_action, ..) => + match arg.info { + Option(action~, ..) => inherited_action == action + _ => false + } + Positional(_) => false + } +} + +///| +fn validate_inherited_global_long_collision( + arg : Arg, + long : String, + inherited_globals : Array[Arg], +) -> Unit raise ArgBuildError { + if inherited_global_long_owner(arg.name, long, inherited_globals) + is Some(owner) { + raise Unsupported( + "arg '\{arg.name}' long option --\{long} conflicts with inherited global '\{owner}'", + ) + } +} + +///| +fn validate_inherited_global_short_collision( + arg : Arg, + short : Char, + inherited_globals : Array[Arg], +) -> Unit raise ArgBuildError { + if inherited_global_short_owner(arg.name, short, inherited_globals) + is Some(owner) { + raise Unsupported( + "arg '\{arg.name}' short option -\{short} conflicts with inherited global '\{owner}'", + ) + } +} + +///| +fn inherited_global_long_owner( + current_name : String, + long : String, + inherited_globals : Array[Arg], +) -> String? { + for inherited in inherited_globals { + if inherited.name == current_name { + continue + } + match inherited.info { + Flag(long=inherited_long, negatable~, ..) => + if inherited_long is Some(name) && + (name == long || (negatable && "no-\{name}" == long)) { + return Some(inherited.name) + } + Option(long=inherited_long, ..) => + if inherited_long is Some(name) && name == long { + return Some(inherited.name) + } + Positional(_) => () + } + } + None +} + +///| +fn inherited_global_short_owner( + current_name : String, + short : Char, + inherited_globals : Array[Arg], +) -> String? { + for inherited in inherited_globals { + if inherited.name == current_name { continue } - if inherited_global_names.contains(arg.name) { - raise Unsupported( - "arg '\{arg.name}' shadows an inherited global; rename the arg or mark it global", - ) + match inherited.info { + Flag(short=inherited_short, ..) | Option(short=inherited_short, ..) => + if inherited_short is Some(value) && value == short { + return Some(inherited.name) + } + Positional(_) => () } } + None } ///| diff --git a/argparse/parser_values.mbt b/argparse/parser_values.mbt index 320a65c9a..45d742864 100644 --- a/argparse/parser_values.mbt +++ b/argparse/parser_values.mbt @@ -230,12 +230,8 @@ fn apply_env( matches.flags[name] = flag matches.flag_sources[name] = Env } - Option(action=Set, ..) | Positional(_) => { - let flag = parse_bool(value) - matches.flags[name] = flag - matches.flag_sources[name] = Env - } - Flag(action=Help | Version, ..) | Option(action=Append, ..) => () + Option(_) | Positional(_) => () + Flag(action=Help | Version, ..) => () } } } From a238e5c546fc3266dc9e0a44c90a95448bddd299 Mon Sep 17 00:00:00 2001 From: zihang Date: Thu, 26 Feb 2026 18:31:44 +0800 Subject: [PATCH 15/40] refactor: separate argument config and remove &Arg --- argparse/README.mbt.md | 39 +- argparse/arg_spec.mbt | 37 -- argparse/argparse_blackbox_test.mbt | 597 ++++++++++++---------------- argparse/argparse_test.mbt | 88 ++-- argparse/command.mbt | 43 +- argparse/pkg.generated.mbti | 9 +- 6 files changed, 354 insertions(+), 459 deletions(-) diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index bc895e2b8..8d4d350d1 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -26,7 +26,7 @@ Arguments without both are positional-only and should be declared with ```mbt check ///| test "name-only option is rejected" { - let cmd = @argparse.Command("demo", args=[@argparse.OptionArg("input")]) + let cmd = @argparse.Command("demo", options=[OptionArg("input")]) try cmd.parse(argv=["file.txt"], env={}) catch { @argparse.ArgBuildError::Unsupported(msg) => inspect(msg, content="flag/option args require short/long") @@ -42,11 +42,12 @@ test "name-only option is rejected" { ```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 cmd = @argparse.Command( + "demo", + flags=[FlagArg("verbose", short='v', long="verbose")], + options=[OptionArg("count", long="count")], + positionals=[PositionalArg("name", index=0)], + ) let matches = cmd.parse(argv=["-v", "--count", "2", "alice"], env={}) catch { _ => panic() } @@ -56,12 +57,12 @@ test "flag option positional" { ///| test "subcommand with global flag" { - let echo = @argparse.Command("echo", args=[ - @argparse.PositionalArg("msg", index=0), + let echo = @argparse.Command("echo", positionals=[ + PositionalArg("msg", index=0), ]) let cmd = @argparse.Command( "demo", - args=[@argparse.FlagArg("verbose", short='v', long="verbose", global=true)], + flags=[FlagArg("verbose", short='v', long="verbose", global=true)], subcommands=[echo], ) let matches = cmd.parse(argv=["--verbose", "echo", "hi"], env={}) catch { @@ -84,15 +85,13 @@ 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"), - ]) + let cmd = @argparse.Command( + "demo", + about="demo app", + version="1.0.0", + flags=[FlagArg("verbose", short='v', long="verbose", about="verbose mode")], + options=[OptionArg("count", long="count", about="repeat count")], + ) try cmd.parse(argv=["--help"], env={}) catch { @argparse.DisplayHelp::Message(text) => inspect( @@ -118,8 +117,8 @@ test "help snapshot" { ///| test "custom version option overrides built-in version flag" { - let cmd = @argparse.Command("demo", version="1.0.0", args=[ - @argparse.FlagArg( + let cmd = @argparse.Command("demo", version="1.0.0", flags=[ + FlagArg( "custom_version", short='V', long="version", diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt index 86b435e91..33756451b 100644 --- a/argparse/arg_spec.mbt +++ b/argparse/arg_spec.mbt @@ -64,13 +64,6 @@ priv enum ArgInfo { ) } -///| -/// Trait for declarative arg constructors. -trait ArgLike { - to_arg(Self) -> Arg - validate(Self, ValidationCtx) -> Unit raise ArgBuildError -} - ///| /// Declarative flag constructor wrapper. pub struct FlagArg { @@ -93,16 +86,6 @@ pub struct FlagArg { ) -> FlagArg } -///| -pub impl ArgLike for FlagArg with to_arg(self : FlagArg) { - self.arg -} - -///| -pub impl ArgLike for FlagArg with validate(self, ctx) { - validate_flag_arg(self.arg, ctx) -} - ///| /// Create a flag argument. /// @@ -179,16 +162,6 @@ pub struct OptionArg { ) -> OptionArg } -///| -pub impl ArgLike for OptionArg with to_arg(self : OptionArg) { - self.arg -} - -///| -pub impl ArgLike for OptionArg with validate(self, ctx) { - validate_option_arg(self.arg, ctx) -} - ///| /// Create an option argument that consumes one value per occurrence. /// @@ -271,16 +244,6 @@ pub struct PositionalArg { ) -> PositionalArg } -///| -pub impl ArgLike for PositionalArg with to_arg(self : PositionalArg) { - self.arg -} - -///| -pub impl ArgLike for PositionalArg with validate(self, ctx) { - validate_positional_arg(self.arg, ctx) -} - ///| /// Create a positional argument. /// diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index fad996558..60558f238 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -40,11 +40,13 @@ test "render help snapshot with groups and hidden entries" { Command("run", about="run"), Command("hidden", about="hidden", hidden=true), ], - args=[ - @argparse.FlagArg("fast", short='f', long="fast"), - @argparse.FlagArg("slow", long="slow", hidden=true), - @argparse.FlagArg("cache", long="cache", negatable=true, about="cache"), - @argparse.OptionArg( + flags=[ + FlagArg("fast", short='f', long="fast"), + FlagArg("slow", long="slow", hidden=true), + FlagArg("cache", long="cache", negatable=true, about="cache"), + ], + options=[ + OptionArg( "path", short='p', long="path", @@ -52,9 +54,11 @@ test "render help snapshot with groups and hidden entries" { default_values=["a", "b"], required=true, ), - @argparse.PositionalArg("target", index=0, required=true), - @argparse.PositionalArg("rest", index=1, num_args=ValueRange(lower=0)), - @argparse.PositionalArg("secret", index=2, hidden=true), + ], + positionals=[ + PositionalArg("target", index=0, required=true), + PositionalArg("rest", index=1, num_args=ValueRange(lower=0)), + PositionalArg("secret", index=2, hidden=true), ], ) inspect( @@ -88,8 +92,8 @@ test "render help conversion coverage snapshot" { let cmd = @argparse.Command( "shape", groups=[ArgGroup("grp", args=["f", "opt", "pos"])], - args=[ - @argparse.FlagArg( + flags=[ + FlagArg( "f", short='f', about="f", @@ -99,7 +103,9 @@ test "render help conversion coverage snapshot" { global=true, hidden=true, ), - @argparse.OptionArg( + ], + options=[ + OptionArg( "opt", short='o', about="opt", @@ -111,7 +117,9 @@ test "render help conversion coverage snapshot" { hidden=true, conflicts_with=["pos"], ), - @argparse.PositionalArg( + ], + positionals=[ + PositionalArg( "pos", about="pos", env="POS_ENV", @@ -141,8 +149,8 @@ test "render help conversion coverage snapshot" { ///| test "count flags and sources with pattern matching" { - let cmd = @argparse.Command("demo", args=[ - @argparse.FlagArg("verbose", short='v', long="verbose", action=Count), + let cmd = @argparse.Command("demo", flags=[ + FlagArg("verbose", short='v', long="verbose", action=Count), ]) let matches = cmd.parse(argv=["-v", "-v", "-v"], env=empty_env()) catch { _ => panic() @@ -157,8 +165,8 @@ test "global option merges parent and child values" { let child = @argparse.Command("run") let cmd = @argparse.Command( "demo", - args=[ - @argparse.OptionArg( + options=[ + OptionArg( "profile", short='p', long="profile", @@ -187,9 +195,9 @@ test "global option merges parent and child values" { test "global requires is validated after parent-child merge" { let cmd = @argparse.Command( "demo", - args=[ - @argparse.OptionArg("mode", long="mode", requires=["config"], global=true), - @argparse.OptionArg("config", long="config", global=true), + options=[ + OptionArg("mode", long="mode", requires=["config"], global=true), + OptionArg("config", long="config", global=true), ], subcommands=[Command("run")], ) @@ -212,8 +220,8 @@ test "global append keeps parent argv over child env/default" { let child = @argparse.Command("run") let cmd = @argparse.Command( "demo", - args=[ - @argparse.OptionArg( + options=[ + OptionArg( "profile", long="profile", action=Append, @@ -244,8 +252,8 @@ test "global scalar keeps parent argv over child env/default" { let child = @argparse.Command("run") let cmd = @argparse.Command( "demo", - args=[ - @argparse.OptionArg( + options=[ + OptionArg( "profile", long="profile", env="PROFILE", @@ -275,14 +283,8 @@ test "global count merges parent and child occurrences" { let child = @argparse.Command("run") let cmd = @argparse.Command( "demo", - args=[ - @argparse.FlagArg( - "verbose", - short='v', - long="verbose", - action=Count, - global=true, - ), + flags=[ + FlagArg("verbose", short='v', long="verbose", action=Count, global=true), ], subcommands=[child], ) @@ -302,8 +304,8 @@ test "global count keeps parent argv over child env fallback" { let child = @argparse.Command("run") let cmd = @argparse.Command( "demo", - args=[ - @argparse.FlagArg( + flags=[ + FlagArg( "verbose", short='v', long="verbose", @@ -332,9 +334,7 @@ test "global flag keeps parent argv over child env fallback" { let child = @argparse.Command("run") let cmd = @argparse.Command( "demo", - args=[ - @argparse.FlagArg("verbose", long="verbose", env="VERBOSE", global=true), - ], + flags=[FlagArg("verbose", long="verbose", env="VERBOSE", global=true)], subcommands=[child], ) @@ -354,7 +354,7 @@ test "global flag keeps parent argv over child env fallback" { test "subcommand cannot follow positional arguments" { let cmd = @argparse.Command( "demo", - args=[@argparse.PositionalArg("input", index=0)], + positionals=[PositionalArg("input", index=0)], subcommands=[Command("run")], ) try cmd.parse(argv=["raw", "run"], env=empty_env()) catch { @@ -376,8 +376,8 @@ test "global count source keeps env across subcommand merge" { let child = @argparse.Command("run") let cmd = @argparse.Command( "demo", - args=[ - @argparse.FlagArg( + flags=[ + FlagArg( "verbose", short='v', long="verbose", @@ -498,8 +498,8 @@ test "subcommand help includes inherited global options" { let leaf = @argparse.Command("echo", about="echo") let cmd = @argparse.Command( "demo", - args=[ - @argparse.FlagArg( + flags=[ + FlagArg( "verbose", short='v', long="verbose", @@ -533,8 +533,8 @@ test "subcommand help includes inherited global options" { ///| test "unknown argument suggestions are exposed" { - let cmd = @argparse.Command("demo", args=[ - @argparse.FlagArg("verbose", short='v', long="verbose"), + let cmd = @argparse.Command("demo", flags=[ + FlagArg("verbose", short='v', long="verbose"), ]) try cmd.parse(argv=["--verbse"], env=empty_env()) catch { @@ -570,8 +570,8 @@ test "unknown argument suggestions are exposed" { ///| test "long and short value parsing branches" { - let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("count", short='c', long="count"), + let cmd = @argparse.Command("demo", options=[ + OptionArg("count", short='c', long="count"), ]) let long_inline = cmd.parse(argv=["--count=2"], env=empty_env()) catch { @@ -607,8 +607,8 @@ test "long and short value parsing branches" { ///| test "append option action is publicly selectable" { - let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("tag", long="tag", action=Append), + let cmd = @argparse.Command("demo", options=[ + OptionArg("tag", long="tag", action=Append), ]) let appended = cmd.parse(argv=["--tag", "a", "--tag", "b"], env=empty_env()) catch { _ => panic() @@ -619,10 +619,11 @@ test "append option action is publicly selectable" { ///| test "negation parsing and invalid negation forms" { - let cmd = @argparse.Command("demo", args=[ - @argparse.FlagArg("cache", long="cache", negatable=true), - @argparse.OptionArg("path", long="path"), - ]) + let cmd = @argparse.Command( + "demo", + flags=[FlagArg("cache", long="cache", negatable=true)], + options=[OptionArg("path", long="path")], + ) let off = cmd.parse(argv=["--no-cache"], env=empty_env()) catch { _ => panic() @@ -654,8 +655,8 @@ test "negation parsing and invalid negation forms" { _ => panic() } - let count_cmd = @argparse.Command("demo", args=[ - @argparse.FlagArg("verbose", long="verbose", action=Count, negatable=true), + let count_cmd = @argparse.Command("demo", flags=[ + FlagArg("verbose", long="verbose", action=Count, negatable=true), ]) let reset = count_cmd.parse( argv=["--verbose", "--no-verbose"], @@ -670,8 +671,8 @@ test "negation parsing and invalid negation forms" { ///| test "positionals force mode and dash handling" { - let force_cmd = @argparse.Command("demo", args=[ - @argparse.PositionalArg( + let force_cmd = @argparse.Command("demo", positionals=[ + PositionalArg( "tail", index=0, num_args=ValueRange(lower=0), @@ -689,8 +690,8 @@ test "positionals force mode and dash handling" { } assert_true(dashed.values is { "tail": ["p", "q"], .. }) - let negative_cmd = @argparse.Command("demo", args=[ - @argparse.PositionalArg("n", index=0), + let negative_cmd = @argparse.Command("demo", positionals=[ + PositionalArg("n", index=0), ]) let negative = negative_cmd.parse(argv=["-9"], env=empty_env()) catch { _ => panic() @@ -707,8 +708,8 @@ test "positionals force mode and dash handling" { ///| test "variadic positional keeps accepting hyphen values after first token" { - let cmd = @argparse.Command("demo", args=[ - @argparse.PositionalArg( + let cmd = @argparse.Command("demo", positionals=[ + PositionalArg( "tail", index=0, num_args=ValueRange(lower=0), @@ -723,9 +724,9 @@ test "variadic positional keeps accepting hyphen values after first token" { ///| test "bounded positional does not greedily consume later required values" { - let cmd = @argparse.Command("demo", args=[ - @argparse.PositionalArg("first", num_args=ValueRange(lower=1, upper=2)), - @argparse.PositionalArg("second", required=true), + let cmd = @argparse.Command("demo", positionals=[ + PositionalArg("first", num_args=ValueRange(lower=1, upper=2)), + PositionalArg("second", required=true), ]) let two = cmd.parse(argv=["a", "b"], env=empty_env()) catch { _ => panic() } @@ -739,13 +740,9 @@ test "bounded positional does not greedily consume later required values" { ///| test "indexed non-last positional allows explicit single num_args" { - let cmd = @argparse.Command("demo", args=[ - @argparse.PositionalArg( - "first", - index=0, - num_args=@argparse.ValueRange::single(), - ), - @argparse.PositionalArg("second", index=1, required=true), + let cmd = @argparse.Command("demo", positionals=[ + PositionalArg("first", index=0, num_args=@argparse.ValueRange::single()), + PositionalArg("second", index=1, required=true), ]) let parsed = cmd.parse(argv=["a", "b"], env=empty_env()) catch { @@ -757,13 +754,9 @@ test "indexed non-last positional allows explicit single num_args" { ///| test "empty positional value range is rejected at build time" { try - @argparse.Command("demo", args=[ - @argparse.PositionalArg( - "skip", - index=0, - num_args=ValueRange(lower=0, upper=0), - ), - @argparse.PositionalArg("name", index=1, required=true), + @argparse.Command("demo", positionals=[ + PositionalArg("skip", index=0, num_args=ValueRange(lower=0, upper=0)), + PositionalArg("name", index=1, required=true), ]).parse(argv=["alice"], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -776,10 +769,10 @@ test "empty positional value range is rejected at build time" { ///| test "env parsing for settrue setfalse count and invalid values" { - let cmd = @argparse.Command("demo", args=[ - @argparse.FlagArg("on", long="on", action=SetTrue, env="ON"), - @argparse.FlagArg("off", long="off", action=SetFalse, env="OFF"), - @argparse.FlagArg("v", long="v", action=Count, env="V"), + let cmd = @argparse.Command("demo", flags=[ + FlagArg("on", long="on", action=SetTrue, env="ON"), + FlagArg("off", long="off", action=SetFalse, env="OFF"), + FlagArg("v", long="v", action=Count, env="V"), ]) let parsed = cmd.parse(argv=[], env={ "ON": "true", "OFF": "true", "V": "3" }) catch { @@ -844,11 +837,9 @@ test "env parsing for settrue setfalse count and invalid values" { ///| test "defaults and value range helpers through public API" { - let defaults = @argparse.Command("demo", args=[ - @argparse.OptionArg("mode", long="mode", action=Append, default_values=[ - "a", "b", - ]), - @argparse.OptionArg("one", long="one", default_values=["x"]), + let defaults = @argparse.Command("demo", options=[ + OptionArg("mode", long="mode", action=Append, default_values=["a", "b"]), + OptionArg("one", long="one", default_values=["x"]), ]) let by_default = defaults.parse(argv=[], env=empty_env()) catch { _ => panic() @@ -856,8 +847,8 @@ test "defaults and value range helpers through public API" { assert_true(by_default.values is { "mode": ["a", "b"], "one": ["x"], .. }) assert_true(by_default.sources is { "mode": Default, "one": Default, .. }) - let upper_only = @argparse.Command("demo", args=[ - @argparse.OptionArg("tag", long="tag", action=Append), + let upper_only = @argparse.Command("demo", options=[ + OptionArg("tag", long="tag", action=Append), ]) let upper_parsed = upper_only.parse( argv=["--tag", "a", "--tag", "b", "--tag", "c"], @@ -867,8 +858,8 @@ test "defaults and value range helpers through public API" { } assert_true(upper_parsed.values is { "tag": ["a", "b", "c"], .. }) - let lower_only = @argparse.Command("demo", args=[ - @argparse.OptionArg("tag", long="tag"), + let lower_only = @argparse.Command("demo", options=[ + OptionArg("tag", long="tag"), ]) let lower_absent = lower_only.parse(argv=[], env=empty_env()) catch { _ => panic() @@ -893,9 +884,7 @@ test "defaults and value range helpers through public API" { ///| test "options consume exactly one value per occurrence" { - let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("tag", long="tag"), - ]) + let cmd = @argparse.Command("demo", options=[OptionArg("tag", long="tag")]) let parsed = cmd.parse(argv=["--tag", "a"], env=empty_env()) catch { _ => panic() } @@ -912,9 +901,7 @@ test "options consume exactly one value per occurrence" { ///| test "set options reject duplicate occurrences" { - let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("mode", long="mode"), - ]) + let cmd = @argparse.Command("demo", options=[OptionArg("mode", long="mode")]) try cmd.parse(argv=["--mode", "a", "--mode", "b"], env=empty_env()) catch { @argparse.ArgParseError::InvalidArgument(msg) => inspect(msg, content="argument '--mode' cannot be used multiple times") @@ -927,7 +914,7 @@ test "set options reject duplicate occurrences" { ///| test "flag and option args require short or long names" { try - @argparse.Command("demo", args=[@argparse.OptionArg("input")]).parse( + @argparse.Command("demo", options=[OptionArg("input")]).parse( argv=[], env=empty_env(), ) @@ -940,7 +927,7 @@ test "flag and option args require short or long names" { } try - @argparse.Command("demo", args=[@argparse.FlagArg("verbose")]).parse( + @argparse.Command("demo", flags=[FlagArg("verbose")]).parse( argv=[], env=empty_env(), ) @@ -955,8 +942,8 @@ test "flag and option args require short or long names" { ///| test "append options collect values across repeated occurrences" { - let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("arg", long="arg", action=Append), + let cmd = @argparse.Command("demo", options=[ + OptionArg("arg", long="arg", action=Append), ]) let parsed = cmd.parse(argv=["--arg", "x", "--arg", "y"], env=empty_env()) catch { _ => panic() @@ -967,10 +954,11 @@ test "append options collect values across repeated occurrences" { ///| test "option parsing stops at the next option token" { - let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("arg", short='a', long="arg"), - @argparse.FlagArg("verbose", long="verbose"), - ]) + let cmd = @argparse.Command( + "demo", + flags=[FlagArg("verbose", long="verbose")], + options=[OptionArg("arg", short='a', long="arg")], + ) let stopped = cmd.parse(argv=["--arg", "x", "--verbose"], env=empty_env()) catch { _ => panic() @@ -995,10 +983,11 @@ test "option parsing stops at the next option token" { ///| test "options always require a value" { - let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("opt", long="opt"), - @argparse.FlagArg("verbose", long="verbose"), - ]) + let cmd = @argparse.Command( + "demo", + flags=[FlagArg("verbose", long="verbose")], + options=[OptionArg("opt", long="opt")], + ) try cmd.parse(argv=["--opt", "--verbose"], env=empty_env()) catch { @argparse.ArgParseError::MissingValue(name) => assert_true(name == "--opt") _ => panic() @@ -1006,8 +995,8 @@ test "options always require a value" { _ => panic() } - let zero_value_required = @argparse.Command("demo", args=[ - @argparse.OptionArg("opt", long="opt", required=true), + let zero_value_required = @argparse.Command("demo", options=[ + OptionArg("opt", long="opt", required=true), ]).parse(argv=["--opt", "x"], env=empty_env()) catch { _ => panic() } @@ -1016,8 +1005,8 @@ test "options always require a value" { ///| test "option values reject hyphen tokens unless allow_hyphen_values is enabled" { - let strict = @argparse.Command("demo", args=[ - @argparse.OptionArg("pattern", long="pattern"), + let strict = @argparse.Command("demo", options=[ + OptionArg("pattern", long="pattern"), ]) let mut rejected = false try strict.parse(argv=["--pattern", "-file"], env=empty_env()) catch { @@ -1035,8 +1024,8 @@ test "option values reject hyphen tokens unless allow_hyphen_values is enabled" } assert_true(rejected) - let permissive = @argparse.Command("demo", args=[ - @argparse.OptionArg("pattern", long="pattern", allow_hyphen_values=true), + let permissive = @argparse.Command("demo", options=[ + OptionArg("pattern", long="pattern", allow_hyphen_values=true), ]) let parsed = permissive.parse(argv=["--pattern", "-file"], env=empty_env()) catch { _ => panic() @@ -1047,9 +1036,7 @@ test "option values reject hyphen tokens unless allow_hyphen_values is enabled" ///| test "from_matches uses public decoding hook" { - let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("name", long="name"), - ]) + let cmd = @argparse.Command("demo", options=[OptionArg("name", long="name")]) let matches = cmd.parse(argv=["--name", "alice"], env=empty_env()) catch { _ => panic() } @@ -1061,8 +1048,8 @@ test "from_matches uses public decoding hook" { ///| test "default argv path is reachable" { - let cmd = @argparse.Command("demo", args=[ - @argparse.PositionalArg( + let cmd = @argparse.Command("demo", positionals=[ + PositionalArg( "rest", num_args=ValueRange(lower=0), allow_hyphen_values=true, @@ -1074,7 +1061,7 @@ test "default argv path is reachable" { ///| test "validation branches exposed through parse" { try - @argparse.Command("demo", args=[@argparse.FlagArg("f", action=Help)]).parse( + @argparse.Command("demo", flags=[FlagArg("f", action=Help)]).parse( argv=[], env=empty_env(), ) @@ -1092,8 +1079,8 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", args=[ - @argparse.FlagArg("f", long="f", action=Help, negatable=true), + @argparse.Command("demo", flags=[ + FlagArg("f", long="f", action=Help, negatable=true), ]).parse(argv=[], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1109,8 +1096,8 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", args=[ - @argparse.FlagArg("f", long="f", action=Help, env="F"), + @argparse.Command("demo", flags=[ + FlagArg("f", long="f", action=Help, env="F"), ]).parse(argv=[], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1126,7 +1113,7 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", args=[@argparse.OptionArg("x", long="x")]).parse( + @argparse.Command("demo", options=[OptionArg("x", long="x")]).parse( argv=["--x", "a", "b"], env=empty_env(), ) @@ -1138,8 +1125,8 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", args=[ - @argparse.OptionArg("x", long="x", default_values=["a", "b"]), + @argparse.Command("demo", options=[ + OptionArg("x", long="x", default_values=["a", "b"]), ]).parse(argv=[], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1155,8 +1142,8 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", args=[ - @argparse.PositionalArg("x", num_args=ValueRange(lower=3, upper=2)), + @argparse.Command("demo", positionals=[ + PositionalArg("x", num_args=ValueRange(lower=3, upper=2)), ]).parse(argv=[], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1172,8 +1159,8 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", args=[ - @argparse.PositionalArg("x", num_args=ValueRange(lower=-1, upper=2)), + @argparse.Command("demo", positionals=[ + PositionalArg("x", num_args=ValueRange(lower=-1, upper=2)), ]).parse(argv=[], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1189,8 +1176,8 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", args=[ - @argparse.PositionalArg("x", num_args=ValueRange(lower=0, upper=-1)), + @argparse.Command("demo", positionals=[ + PositionalArg("x", num_args=ValueRange(lower=0, upper=-1)), ]).parse(argv=[], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1206,13 +1193,9 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", args=[ - @argparse.PositionalArg( - "x", - index=0, - num_args=ValueRange(lower=0, upper=2), - ), - @argparse.PositionalArg("y", index=1), + @argparse.Command("demo", positionals=[ + PositionalArg("x", index=0, num_args=ValueRange(lower=0, upper=2)), + PositionalArg("y", index=1), ]).parse(argv=["a"], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1228,9 +1211,9 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", args=[ - @argparse.PositionalArg("x", index=0), - @argparse.PositionalArg("y", index=0), + @argparse.Command("demo", positionals=[ + PositionalArg("x", index=0), + PositionalArg("y", index=0), ]).parse(argv=["a"], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1318,9 +1301,9 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", args=[ - @argparse.OptionArg("x", long="x"), - @argparse.OptionArg("x", long="y"), + @argparse.Command("demo", options=[ + OptionArg("x", long="x"), + OptionArg("x", long="y"), ]).parse(argv=[], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1336,9 +1319,9 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", args=[ - @argparse.OptionArg("x", long="same"), - @argparse.OptionArg("y", long="same"), + @argparse.Command("demo", options=[ + OptionArg("x", long="same"), + OptionArg("y", long="same"), ]).parse(argv=[], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1354,9 +1337,9 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", args=[ - @argparse.FlagArg("hello", long="hello", negatable=true), - @argparse.FlagArg("x", long="no-hello"), + @argparse.Command("demo", flags=[ + FlagArg("hello", long="hello", negatable=true), + FlagArg("x", long="no-hello"), ]).parse(argv=[], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1372,9 +1355,9 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", args=[ - @argparse.OptionArg("x", short='s'), - @argparse.OptionArg("y", short='s'), + @argparse.Command("demo", options=[ + OptionArg("x", short='s'), + OptionArg("y", short='s'), ]).parse(argv=[], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1390,9 +1373,10 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", args=[ - @argparse.FlagArg("x", long="x", requires=["x"]), - ]).parse(argv=[], env=empty_env()) + @argparse.Command("demo", flags=[FlagArg("x", long="x", requires=["x"])]).parse( + argv=[], + env=empty_env(), + ) catch { @argparse.ArgBuildError::Unsupported(msg) => inspect( @@ -1407,8 +1391,8 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", args=[ - @argparse.FlagArg("x", long="x", conflicts_with=["x"]), + @argparse.Command("demo", flags=[ + FlagArg("x", long="x", conflicts_with=["x"]), ]).parse(argv=[], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1477,13 +1461,8 @@ test "validation branches exposed through parse" { _ => panic() } - let custom_help = @argparse.Command("demo", args=[ - @argparse.FlagArg( - "custom_help", - short='h', - long="help", - about="custom help", - ), + let custom_help = @argparse.Command("demo", flags=[ + FlagArg("custom_help", short='h', long="help", about="custom help"), ]) let help_short = custom_help.parse(argv=["-h"], env=empty_env()) catch { _ => panic() @@ -1504,13 +1483,8 @@ test "validation branches exposed through parse" { ), ) - let custom_version = @argparse.Command("demo", version="1.0", args=[ - @argparse.FlagArg( - "custom_version", - short='V', - long="version", - about="custom version", - ), + let custom_version = @argparse.Command("demo", version="1.0", flags=[ + FlagArg("custom_version", short='V', long="version", about="custom version"), ]) let version_short = custom_version.parse(argv=["-V"], env=empty_env()) catch { _ => panic() @@ -1533,9 +1507,10 @@ test "validation branches exposed through parse" { ) try - @argparse.Command("demo", args=[ - @argparse.FlagArg("v", long="v", action=Version), - ]).parse(argv=[], env=empty_env()) + @argparse.Command("demo", flags=[FlagArg("v", long="v", action=Version)]).parse( + argv=[], + env=empty_env(), + ) catch { @argparse.ArgBuildError::Unsupported(msg) => inspect( @@ -1582,8 +1557,8 @@ test "builtin and custom help/version dispatch edge paths" { _ => panic() } - let long_help = @argparse.Command("demo", args=[ - @argparse.FlagArg("assist", long="assist", action=Help), + let long_help = @argparse.Command("demo", flags=[ + FlagArg("assist", long="assist", action=Help), ]) try long_help.parse(argv=["--assist"], env=empty_env()) catch { @argparse.DisplayHelp::Message(text) => @@ -1593,8 +1568,8 @@ test "builtin and custom help/version dispatch edge paths" { _ => panic() } - let short_help = @argparse.Command("demo", args=[ - @argparse.FlagArg("assist", short='?', action=Help), + let short_help = @argparse.Command("demo", flags=[ + FlagArg("assist", short='?', action=Help), ]) try short_help.parse(argv=["-?"], env=empty_env()) catch { @argparse.DisplayHelp::Message(text) => @@ -1609,7 +1584,7 @@ test "builtin and custom help/version dispatch edge paths" { test "subcommand lookup falls back to positional value" { let cmd = @argparse.Command( "demo", - args=[@argparse.PositionalArg("input", index=0)], + positionals=[PositionalArg("input", index=0)], subcommands=[Command("run")], ) let parsed = cmd.parse(argv=["raw"], env=empty_env()) catch { _ => panic() } @@ -1654,10 +1629,8 @@ test "group requires/conflicts can target argument names" { let requires_cmd = @argparse.Command( "demo", groups=[ArgGroup("mode", args=["fast"], requires=["config"])], - args=[ - @argparse.FlagArg("fast", long="fast"), - @argparse.OptionArg("config", long="config"), - ], + flags=[FlagArg("fast", long="fast")], + options=[OptionArg("config", long="config")], ) let ok = requires_cmd.parse( @@ -1681,10 +1654,8 @@ test "group requires/conflicts can target argument names" { let conflicts_cmd = @argparse.Command( "demo", groups=[ArgGroup("mode", args=["fast"], conflicts_with=["config"])], - args=[ - @argparse.FlagArg("fast", long="fast"), - @argparse.OptionArg("config", long="config"), - ], + flags=[FlagArg("fast", long="fast")], + options=[OptionArg("config", long="config")], ) try @@ -1703,8 +1674,8 @@ test "group requires/conflicts can target argument names" { ///| test "group without members has no parse effect" { - let cmd = @argparse.Command("demo", groups=[ArgGroup("known")], args=[ - @argparse.FlagArg("x", long="x"), + let cmd = @argparse.Command("demo", groups=[ArgGroup("known")], flags=[ + FlagArg("x", long="x"), ]) let parsed = cmd.parse(argv=["--x"], env=empty_env()) catch { _ => panic() } assert_true(parsed.flags is { "x": true, .. }) @@ -1715,8 +1686,8 @@ test "group without members has no parse effect" { ///| test "arg validation catches unknown requires target" { try - @argparse.Command("demo", args=[ - @argparse.OptionArg("mode", long="mode", requires=["missing"]), + @argparse.Command("demo", options=[ + OptionArg("mode", long="mode", requires=["missing"]), ]).parse(argv=["--mode", "fast"], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1730,8 +1701,8 @@ test "arg validation catches unknown requires target" { ///| test "arg validation catches unknown conflicts_with target" { try - @argparse.Command("demo", args=[ - @argparse.OptionArg("mode", long="mode", conflicts_with=["missing"]), + @argparse.Command("demo", options=[ + OptionArg("mode", long="mode", conflicts_with=["missing"]), ]).parse(argv=["--mode", "fast"], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -1747,10 +1718,7 @@ test "empty groups without presence do not fail" { let grouped_ok = @argparse.Command( "demo", groups=[ArgGroup("left", args=["l"]), ArgGroup("right", args=["r"])], - args=[ - @argparse.FlagArg("l", long="left"), - @argparse.FlagArg("r", long="right"), - ], + flags=[FlagArg("l", long="left"), FlagArg("r", long="right")], ) let parsed = grouped_ok.parse(argv=["--left"], env=empty_env()) catch { _ => panic() @@ -1760,19 +1728,14 @@ test "empty groups without presence do not fail" { ///| test "help rendering edge paths stay stable" { - let required_many = @argparse.Command("demo", args=[ - @argparse.PositionalArg( - "files", - index=0, - required=true, - num_args=ValueRange(lower=1), - ), + let required_many = @argparse.Command("demo", positionals=[ + PositionalArg("files", index=0, required=true, num_args=ValueRange(lower=1)), ]) let required_help = required_many.render_help() assert_true(required_help.has_prefix("Usage: demo ")) - let short_only_builtin = @argparse.Command("demo", args=[ - @argparse.OptionArg("helpopt", long="help"), + let short_only_builtin = @argparse.Command("demo", options=[ + OptionArg("helpopt", long="help"), ]) let short_only_text = short_only_builtin.render_help() assert_true(short_only_text.has_prefix("Usage: demo")) @@ -1789,8 +1752,8 @@ test "help rendering edge paths stay stable" { _ => panic() } - let long_only_builtin = @argparse.Command("demo", args=[ - @argparse.FlagArg("custom_h", short='h'), + let long_only_builtin = @argparse.Command("demo", flags=[ + FlagArg("custom_h", short='h'), ]) let long_only_text = long_only_builtin.render_help() assert_true(long_only_text.has_prefix("Usage: demo")) @@ -1813,8 +1776,8 @@ test "help rendering edge paths stay stable" { let empty_options_help = empty_options.render_help() assert_true(empty_options_help.has_prefix("Usage: demo")) - let implicit_group = @argparse.Command("demo", args=[ - @argparse.PositionalArg("item", index=0), + let implicit_group = @argparse.Command("demo", positionals=[ + PositionalArg("item", index=0), ]) let implicit_group_help = implicit_group.render_help() assert_true(implicit_group_help.has_prefix("Usage: demo [item]")) @@ -1872,15 +1835,15 @@ test "parse error formatting covers public variants" { ///| test "options require one value per occurrence" { - let with_value = @argparse.Command("demo", args=[ - @argparse.OptionArg("tag", long="tag"), + let with_value = @argparse.Command("demo", options=[ + OptionArg("tag", long="tag"), ]).parse(argv=["--tag", "x"], env=empty_env()) catch { _ => panic() } assert_true(with_value.values is { "tag": ["x"], .. }) try - @argparse.Command("demo", args=[@argparse.OptionArg("tag", long="tag")]).parse( + @argparse.Command("demo", options=[OptionArg("tag", long="tag")]).parse( argv=["--tag"], env=empty_env(), ) @@ -1894,9 +1857,8 @@ test "options require one value per occurrence" { ///| test "short options require one value before next option token" { - let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("x", short='x'), - @argparse.FlagArg("verbose", short='v'), + let cmd = @argparse.Command("demo", flags=[FlagArg("verbose", short='v')], options=[ + OptionArg("x", short='x'), ]) let ok = cmd.parse(argv=["-x", "a", "-v"], env=empty_env()) catch { _ => panic() @@ -1914,9 +1876,9 @@ test "short options require one value before next option token" { ///| test "version action dispatches on custom long and short flags" { - let cmd = @argparse.Command("demo", version="2.0.0", args=[ - @argparse.FlagArg("show_long", long="show-version", action=Version), - @argparse.FlagArg("show_short", short='S', action=Version), + let cmd = @argparse.Command("demo", version="2.0.0", flags=[ + FlagArg("show_long", long="show-version", action=Version), + FlagArg("show_short", short='S', action=Version), ]) try cmd.parse(argv=["--show-version"], env=empty_env()) catch { @@ -1939,8 +1901,8 @@ test "global version action keeps parent version text in subcommand context" { let cmd = @argparse.Command( "demo", version="1.0.0", - args=[ - @argparse.FlagArg( + flags=[ + FlagArg( "show_version", short='S', long="show-version", @@ -1975,8 +1937,8 @@ test "global version action keeps parent version text in subcommand context" { ///| test "required and env-fed ranged values validate after parsing" { - let required_cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("input", long="input", required=true), + let required_cmd = @argparse.Command("demo", options=[ + OptionArg("input", long="input", required=true), ]) try required_cmd.parse(argv=[], env=empty_env()) catch { @argparse.ArgParseError::MissingRequired(name) => @@ -1986,8 +1948,8 @@ test "required and env-fed ranged values validate after parsing" { _ => panic() } - let env_min_cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("pair", long="pair", env="PAIR"), + let env_min_cmd = @argparse.Command("demo", options=[ + OptionArg("pair", long="pair", env="PAIR"), ]) let env_value = env_min_cmd.parse(argv=[], env={ "PAIR": "one" }) catch { _ => panic() @@ -1998,14 +1960,10 @@ test "required and env-fed ranged values validate after parsing" { ///| test "positionals honor explicit index sorting with last ranged positional" { - let cmd = @argparse.Command("demo", args=[ - @argparse.PositionalArg( - "late", - index=2, - num_args=ValueRange(lower=2, upper=2), - ), - @argparse.PositionalArg("first", index=0), - @argparse.PositionalArg("mid", index=1), + let cmd = @argparse.Command("demo", positionals=[ + PositionalArg("late", index=2, num_args=ValueRange(lower=2, upper=2)), + PositionalArg("first", index=0), + PositionalArg("mid", index=1), ]) let parsed = cmd.parse(argv=["a", "b", "c", "d"], env=empty_env()) catch { @@ -2018,12 +1976,8 @@ test "positionals honor explicit index sorting with last ranged positional" { ///| test "positional num_args lower bound rejects missing argv values" { - let cmd = @argparse.Command("demo", args=[ - @argparse.PositionalArg( - "first", - index=0, - num_args=ValueRange(lower=2, upper=3), - ), + let cmd = @argparse.Command("demo", positionals=[ + PositionalArg("first", index=0, num_args=ValueRange(lower=2, upper=3)), ]) try cmd.parse(argv=[], env=empty_env()) catch { @@ -2040,9 +1994,9 @@ test "positional num_args lower bound rejects missing argv values" { ///| test "positional max clamp leaves trailing value for next positional" { - let cmd = @argparse.Command("demo", args=[ - @argparse.PositionalArg("items", num_args=ValueRange(lower=0, upper=2)), - @argparse.PositionalArg("tail"), + let cmd = @argparse.Command("demo", positionals=[ + PositionalArg("items", num_args=ValueRange(lower=0, upper=2)), + PositionalArg("tail"), ]) let parsed = cmd.parse(argv=["a", "b", "c"], env=empty_env()) catch { @@ -2053,12 +2007,15 @@ test "positional max clamp leaves trailing value for next positional" { ///| test "options with allow_hyphen_values accept option-like single values" { - let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("arg", long="arg", allow_hyphen_values=true), - @argparse.FlagArg("verbose", long="verbose"), - @argparse.FlagArg("cache", long="cache", negatable=true), - @argparse.FlagArg("quiet", short='q'), - ]) + let cmd = @argparse.Command( + "demo", + flags=[ + FlagArg("verbose", long="verbose"), + FlagArg("cache", long="cache", negatable=true), + FlagArg("quiet", short='q'), + ], + options=[OptionArg("arg", long="arg", allow_hyphen_values=true)], + ) let known_long = cmd.parse(argv=["--arg", "--verbose"], env=empty_env()) catch { _ => panic() @@ -2086,15 +2043,18 @@ test "options with allow_hyphen_values accept option-like single values" { assert_true(known_short.values is { "arg": ["-q"], .. }) assert_true(known_short.flags is { "quiet"? : None, .. }) - let cmd_with_rest = @argparse.Command("demo", args=[ - @argparse.OptionArg("arg", long="arg", allow_hyphen_values=true), - @argparse.PositionalArg( - "rest", - index=0, - num_args=ValueRange(lower=0), - allow_hyphen_values=true, - ), - ]) + let cmd_with_rest = @argparse.Command( + "demo", + options=[OptionArg("arg", long="arg", allow_hyphen_values=true)], + positionals=[ + PositionalArg( + "rest", + index=0, + num_args=ValueRange(lower=0), + allow_hyphen_values=true, + ), + ], + ) let sentinel_stop = cmd_with_rest.parse( argv=["--arg", "x", "--", "tail"], env=empty_env(), @@ -2106,10 +2066,11 @@ test "options with allow_hyphen_values accept option-like single values" { ///| test "single-value options avoid consuming additional option values" { - let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("one", long="one"), - @argparse.FlagArg("verbose", long="verbose"), - ]) + let cmd = @argparse.Command( + "demo", + flags=[FlagArg("verbose", long="verbose")], + options=[OptionArg("one", long="one")], + ) let parsed = cmd.parse(argv=["--one", "x", "--verbose"], env=empty_env()) catch { _ => panic() @@ -2120,10 +2081,11 @@ test "single-value options avoid consuming additional option values" { ///| test "missing option values are reported when next token is another option" { - let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("arg", long="arg"), - @argparse.FlagArg("verbose", long="verbose"), - ]) + let cmd = @argparse.Command( + "demo", + flags=[FlagArg("verbose", long="verbose")], + options=[OptionArg("arg", long="arg")], + ) let ok = cmd.parse(argv=["--arg", "x", "--verbose"], env=empty_env()) catch { _ => panic() @@ -2141,9 +2103,7 @@ test "missing option values are reported when next token is another option" { ///| test "short-only set options use short label in duplicate errors" { - let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("mode", short='m'), - ]) + let cmd = @argparse.Command("demo", options=[OptionArg("mode", short='m')]) try cmd.parse(argv=["-m", "a", "-m", "b"], env=empty_env()) catch { @argparse.ArgParseError::InvalidArgument(msg) => inspect(msg, content="argument '-m' cannot be used multiple times") @@ -2155,8 +2115,8 @@ test "short-only set options use short label in duplicate errors" { ///| test "unknown short suggestion can be absent" { - let cmd = @argparse.Command("demo", disable_help_flag=true, args=[ - @argparse.OptionArg("name", long="name"), + let cmd = @argparse.Command("demo", disable_help_flag=true, options=[ + OptionArg("name", long="name"), ]) try cmd.parse(argv=["-x"], env=empty_env()) catch { @@ -2172,8 +2132,8 @@ test "unknown short suggestion can be absent" { ///| test "setfalse flags apply false when present" { - let cmd = @argparse.Command("demo", args=[ - @argparse.FlagArg("failfast", long="failfast", action=SetFalse), + let cmd = @argparse.Command("demo", flags=[ + FlagArg("failfast", long="failfast", action=SetFalse), ]) let parsed = cmd.parse(argv=["--failfast"], env=empty_env()) catch { _ => panic() @@ -2184,9 +2144,8 @@ test "setfalse flags apply false when present" { ///| test "allow_hyphen positional treats unknown long token as value" { - let cmd = @argparse.Command("demo", args=[ - @argparse.PositionalArg("input", index=0, allow_hyphen_values=true), - @argparse.FlagArg("known", long="known"), + let cmd = @argparse.Command("demo", flags=[FlagArg("known", long="known")], positionals=[ + PositionalArg("input", index=0, allow_hyphen_values=true), ]) let parsed = cmd.parse(argv=["--mystery"], env=empty_env()) catch { _ => panic() @@ -2198,14 +2157,9 @@ test "allow_hyphen positional treats unknown long token as value" { test "global value from child default is merged back to parent" { let cmd = @argparse.Command( "demo", - args=[ - @argparse.OptionArg( - "mode", - long="mode", - default_values=["safe"], - global=true, - ), - @argparse.OptionArg("unused", long="unused", global=true), + options=[ + OptionArg("mode", long="mode", default_values=["safe"], global=true), + OptionArg("unused", long="unused", global=true), ], subcommands=[Command("run")], ) @@ -2224,18 +2178,11 @@ test "global value from child default is merged back to parent" { test "child global arg with inherited global name updates parent global" { let cmd = @argparse.Command( "demo", - args=[ - @argparse.OptionArg( - "mode", - long="mode", - default_values=["safe"], - global=true, - ), + options=[ + OptionArg("mode", long="mode", default_values=["safe"], global=true), ], subcommands=[ - Command("run", args=[ - @argparse.OptionArg("mode", long="mode", global=true), - ]), + Command("run", options=[OptionArg("mode", long="mode", global=true)]), ], ) @@ -2256,8 +2203,8 @@ test "child local arg shadowing inherited global is rejected at build time" { try @argparse.Command( "demo", - args=[ - @argparse.OptionArg( + options=[ + OptionArg( "mode", long="mode", env="MODE", @@ -2265,9 +2212,7 @@ test "child local arg shadowing inherited global is rejected at build time" { global=true, ), ], - subcommands=[ - Command("run", args=[@argparse.OptionArg("mode", long="mode")]), - ], + subcommands=[Command("run", options=[OptionArg("mode", long="mode")])], ).parse(argv=["run"], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => @@ -2282,14 +2227,8 @@ test "child local arg shadowing inherited global is rejected at build time" { test "global append env value from child is merged back to parent" { let cmd = @argparse.Command( "demo", - args=[ - @argparse.OptionArg( - "tag", - long="tag", - action=Append, - env="TAG", - global=true, - ), + options=[ + OptionArg("tag", long="tag", action=Append, env="TAG", global=true), ], subcommands=[Command("run")], ) @@ -2310,7 +2249,7 @@ test "global append env value from child is merged back to parent" { test "global flag set in child argv is merged back to parent" { let cmd = @argparse.Command( "demo", - args=[@argparse.FlagArg("verbose", long="verbose", global=true)], + flags=[FlagArg("verbose", long="verbose", global=true)], subcommands=[Command("run")], ) @@ -2330,8 +2269,8 @@ test "global flag set in child argv is merged back to parent" { test "global count negation after subcommand resets merged state" { let cmd = @argparse.Command( "demo", - args=[ - @argparse.FlagArg( + flags=[ + FlagArg( "verbose", long="verbose", action=Count, @@ -2363,7 +2302,7 @@ test "global count negation after subcommand resets merged state" { test "global set option rejects duplicate occurrences across subcommands" { let cmd = @argparse.Command( "demo", - args=[@argparse.OptionArg("mode", long="mode", global=true)], + options=[OptionArg("mode", long="mode", global=true)], subcommands=[Command("run")], ) try @@ -2384,13 +2323,9 @@ test "global override with incompatible inherited type is rejected" { try @argparse.Command( "demo", - args=[ - @argparse.OptionArg("mode", long="mode", required=true, global=true), - ], + options=[OptionArg("mode", long="mode", required=true, global=true)], subcommands=[ - Command("run", args=[ - @argparse.FlagArg("mode", long="mode", global=true), - ]), + Command("run", flags=[FlagArg("mode", long="mode", global=true)]), ], ).parse(argv=["run", "--mode"], env=empty_env()) catch { @@ -2407,10 +2342,8 @@ test "child local long alias collision with inherited global is rejected" { try @argparse.Command( "demo", - args=[@argparse.FlagArg("verbose", long="verbose", global=true)], - subcommands=[ - Command("run", args=[@argparse.OptionArg("local", long="verbose")]), - ], + flags=[FlagArg("verbose", long="verbose", global=true)], + subcommands=[Command("run", options=[OptionArg("local", long="verbose")])], ).parse(argv=["run", "--verbose"], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => { @@ -2428,10 +2361,8 @@ test "child local short alias collision with inherited global is rejected" { try @argparse.Command( "demo", - args=[@argparse.FlagArg("verbose", short='v', global=true)], - subcommands=[ - Command("run", args=[@argparse.OptionArg("local", short='v')]), - ], + flags=[FlagArg("verbose", short='v', global=true)], + subcommands=[Command("run", options=[OptionArg("local", short='v')])], ).parse(argv=["run", "-v"], env=empty_env()) catch { @argparse.ArgBuildError::Unsupported(msg) => { @@ -2450,7 +2381,7 @@ test "nested subcommands inherit finalized globals from ancestors" { let mid = @argparse.Command("mid", subcommands=[leaf]) let cmd = @argparse.Command( "demo", - args=[@argparse.FlagArg("verbose", long="verbose", global=true)], + flags=[FlagArg("verbose", long="verbose", global=true)], subcommands=[mid], ) @@ -2469,17 +2400,15 @@ test "nested subcommands inherit finalized globals from ancestors" { ///| test "non-bmp short option token does not panic" { - let cmd = @argparse.Command("demo", args=[ - @argparse.FlagArg("party", short='🎉'), - ]) + let cmd = @argparse.Command("demo", flags=[FlagArg("party", short='🎉')]) let parsed = cmd.parse(argv=["-🎉"], env=empty_env()) catch { _ => panic() } assert_true(parsed.flags is { "party": true, .. }) } ///| test "non-bmp hyphen token reports unknown argument without panic" { - let cmd = @argparse.Command("demo", args=[ - @argparse.PositionalArg("value", index=0), + let cmd = @argparse.Command("demo", positionals=[ + PositionalArg("value", index=0), ]) try cmd.parse(argv=["-🎉"], env=empty_env()) catch { @argparse.ArgParseError::UnknownArgument(arg, hint) => { @@ -2494,8 +2423,8 @@ test "non-bmp hyphen token reports unknown argument without panic" { ///| test "option env values remain string values instead of flags" { - let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("mode", long="mode", env="MODE"), + let cmd = @argparse.Command("demo", options=[ + OptionArg("mode", long="mode", env="MODE"), ]) let parsed = cmd.parse(argv=[], env={ "MODE": "fast" }) catch { _ => panic() } assert_true(parsed.values is { "mode": ["fast"], .. }) diff --git a/argparse/argparse_test.mbt b/argparse/argparse_test.mbt index fca3e2dc9..8be1612f9 100644 --- a/argparse/argparse_test.mbt +++ b/argparse/argparse_test.mbt @@ -19,11 +19,12 @@ fn empty_env() -> Map[String, String] { ///| test "declarative parse basics" { - let cmd = @argparse.Command("demo", args=[ - @argparse.FlagArg("verbose", short='v', long="verbose"), - @argparse.OptionArg("count", long="count", env="COUNT"), - @argparse.PositionalArg("name", index=0), - ]) + let cmd = @argparse.Command( + "demo", + flags=[FlagArg("verbose", short='v', long="verbose")], + options=[OptionArg("count", long="count", env="COUNT")], + positionals=[PositionalArg("name", index=0)], + ) let matches = cmd.parse(argv=["-v", "--count", "3", "alice"], env=empty_env()) catch { _ => panic() } @@ -85,9 +86,9 @@ test "parse error show is readable" { ///| test "relationships and num args" { - let requires_cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("mode", long="mode", requires=["config"]), - @argparse.OptionArg("config", long="config"), + let requires_cmd = @argparse.Command("demo", options=[ + OptionArg("mode", long="mode", requires=["config"]), + OptionArg("config", long="config"), ]) try requires_cmd.parse(argv=["--mode", "fast"], env=empty_env()) catch { @@ -98,8 +99,8 @@ test "relationships and num args" { _ => panic() } - let appended = @argparse.Command("demo", args=[ - @argparse.OptionArg("tag", long="tag", action=Append), + let appended = @argparse.Command("demo", options=[ + OptionArg("tag", long="tag", action=Append), ]).parse(argv=["--tag", "a", "--tag", "b", "--tag", "c"], env=empty_env()) catch { _ => panic() } @@ -113,10 +114,7 @@ test "arg groups required and multiple" { groups=[ ArgGroup("mode", required=true, multiple=false, args=["fast", "slow"]), ], - args=[ - @argparse.FlagArg("fast", long="fast"), - @argparse.FlagArg("slow", long="slow"), - ], + flags=[FlagArg("fast", long="fast"), FlagArg("slow", long="slow")], ) try cmd.parse(argv=[], env=empty_env()) catch { @@ -143,10 +141,7 @@ test "arg groups requires and conflicts" { ArgGroup("mode", args=["fast"], requires=["output"]), ArgGroup("output", args=["json"]), ], - args=[ - @argparse.FlagArg("fast", long="fast"), - @argparse.FlagArg("json", long="json"), - ], + flags=[FlagArg("fast", long="fast"), FlagArg("json", long="json")], ) try requires_cmd.parse(argv=["--fast"], env=empty_env()) catch { @@ -163,10 +158,7 @@ test "arg groups requires and conflicts" { ArgGroup("mode", args=["fast"], conflicts_with=["output"]), ArgGroup("output", args=["json"]), ], - args=[ - @argparse.FlagArg("fast", long="fast"), - @argparse.FlagArg("json", long="json"), - ], + flags=[FlagArg("fast", long="fast"), FlagArg("json", long="json")], ) try conflict_cmd.parse(argv=["--fast", "--json"], env=empty_env()) catch { @@ -185,8 +177,8 @@ test "arg groups requires and conflicts" { ///| test "subcommand parsing" { - let echo = @argparse.Command("echo", args=[ - @argparse.PositionalArg("msg", index=0), + let echo = @argparse.Command("echo", positionals=[ + PositionalArg("msg", index=0), ]) let root = @argparse.Command("root", subcommands=[echo]) @@ -204,18 +196,15 @@ test "full help snapshot" { let cmd = @argparse.Command( "demo", about="Demo command", - args=[ - @argparse.FlagArg( - "verbose", - short='v', - long="verbose", - about="Enable verbose mode", - ), - @argparse.OptionArg("count", long="count", about="Repeat count", default_values=[ + flags=[ + FlagArg("verbose", short='v', long="verbose", about="Enable verbose mode"), + ], + options=[ + OptionArg("count", long="count", about="Repeat count", default_values=[ "1", ]), - @argparse.PositionalArg("name", index=0, about="Target name"), ], + positionals=[PositionalArg("name", index=0, about="Target name")], subcommands=[Command("echo", about="Echo a message")], ) inspect( @@ -243,8 +232,8 @@ test "full help snapshot" { ///| test "value source precedence argv env default" { - let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("level", long="level", env="LEVEL", default_values=["1"]), + let cmd = @argparse.Command("demo", options=[ + OptionArg("level", long="level", env="LEVEL", default_values=["1"]), ]) let from_default = cmd.parse(argv=[], env=empty_env()) catch { _ => panic() } @@ -264,8 +253,8 @@ test "value source precedence argv env default" { ///| test "omitted env does not read process environment by default" { - let cmd = @argparse.Command("demo", args=[ - @argparse.OptionArg("count", long="count", env="COUNT"), + let cmd = @argparse.Command("demo", options=[ + OptionArg("count", long="count", env="COUNT"), ]) let matches = cmd.parse(argv=[]) catch { _ => panic() } assert_true(matches.values is { "count"? : None, .. }) @@ -277,9 +266,9 @@ test "options and multiple values" { let serve = @argparse.Command("serve") let cmd = @argparse.Command( "demo", - args=[ - @argparse.OptionArg("count", short='c', long="count"), - @argparse.OptionArg("tag", long="tag", action=Append), + options=[ + OptionArg("count", short='c', long="count"), + OptionArg("tag", long="tag", action=Append), ], subcommands=[serve], ) @@ -307,16 +296,11 @@ test "options and multiple values" { ///| test "negatable and conflicts" { - let cmd = @argparse.Command("demo", args=[ - @argparse.FlagArg("cache", long="cache", negatable=true), - @argparse.FlagArg( - "failfast", - long="failfast", - action=SetFalse, - negatable=true, - ), - @argparse.FlagArg("verbose", long="verbose", conflicts_with=["quiet"]), - @argparse.FlagArg("quiet", long="quiet"), + let cmd = @argparse.Command("demo", flags=[ + FlagArg("cache", long="cache", negatable=true), + FlagArg("failfast", long="failfast", action=SetFalse, negatable=true), + FlagArg("verbose", long="verbose", conflicts_with=["quiet"]), + FlagArg("quiet", long="quiet"), ]) let no_cache = cmd.parse(argv=["--no-cache"], env=empty_env()) catch { @@ -346,9 +330,7 @@ test "negatable and conflicts" { ///| test "flag does not accept inline value" { - let cmd = @argparse.Command("demo", args=[ - @argparse.FlagArg("verbose", long="verbose"), - ]) + let cmd = @argparse.Command("demo", flags=[FlagArg("verbose", long="verbose")]) try cmd.parse(argv=["--verbose=true"], env=empty_env()) catch { @argparse.ArgParseError::InvalidArgument(arg) => inspect(arg, content="--verbose=true") diff --git a/argparse/command.mbt b/argparse/command.mbt index 71290f682..bc46be46d 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -32,7 +32,9 @@ pub struct Command { /// Create a declarative command specification. fn new( name : StringView, - args? : ArrayView[&ArgLike], + flags? : ArrayView[FlagArg], + options? : ArrayView[OptionArg], + positionals? : ArrayView[PositionalArg], subcommands? : ArrayView[Command], about? : StringView, version? : StringView, @@ -50,12 +52,14 @@ pub struct Command { /// Create a declarative command specification. /// /// Notes: -/// - `args` accepts `FlagArg` / `OptionArg` / `PositionalArg` via `ArgLike`. +/// - `flags` / `options` / `positionals` declare command arguments by kind. /// - `groups` explicitly declares all group memberships and policies. /// - Built-in `--help`/`--version` behavior can be disabled with the flags below. pub fn Command::new( name : StringView, - args? : ArrayView[&ArgLike] = [], + flags? : ArrayView[FlagArg] = [], + options? : ArrayView[OptionArg] = [], + positionals? : ArrayView[PositionalArg] = [], subcommands? : ArrayView[Command] = [], about? : StringView, version? : StringView, @@ -67,7 +71,7 @@ pub fn Command::new( hidden? : Bool = false, groups? : ArrayView[ArgGroup] = [], ) -> Command { - let (parsed_args, arg_error) = collect_args(args) + let (parsed_args, arg_error) = collect_args(flags, options, positionals) let groups = groups.to_array() let cmd = Command::{ name: name.to_string(), @@ -209,12 +213,35 @@ fn find_decl_subcommand(subs : Array[Command], name : String) -> Command? { } ///| -fn collect_args(specs : ArrayView[&ArgLike]) -> (Array[Arg], ArgBuildError?) { - let args = specs.map(spec => spec.to_arg()) +fn collect_args( + flags : ArrayView[FlagArg], + options : ArrayView[OptionArg], + positionals : ArrayView[PositionalArg], +) -> (Array[Arg], ArgBuildError?) { + let args : Array[Arg] = [] + for flag in flags { + args.push(flag.arg) + } + for option in options { + args.push(option.arg) + } + for positional in positionals { + args.push(positional.arg) + } let ctx = ValidationCtx::new() let mut first_error : ArgBuildError? = None - for spec in specs { - spec.validate(ctx) catch { + for flag in flags { + validate_flag_arg(flag.arg, ctx) catch { + err => if first_error is None { first_error = Some(err) } + } + } + for option in options { + validate_option_arg(option.arg, ctx) catch { + err => if first_error is None { first_error = Some(err) } + } + } + for positional in positionals { + validate_positional_arg(positional.arg, ctx) catch { err => if first_error is None { first_error = Some(err) } } } diff --git a/argparse/pkg.generated.mbti b/argparse/pkg.generated.mbti index 62f2c99a5..bcbe51bcb 100644 --- a/argparse/pkg.generated.mbti +++ b/argparse/pkg.generated.mbti @@ -45,9 +45,9 @@ pub fn ArgGroup::new(StringView, required? : Bool, multiple? : Bool, args? : Arr pub struct Command { // private fields - fn new(StringView, args? : ArrayView[&ArgLike], 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]) -> Command + fn new(StringView, flags? : ArrayView[FlagArg], options? : ArrayView[OptionArg], positionals? : ArrayView[PositionalArg], 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]) -> Command } -pub fn Command::new(StringView, args? : ArrayView[&ArgLike], subcommands? : ArrayView[Self], 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]) -> Self +pub fn Command::new(StringView, flags? : ArrayView[FlagArg], options? : ArrayView[OptionArg], positionals? : ArrayView[PositionalArg], subcommands? : ArrayView[Self], 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]) -> Self pub fn Command::parse(Self, argv? : ArrayView[String], env? : Map[String, String]) -> Matches raise pub fn Command::render_help(Self) -> String @@ -67,7 +67,6 @@ pub struct FlagArg { fn new(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 } pub fn FlagArg::new(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) -> Self -pub impl ArgLike for FlagArg pub struct Matches { flags : Map[String, Bool] @@ -91,7 +90,6 @@ pub struct OptionArg { fn new(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 } pub fn OptionArg::new(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) -> Self -pub impl ArgLike for OptionArg pub struct PositionalArg { // private fields @@ -99,7 +97,6 @@ pub struct PositionalArg { fn new(StringView, index? : Int, about? : StringView, env? : StringView, default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : ArrayView[String], conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, hidden? : Bool) -> PositionalArg } pub fn PositionalArg::new(StringView, index? : Int, about? : StringView, env? : StringView, default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : ArrayView[String], conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, hidden? : Bool) -> Self -pub impl ArgLike for PositionalArg pub struct ValueRange { // private fields @@ -122,8 +119,6 @@ pub impl Show for ValueSource // Type aliases // Traits -trait ArgLike - pub(open) trait FromMatches { from_matches(Matches) -> Self raise ArgParseError } From 93454f2c77a0c369176754733e731260279fec42 Mon Sep 17 00:00:00 2001 From: zihang Date: Fri, 27 Feb 2026 13:49:49 +0800 Subject: [PATCH 16/40] Fix global override merge edge cases in argparse --- argparse/argparse_blackbox_test.mbt | 76 +++++++++++++++++++++++++++++ argparse/parser.mbt | 17 ++++++- argparse/parser_validate.mbt | 5 +- 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 60558f238..d4752e0ae 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -2431,3 +2431,79 @@ test "option env values remain string values instead of flags" { assert_true(parsed.flags.get("mode") is None) assert_true(parsed.sources is { "mode": Env, .. }) } + +///| +test "nested global override deduplicates count merge by name" { + let leaf = @argparse.Command("leaf") + let mid = @argparse.Command( + "mid", + flags=[FlagArg("verbose", long="verbose", action=Count, global=true)], + subcommands=[leaf], + ) + let root = @argparse.Command( + "root", + flags=[FlagArg("verbose", long="verbose", action=Count, global=true)], + subcommands=[mid], + ) + + let parsed = root.parse(argv=["mid", "leaf", "--verbose"], env=empty_env()) catch { + _ => panic() + } + assert_true(parsed.flag_counts is { "verbose": 1, .. }) + assert_true( + parsed.subcommand is Some(("mid", sub_mid)) && + sub_mid.flag_counts is { "verbose": 1, .. } && + sub_mid.subcommand is Some(("leaf", sub_leaf)) && + sub_leaf.flag_counts is { "verbose": 1, .. }, + ) +} + +///| +test "nested global override keeps single set value without false duplicate error" { + let leaf = @argparse.Command("leaf") + let mid = @argparse.Command( + "mid", + options=[OptionArg("mode", long="mode", global=true)], + subcommands=[leaf], + ) + let root = @argparse.Command( + "root", + options=[OptionArg("mode", long="mode", global=true)], + subcommands=[mid], + ) + + let parsed = root.parse( + argv=["mid", "leaf", "--mode", "fast"], + env=empty_env(), + ) catch { + _ => panic() + } + assert_true(parsed.values is { "mode": ["fast"], .. }) + assert_true( + parsed.subcommand is Some(("mid", sub_mid)) && + sub_mid.values is { "mode": ["fast"], .. } && + sub_mid.subcommand is Some(("leaf", sub_leaf)) && + sub_leaf.values is { "mode": ["fast"], .. }, + ) +} + +///| +test "global override with different negatable setting is rejected" { + try + @argparse.Command( + "demo", + flags=[FlagArg("verbose", long="verbose", negatable=true, global=true)], + subcommands=[ + Command("run", flags=[ + FlagArg("verbose", long="verbose", negatable=false, global=true), + ]), + ], + ).parse(argv=["run"], env=empty_env()) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + assert_true(msg.contains("incompatible")) + _ => panic() + } noraise { + _ => panic() + } +} diff --git a/argparse/parser.mbt b/argparse/parser.mbt index eb1d670de..261d73ef1 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -78,6 +78,21 @@ fn default_argv() -> Array[String] { } } +///| +fn merge_global_defs( + inherited_globals : Array[Arg], + globals_here : Array[Arg], +) -> Array[Arg] { + let merged = inherited_globals.copy() + for global in globals_here { + match merged.search_by(arg => arg.name == global.name) { + Some(idx) => merged[idx] = global + None => merged.push(global) + } + } + merged +} + ///| fn parse_command( cmd : Command, @@ -99,7 +114,7 @@ fn parse_command( } let matches = new_matches_parse_state() let globals_here = collect_globals(args) - let child_globals = inherited_globals + globals_here + let child_globals = merge_global_defs(inherited_globals, globals_here) let child_version_long = inherited_version_long.copy() let child_version_short = inherited_version_short.copy() for global in globals_here { diff --git a/argparse/parser_validate.mbt b/argparse/parser_validate.mbt index 6effa2543..5ee7906d0 100644 --- a/argparse/parser_validate.mbt +++ b/argparse/parser_validate.mbt @@ -191,9 +191,10 @@ fn merge_inherited_globals( ///| fn global_override_compatible(inherited_arg : Arg, arg : Arg) -> Bool { match inherited_arg.info { - Flag(action=inherited_action, ..) => + Flag(action=inherited_action, negatable=inherited_negatable, ..) => match arg.info { - Flag(action~, ..) => inherited_action == action + Flag(action~, negatable~, ..) => + inherited_action == action && inherited_negatable == negatable _ => false } Option(action=inherited_action, ..) => From e530896211425a8b68c02638f8b9435ce76bba49 Mon Sep 17 00:00:00 2001 From: zihang Date: Fri, 27 Feb 2026 14:34:43 +0800 Subject: [PATCH 17/40] fix(argparse): infer positional indices for mixed positionals --- argparse/README.mbt.md | 5 ++-- argparse/arg_spec.mbt | 5 ++-- argparse/argparse_blackbox_test.mbt | 34 +++++++++++++++++++++++++++ argparse/parser_positionals.mbt | 36 +++++++++++++++++++---------- argparse/parser_validate.mbt | 21 +++++++++++++++++ 5 files changed, 85 insertions(+), 16 deletions(-) diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index 8d4d350d1..9bbf24fb4 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -10,8 +10,9 @@ predictable subset of its behavior. 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. +- Positionals without `index` get inferred indices in declaration order. +- All positionals are ordered by the resolved index. +- Positional indices cannot skip values. - 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 diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt index 33756451b..04dc975c6 100644 --- a/argparse/arg_spec.mbt +++ b/argparse/arg_spec.mbt @@ -249,8 +249,9 @@ pub struct PositionalArg { /// /// Positional ordering: /// - `index` is zero-based. -/// - Indexed positionals are sorted by `index`. -/// - Unindexed positionals are appended after indexed ones in declaration order. +/// - Positionals without `index` get inferred indices in declaration order. +/// - All positionals are ordered by the resolved index. +/// - Positional indices cannot skip values. /// /// `num_args` controls the accepted value count. /// diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index d4752e0ae..87dc00715 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -1974,6 +1974,40 @@ test "positionals honor explicit index sorting with last ranged positional" { ) } +///| +test "mixed indexed and unindexed positionals keep inferred order" { + let cmd = @argparse.Command("demo", positionals=[ + PositionalArg("first"), + PositionalArg("second", index=1), + ]) + + let parsed = cmd.parse(argv=["a", "b"], env=empty_env()) catch { + _ => panic() + } + assert_true(parsed.values is { "first": ["a"], "second": ["b"], .. }) +} + +///| +test "positional indices cannot skip values" { + try + @argparse.Command("demo", positionals=[PositionalArg("late", index=1)]).parse( + argv=["x"], + env=empty_env(), + ) + catch { + @argparse.ArgBuildError::Unsupported(msg) => + inspect( + msg, + content=( + #|positional indices cannot skip values; missing index: 0 + ), + ) + _ => panic() + } noraise { + _ => panic() + } +} + ///| test "positional num_args lower bound rejects missing argv values" { let cmd = @argparse.Command("demo", positionals=[ diff --git a/argparse/parser_positionals.mbt b/argparse/parser_positionals.mbt index 9dc69c035..847c8986a 100644 --- a/argparse/parser_positionals.mbt +++ b/argparse/parser_positionals.mbt @@ -13,27 +13,39 @@ // limitations under the License. ///| -fn positional_args(args : Array[Arg]) -> Array[Arg] { - let with_index = [] - let without_index = [] +fn positional_indexed_args(args : Array[Arg]) -> Array[(Int, Arg)] { + let entries = [] + let mut next_index = 0 for arg in args { if arg.info is Positional(index~, ..) { - if index is Some(idx) { - with_index.push((idx, arg)) - } else { - without_index.push(arg) + let resolved = match index { + Some(value) => { + let candidate = value + 1 + if candidate > next_index { + next_index = candidate + } + value + } + None => { + let value = next_index + next_index = value + 1 + value + } } + entries.push((resolved, arg)) } } - with_index.sort_by_key(pair => pair.0) + entries.sort_by_key(pair => pair.0) + entries +} + +///| +fn positional_args(args : Array[Arg]) -> Array[Arg] { let ordered = [] - for item in with_index { + for item in positional_indexed_args(args) { let (_, arg) = item ordered.push(arg) } - for arg in without_index { - ordered.push(arg) - } ordered } diff --git a/argparse/parser_validate.mbt b/argparse/parser_validate.mbt index 5ee7906d0..1864bbf13 100644 --- a/argparse/parser_validate.mbt +++ b/argparse/parser_validate.mbt @@ -91,6 +91,7 @@ fn ValidationCtx::record_arg( ///| fn ValidationCtx::finalize(self : ValidationCtx) -> Unit raise ArgBuildError { validate_requires_conflicts_targets(self.args, self.seen_names) + validate_positional_index_layout(self.args) validate_indexed_positional_num_args(self.args) } @@ -304,6 +305,26 @@ fn validate_indexed_positional_num_args( } } +///| +fn validate_positional_index_layout( + args : Array[Arg], +) -> Unit raise ArgBuildError { + let indexed = positional_indexed_args(args) + let mut expected = 0 + for item in indexed { + let (index, _) = item + if index < expected { + raise Unsupported("duplicate positional index: \{index}") + } + if index > expected { + raise Unsupported( + "positional indices cannot skip values; missing index: \{expected}", + ) + } + expected = expected + 1 + } +} + ///| fn validate_flag_arg( arg : Arg, From e32633e17aeb3107ff19777f84e4b0a287c0a7be Mon Sep 17 00:00:00 2001 From: Hongbo Zhang Date: Sat, 28 Feb 2026 15:23:47 +0800 Subject: [PATCH 18/40] add debug support --- argparse/command.mbt | 1 + argparse/matches.mbt | 7 +++++-- argparse/moon.pkg | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/argparse/command.mbt b/argparse/command.mbt index bc46be46d..343837683 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -112,6 +112,7 @@ pub fn Command::render_help(self : Command) -> String { /// - Raises `ArgParseError` when user input does not satisfy the definition. /// /// Value precedence is `argv > env > default_values`. +#as_free_fn pub fn Command::parse( self : Command, argv? : ArrayView[String] = default_argv(), diff --git a/argparse/matches.mbt b/argparse/matches.mbt index e64954f0a..b172101a8 100644 --- a/argparse/matches.mbt +++ b/argparse/matches.mbt @@ -12,13 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +///| +using @debug {trait Debug, type Repr} + ///| /// Where a value/flag came from. pub enum ValueSource { Argv Env Default -} derive(Eq, Show) +} derive(Eq, Show, Debug) ///| /// Parse results for declarative commands. @@ -32,7 +35,7 @@ pub struct Matches { priv flag_sources : Map[String, ValueSource] priv value_sources : Map[String, ValueSource] priv mut parsed_subcommand : (String, Matches)? -} +} derive(Debug) ///| fn new_matches_parse_state() -> Matches { diff --git a/argparse/moon.pkg b/argparse/moon.pkg index 1a2c08f0e..160a04f4e 100644 --- a/argparse/moon.pkg +++ b/argparse/moon.pkg @@ -3,6 +3,7 @@ import { "moonbitlang/core/env", "moonbitlang/core/strconv", "moonbitlang/core/set", + "moonbitlang/core/debug", } warnings = "+unnecessary_annotation" From ef634b0bd6f1f2c39ad96dcd8f085e8f54ae967e Mon Sep 17 00:00:00 2001 From: Hongbo Zhang Date: Sat, 28 Feb 2026 15:23:57 +0800 Subject: [PATCH 19/40] add some comments --- argparse/README.mbt.md | 106 ++++++++++++++++++++++++++---------- argparse/pkg.generated.mbti | 7 +++ 2 files changed, 85 insertions(+), 28 deletions(-) diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index 9bbf24fb4..62bbb259a 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -43,37 +43,84 @@ test "name-only option is rejected" { ```mbt check ///| test "flag option positional" { - let cmd = @argparse.Command( - "demo", - flags=[FlagArg("verbose", short='v', long="verbose")], - options=[OptionArg("count", long="count")], - positionals=[PositionalArg("name", index=0)], + let matches = @argparse.parse( + Command( + "demo", + // FlagArg("verbose") + // OptionArg("count") + flags=[FlagArg("verbose", short='v', long="verbose")], + options=[OptionArg("count", long="count")], + // Test: 0 but passed args + // add docs 0,1,2, N + positionals=[PositionalArg("name", index=0)], + ), + argv=["-v", "--count", "2", "alice"], + ) + // CR: Map[String,Bool] -> Set[String]? + debug_inspect( + matches, + content=( + #|{ + #| flags: { "verbose": true }, + #| values: { "count": ["2"], "name": ["alice"] }, + #| flag_counts: {}, + #| sources: { "verbose": Argv, "count": Argv, "name": Argv }, + #| subcommand: None, + #| counts: {}, + #| flag_sources: {}, + #| value_sources: {}, + #| parsed_subcommand: None, + #|} + ), ) - 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", positionals=[ - PositionalArg("msg", index=0), - ]) - let cmd = @argparse.Command( - "demo", - flags=[FlagArg("verbose", short='v', long="verbose", global=true)], - subcommands=[echo], + let matches = @argparse.parse( + Command( + "demo", + flags=[FlagArg("verbose", short='v', long="verbose", global=true)], + subcommands=[ + Command("echo", positionals=[PositionalArg("msg", index=0)]), + Command("repeat", positionals=[PositionalArg("msg", index=0)], options=[ + OptionArg("count", long="count"), + ]), + ], + ), + argv=["--verbose", "echo", "hi"], ) - 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"], .. }, + // FIXME: (upstream) format introduced a new line + debug_inspect( + matches, + content=( + #|{ + #| flags: { "verbose": true }, + #| values: {}, + #| flag_counts: {}, + #| sources: { "verbose": Argv }, + #| subcommand: Some( + #| ( + #| "echo", + #| { + #| flags: { "verbose": true }, + #| values: { "msg": ["hi"] }, + #| flag_counts: {}, + #| sources: { "verbose": Argv, "msg": Argv }, + #| subcommand: None, + #| counts: {}, + #| flag_sources: {}, + #| value_sources: {}, + #| parsed_subcommand: None, + #| }, + #| ), + #| ), + #| counts: {}, + #| flag_sources: {}, + #| value_sources: {}, + #| parsed_subcommand: None, + #|} + ), ) } ``` @@ -93,7 +140,8 @@ test "help snapshot" { flags=[FlagArg("verbose", short='v', long="verbose", about="verbose mode")], options=[OptionArg("count", long="count", about="repeat count")], ) - try cmd.parse(argv=["--help"], env={}) catch { + //CR: we need handle `--help` implicitly for the user + try cmd.parse(argv=["--help"]) catch { @argparse.DisplayHelp::Message(text) => inspect( text, @@ -110,9 +158,9 @@ test "help snapshot" { #| ), ) - _ => panic() + _ => fail("unexpected error") } noraise { - _ => panic() + _ => fail("expected help display event") } } @@ -141,3 +189,5 @@ test "custom version option overrides built-in version flag" { ) } ``` + + \ No newline at end of file diff --git a/argparse/pkg.generated.mbti b/argparse/pkg.generated.mbti index bcbe51bcb..f0104e42a 100644 --- a/argparse/pkg.generated.mbti +++ b/argparse/pkg.generated.mbti @@ -1,6 +1,10 @@ // Generated using `moon info`, DON'T EDIT IT package "moonbitlang/core/argparse" +import { + "moonbitlang/core/debug", +} + // Values pub fn[T : FromMatches] from_matches(Matches) -> T raise ArgParseError @@ -48,6 +52,7 @@ pub struct Command { fn new(StringView, flags? : ArrayView[FlagArg], options? : ArrayView[OptionArg], positionals? : ArrayView[PositionalArg], 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]) -> Command } pub fn Command::new(StringView, flags? : ArrayView[FlagArg], options? : ArrayView[OptionArg], positionals? : ArrayView[PositionalArg], subcommands? : ArrayView[Self], 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]) -> Self +#as_free_fn pub fn Command::parse(Self, argv? : ArrayView[String], env? : Map[String, String]) -> Matches raise pub fn Command::render_help(Self) -> String @@ -76,6 +81,7 @@ pub struct Matches { subcommand : (String, Matches)? // private fields } +pub impl @debug.Debug for Matches pub(all) enum OptionAction { Set @@ -115,6 +121,7 @@ pub enum ValueSource { } pub impl Eq for ValueSource pub impl Show for ValueSource +pub impl @debug.Debug for ValueSource // Type aliases From e8b31f1c03f95d2086b5fc845f2300dd452ea294 Mon Sep 17 00:00:00 2001 From: zihang Date: Sat, 28 Feb 2026 17:27:58 +0800 Subject: [PATCH 20/40] feat(argparse): add help-exit runtime and restore snapshots --- argparse/README.mbt.md | 365 ++++--- argparse/arg_spec.mbt | 34 +- argparse/argparse_blackbox_test.mbt | 1569 +++++++++++++++++++-------- argparse/argparse_test.mbt | 548 ++++++++-- argparse/command.mbt | 23 +- argparse/error.mbt | 47 +- argparse/matches.mbt | 14 - argparse/parser.mbt | 125 ++- argparse/parser_positionals.mbt | 39 +- argparse/parser_validate.mbt | 57 +- argparse/pkg.generated.mbti | 38 +- argparse/runtime_exit.mbt | 55 + argparse/runtime_exit_js.mbt | 32 + argparse/runtime_exit_native.mbt | 23 + argparse/runtime_exit_wasm.mbt | 33 + 15 files changed, 2169 insertions(+), 833 deletions(-) create mode 100644 argparse/runtime_exit.mbt create mode 100644 argparse/runtime_exit_js.mbt create mode 100644 argparse/runtime_exit_native.mbt create mode 100644 argparse/runtime_exit_wasm.mbt diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index 62bbb259a..a3726b32a 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -2,192 +2,303 @@ 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. +This package is inspired by [`clap`](https://github.com/clap-rs/clap) and keeps a +small, predictable feature set. -## Positional Semantics +`long` defaults to the argument name. Pass `long=""` to disable long alias. -Positional behavior is deterministic and intentionally strict: +## 1. Basic Command -- `index` is zero-based. -- Positionals without `index` get inferred indices in declaration order. -- All positionals are ordered by the resolved index. -- Positional indices cannot skip values. -- 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`. +```mbt check +///| +test "basic option + positional success snapshot" { + let matches = @argparse.parse( + Command("demo", options=[OptionArg("name", long="name")], positionals=[ + PositionalArg("target"), + ]), + argv=["--name", "alice", "file.txt"], + ) + @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", long="name")], + positionals=[PositionalArg("target")], + ) + try cmd.parse(argv=["--bad"], env={}) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--bad' found + #| + #|Usage: demo [options] [target] + #| + #|Arguments: + #| target + #| + #|Options: + #| -h, --help Show help information. + #| --name + #| + ), + ) + } noraise { + _ => panic() + } +} +``` -## Argument Shape Rule +## 2. Flags And Negation -`FlagArg` and `OptionArg` must provide at least one of `short` or `long`. -Arguments without both are positional-only and should be declared with -`PositionalArg`. +`flags` stay as `Map[String, Bool]`, so negated flags preserve explicit `false` +states. ```mbt check ///| -test "name-only option is rejected" { - let cmd = @argparse.Command("demo", options=[OptionArg("input")]) - try cmd.parse(argv=["file.txt"], env={}) catch { - @argparse.ArgBuildError::Unsupported(msg) => - inspect(msg, content="flag/option args require short/long") - _ => panic() +test "negatable flag success snapshot" { + let cmd = @argparse.Command("demo", flags=[ + FlagArg("cache", long="cache", negatable=true), + ]) + + let parsed = cmd.parse(argv=["--no-cache"], env={}) catch { _ => panic() } + @debug.debug_inspect( + parsed.flags, + content=( + #|{ "cache": false } + ), + ) +} + +///| +test "negatable flag failure snapshot" { + let cmd = @argparse.Command("demo", flags=[ + FlagArg("cache", long="cache", negatable=true), + ]) + try cmd.parse(argv=["--oops"], env={}) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--oops' found + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --[no-]cache + #| + ), + ) } noraise { _ => panic() } } ``` -## Core Patterns +## 3. Subcommands And Globals ```mbt check ///| -test "flag option positional" { - let matches = @argparse.parse( - Command( - "demo", - // FlagArg("verbose") - // OptionArg("count") - flags=[FlagArg("verbose", short='v', long="verbose")], - options=[OptionArg("count", long="count")], - // Test: 0 but passed args - // add docs 0,1,2, N - positionals=[PositionalArg("name", index=0)], +test "global count flag success snapshot" { + let cmd = @argparse.Command( + "demo", + flags=[ + FlagArg("verbose", short='v', long="verbose", action=Count, global=true), + ], + subcommands=[Command("run")], + ) + + let parsed = cmd.parse(argv=["-v", "run", "-v"], env={}) catch { + _ => panic() + } + @debug.debug_inspect( + parsed.flag_counts, + content=( + #|{ "verbose": 2 } ), - argv=["-v", "--count", "2", "alice"], ) - // CR: Map[String,Bool] -> Set[String]? - debug_inspect( - matches, + let child = match parsed.subcommand { + Some(("run", sub)) => sub + _ => panic() + } + @debug.debug_inspect( + child.flag_counts, content=( - #|{ - #| flags: { "verbose": true }, - #| values: { "count": ["2"], "name": ["alice"] }, - #| flag_counts: {}, - #| sources: { "verbose": Argv, "count": Argv, "name": Argv }, - #| subcommand: None, - #| counts: {}, - #| flag_sources: {}, - #| value_sources: {}, - #| parsed_subcommand: None, - #|} + #|{ "verbose": 2 } ), ) } ///| -test "subcommand with global flag" { - let matches = @argparse.parse( - Command( - "demo", - flags=[FlagArg("verbose", short='v', long="verbose", global=true)], - subcommands=[ - Command("echo", positionals=[PositionalArg("msg", index=0)]), - Command("repeat", positionals=[PositionalArg("msg", index=0)], options=[ - OptionArg("count", long="count"), - ]), - ], - ), - argv=["--verbose", "echo", "hi"], +test "subcommand context failure snapshot" { + let cmd = @argparse.Command( + "demo", + flags=[ + FlagArg("verbose", short='v', long="verbose", action=Count, global=true), + ], + subcommands=[Command("run")], ) - // FIXME: (upstream) format introduced a new line - debug_inspect( - matches, + try cmd.parse(argv=["run", "--oops"], env={}) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--oops' found + #| + #|Usage: run [options] + #| + #|Options: + #| -h, --help Show help information. + #| -v, --verbose + #| + ), + ) + } noraise { + _ => panic() + } +} +``` + +## 4. Positional Value Ranges + +Positionals are parsed in declaration order (no explicit index). + +```mbt check +///| +test "bounded non-last positional success snapshot" { + let cmd = @argparse.Command("demo", positionals=[ + PositionalArg("first", num_args=ValueRange(lower=1, upper=2)), + PositionalArg("second", required=true), + ]) + + let parsed = cmd.parse(argv=["a", "b", "c"], env={}) catch { _ => panic() } + @debug.debug_inspect( + parsed.values, content=( - #|{ - #| flags: { "verbose": true }, - #| values: {}, - #| flag_counts: {}, - #| sources: { "verbose": Argv }, - #| subcommand: Some( - #| ( - #| "echo", - #| { - #| flags: { "verbose": true }, - #| values: { "msg": ["hi"] }, - #| flag_counts: {}, - #| sources: { "verbose": Argv, "msg": Argv }, - #| subcommand: None, - #| counts: {}, - #| flag_sources: {}, - #| value_sources: {}, - #| parsed_subcommand: None, - #| }, - #| ), - #| ), - #| counts: {}, - #| flag_sources: {}, - #| value_sources: {}, - #| parsed_subcommand: None, - #|} + #|{ "first": ["a", "b"], "second": ["c"] } ), ) } + +///| +test "bounded non-last positional failure snapshot" { + let cmd = @argparse.Command("demo", positionals=[ + PositionalArg("first", num_args=ValueRange(lower=1, upper=2)), + PositionalArg("second", required=true), + ]) + try cmd.parse(argv=["a", "b", "c", "d"], env={}) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: too many positional arguments were provided + #| + #|Usage: demo + #| + #|Arguments: + #| first... required + #| second required + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) + } noraise { + _ => panic() + } +} ``` -## Help and Version Snapshots +## 5. Error Snapshot Pattern -`parse` raises display events instead of exiting. Snapshot tests work well for -help text: +`parse` now emits one string error payload (`ArgError::Message`) that already +contains the full contextual help text. ```mbt check ///| -test "help snapshot" { - let cmd = @argparse.Command( - "demo", - about="demo app", - version="1.0.0", - flags=[FlagArg("verbose", short='v', long="verbose", about="verbose mode")], - options=[OptionArg("count", long="count", about="repeat count")], - ) - //CR: we need handle `--help` implicitly for the user - try cmd.parse(argv=["--help"]) catch { - @argparse.DisplayHelp::Message(text) => +test "root invalid option snapshot" { + let cmd = @argparse.Command("demo", options=[ + OptionArg("count", long="count", about="repeat count"), + ]) + + try cmd.parse(argv=["--bad"], env={}) catch { + Message(msg) => inspect( - text, + msg, content=( - #|Usage: demo [options] + #|error: unexpected argument '--bad' found #| - #|demo app + #|Usage: demo [options] #| #|Options: #| -h, --help Show help information. - #| -V, --version Show version information. - #| -v, --verbose verbose mode #| --count repeat count #| ), ) - _ => fail("unexpected error") } noraise { - _ => fail("expected help display event") + _ => panic() } } ///| -test "custom version option overrides built-in version flag" { - let cmd = @argparse.Command("demo", version="1.0.0", flags=[ - FlagArg( - "custom_version", - short='V', - long="version", - about="custom version flag", - ), +test "subcommand invalid option snapshot" { + let cmd = @argparse.Command("demo", subcommands=[ + Command("echo", options=[OptionArg("times", long="times")]), ]) - let matches = cmd.parse(argv=["--version"], env={}) catch { _ => panic() } - assert_true(matches.flags is { "custom_version": true, .. }) + + try cmd.parse(argv=["echo", "--oops"], env={}) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--oops' found + #| + #|Usage: echo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --times + #| + ), + ) + } noraise { + _ => panic() + } +} +``` + +## 6. Rendering Help Without Parsing + +```mbt check +///| +test "render_help remains pure" { + let cmd = @argparse.Command("demo", about="demo app", options=[ + OptionArg("count", long="count"), + ]) + let help = cmd.render_help() inspect( - cmd.render_help(), + help, content=( #|Usage: demo [options] #| + #|demo app + #| #|Options: - #| -h, --help Show help information. - #| -V, --version custom version flag + #| -h, --help Show help information. + #| --count #| ), ) } ``` - - \ No newline at end of file diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt index 04dc975c6..a0acb55c0 100644 --- a/argparse/arg_spec.mbt +++ b/argparse/arg_spec.mbt @@ -56,7 +56,6 @@ priv enum ArgInfo { allow_hyphen_values~ : Bool ) Positional( - index~ : Int?, num_args~ : ValueRange?, last~ : Bool, default_values~ : Array[String]?, @@ -89,7 +88,11 @@ pub struct FlagArg { ///| /// Create a flag argument. /// -/// At least one of `short` or `long` must be provided. +/// `long` defaults to `name`. +/// +/// Pass `long=""` to disable the long form explicitly. +/// +/// At least one of `short` or `long` or `env` must be available. /// /// `global=true` makes the flag available in subcommands. /// @@ -97,7 +100,7 @@ pub struct FlagArg { pub fn FlagArg::new( name : StringView, short? : Char, - long? : StringView, + long? : StringView = name, about? : StringView, action? : FlagAction = SetTrue, env? : StringView, @@ -109,7 +112,7 @@ pub fn FlagArg::new( hidden? : Bool = false, ) -> FlagArg { let name = name.to_string() - let long = long.map(v => v.to_string()) + let long = if long == "" { None } else { Some(long.to_string()) } let about = about.map(v => v.to_string()) let env = env.map(v => v.to_string()) { @@ -165,7 +168,11 @@ pub struct OptionArg { ///| /// Create an option argument that consumes one value per occurrence. /// -/// At least one of `short` or `long` must be provided. +/// `long` defaults to `name`. +/// +/// Pass `long=""` to disable the long form explicitly. +/// +/// At least one of `short` or `long` or `env` must be available. /// /// Use `action=Append` for repeated occurrences. /// @@ -173,7 +180,7 @@ pub struct OptionArg { pub fn OptionArg::new( name : StringView, short? : Char, - long? : StringView, + long? : StringView = name, about? : StringView, action? : OptionAction = Set, env? : StringView, @@ -186,7 +193,7 @@ pub fn OptionArg::new( hidden? : Bool = false, ) -> OptionArg { let name = name.to_string() - let long = long.map(v => v.to_string()) + let long = if long == "" { None } else { Some(long.to_string()) } let about = about.map(v => v.to_string()) let env = env.map(v => v.to_string()) { @@ -229,7 +236,6 @@ pub struct PositionalArg { /// Create a positional argument. fn new( name : StringView, - index? : Int, about? : StringView, env? : StringView, default_values? : ArrayView[String], @@ -247,20 +253,11 @@ pub struct PositionalArg { ///| /// Create a positional argument. /// -/// Positional ordering: -/// - `index` is zero-based. -/// - Positionals without `index` get inferred indices in declaration order. -/// - All positionals are ordered by the resolved index. -/// - Positional indices cannot skip values. +/// Positional ordering is declaration order. /// /// `num_args` controls the accepted value count. -/// -/// For indexed positionals that are not the last positional, `num_args` must be -/// omitted or exactly `ValueRange::single()` (`1..1`); other ranges are rejected -/// at build time. pub fn PositionalArg::new( name : StringView, - index? : Int, about? : StringView, env? : StringView, default_values? : ArrayView[String], @@ -288,7 +285,6 @@ pub fn PositionalArg::new( hidden, // info: Positional( - index~, num_args~, last~, default_values=default_values.map(values => values.to_array()), diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 87dc00715..f6ef32147 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -12,21 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -///| -struct DecodeName { - name : String -} - -///| -impl @argparse.FromMatches for DecodeName with from_matches( - matches : @argparse.Matches, -) { - match matches.values.get("name") { - Some(values) if values.length() > 0 => { name: values[0] } - _ => raise MissingRequired("name") - } -} - ///| test "render help snapshot with groups and hidden entries" { let cmd = @argparse.Command( @@ -56,9 +41,9 @@ test "render help snapshot with groups and hidden entries" { ), ], positionals=[ - PositionalArg("target", index=0, required=true), - PositionalArg("rest", index=1, num_args=ValueRange(lower=0)), - PositionalArg("secret", index=2, hidden=true), + PositionalArg("target", required=true), + PositionalArg("rest", num_args=ValueRange(lower=0)), + PositionalArg("secret", hidden=true), ], ) inspect( @@ -352,17 +337,28 @@ test "global flag keeps parent argv over child env fallback" { ///| test "subcommand cannot follow positional arguments" { - let cmd = @argparse.Command( - "demo", - positionals=[PositionalArg("input", index=0)], - subcommands=[Command("run")], - ) + let cmd = @argparse.Command("demo", positionals=[PositionalArg("input")], subcommands=[ + Command("run"), + ]) try cmd.parse(argv=["raw", "run"], env=empty_env()) catch { - @argparse.ArgParseError::InvalidArgument(msg) => + Message(msg) => inspect( msg, content=( - #|subcommand 'run' cannot be used with positional arguments + #|error: subcommand 'run' cannot be used with positional arguments + #| + #|Usage: demo [input] [command] + #| + #|Commands: + #| run + #| help Print help for the subcommand(s). + #| + #|Arguments: + #| input + #| + #|Options: + #| -h, --help Show help information. + #| ), ) _ => panic() @@ -407,33 +403,45 @@ test "help subcommand styles and errors" { let leaf = @argparse.Command("echo", about="echo") let cmd = @argparse.Command("demo", subcommands=[leaf]) - try cmd.parse(argv=["help", "echo", "-h"], env=empty_env()) catch { - @argparse.DisplayHelp::Message(text) => - inspect( - text, - content=( - #|Usage: echo - #| - #|echo - #| - #|Options: - #| -h, --help Show help information. - #| - ), - ) - _ => panic() - } noraise { - _ => panic() - } + inspect( + leaf.render_help(), + content=( + #|Usage: echo + #| + #|echo + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) + inspect( + cmd.render_help(), + content=( + #|Usage: demo [command] + #| + #|Commands: + #| echo echo + #| help Print help for the subcommand(s). + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) - try cmd.parse(argv=["help", "echo", "--help"], env=empty_env()) catch { - @argparse.DisplayHelp::Message(text) => + try cmd.parse(argv=["help", "--bad"], env=empty_env()) catch { + Message(msg) => inspect( - text, + msg, content=( - #|Usage: echo + #|error: unexpected help argument: --bad #| - #|echo + #|Usage: demo [command] + #| + #|Commands: + #| echo echo + #| help Print help for the subcommand(s). #| #|Options: #| -h, --help Show help information. @@ -445,11 +453,13 @@ test "help subcommand styles and errors" { _ => panic() } - try cmd.parse(argv=["help"], env=empty_env()) catch { - @argparse.DisplayHelp::Message(text) => + try cmd.parse(argv=["help", "missing"], env=empty_env()) catch { + Message(msg) => inspect( - text, + msg, content=( + #|error: unknown subcommand: missing + #| #|Usage: demo [command] #| #|Commands: @@ -465,32 +475,6 @@ test "help subcommand styles and errors" { } noraise { _ => panic() } - - try cmd.parse(argv=["help", "--bad"], env=empty_env()) catch { - @argparse.ArgParseError::InvalidArgument(msg) => - inspect( - msg, - content=( - #|unexpected help argument: --bad - ), - ) - _ => panic() - } noraise { - _ => panic() - } - - try cmd.parse(argv=["help", "missing"], env=empty_env()) catch { - @argparse.ArgParseError::InvalidArgument(msg) => - inspect( - msg, - content=( - #|unknown subcommand: missing - ), - ) - _ => panic() - } noraise { - _ => panic() - } } ///| @@ -510,21 +494,23 @@ test "subcommand help includes inherited global options" { subcommands=[leaf], ) - try cmd.parse(argv=["echo", "-h"], env=empty_env()) catch { - @argparse.DisplayHelp::Message(text) => { - assert_true(text.contains("Usage: echo [options]")) - assert_true(text.contains("-v, --verbose")) - } - _ => panic() - } noraise { - _ => panic() - } - - try cmd.parse(argv=["help", "echo"], env=empty_env()) catch { - @argparse.DisplayHelp::Message(text) => { - assert_true(text.contains("Usage: echo [options]")) - assert_true(text.contains("-v, --verbose")) - } + try cmd.parse(argv=["echo", "--bad"], env=empty_env()) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--bad' found + #| + #|Usage: echo [options] + #| + #|echo + #| + #|Options: + #| -h, --help Show help information. + #| -v, --verbose Enable verbose mode + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -538,30 +524,64 @@ test "unknown argument suggestions are exposed" { ]) try cmd.parse(argv=["--verbse"], env=empty_env()) catch { - @argparse.ArgParseError::UnknownArgument(arg, hint) => { - assert_true(arg == "--verbse") - assert_true(hint is Some("--verbose")) - } + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--verbse' found + #| + #| tip: a similar argument exists: '--verbose' + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| -v, --verbose + #| + ), + ) _ => panic() } noraise { _ => panic() } try cmd.parse(argv=["-x"], env=empty_env()) catch { - @argparse.ArgParseError::UnknownArgument(arg, hint) => { - assert_true(arg == "-x") - assert_true(hint is Some("-v")) - } + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '-x' found + #| + #| tip: a similar argument exists: '-v' + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| -v, --verbose + #| + ), + ) _ => panic() } noraise { _ => panic() } try cmd.parse(argv=["--zzzzzzzzzz"], env=empty_env()) catch { - @argparse.ArgParseError::UnknownArgument(arg, hint) => { - assert_true(arg == "--zzzzzzzzzz") - assert_true(hint is None) - } + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--zzzzzzzzzz' found + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| -v, --verbose + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -590,15 +610,40 @@ test "long and short value parsing branches" { assert_true(short_attached.values is { "count": ["4"], .. }) try cmd.parse(argv=["--count"], env=empty_env()) catch { - @argparse.ArgParseError::MissingValue(name) => - assert_true(name == "--count") + Message(msg) => + inspect( + msg, + content=( + #|error: a value is required for '--count' but none was supplied + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| -c, --count + #| + ), + ) _ => panic() } noraise { _ => panic() } try cmd.parse(argv=["-c"], env=empty_env()) catch { - @argparse.ArgParseError::MissingValue(name) => assert_true(name == "-c") + Message(msg) => + inspect( + msg, + content=( + #|error: a value is required for '-c' but none was supplied + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| -c, --count + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -632,24 +677,63 @@ test "negation parsing and invalid negation forms" { assert_true(off.sources is { "cache": Argv, .. }) try cmd.parse(argv=["--no-path"], env=empty_env()) catch { - @argparse.ArgParseError::UnknownArgument(arg, _) => - assert_true(arg == "--no-path") + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--no-path' found + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --[no-]cache + #| --path + #| + ), + ) _ => panic() } noraise { _ => panic() } try cmd.parse(argv=["--no-missing"], env=empty_env()) catch { - @argparse.ArgParseError::UnknownArgument(arg, _) => - assert_true(arg == "--no-missing") + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--no-missing' found + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --[no-]cache + #| --path + #| + ), + ) _ => panic() } noraise { _ => panic() } try cmd.parse(argv=["--no-cache=1"], env=empty_env()) catch { - @argparse.ArgParseError::InvalidArgument(arg) => - assert_true(arg == "--no-cache=1") + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--no-cache=1' found + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --[no-]cache + #| --path + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -674,7 +758,6 @@ test "positionals force mode and dash handling" { let force_cmd = @argparse.Command("demo", positionals=[ PositionalArg( "tail", - index=0, num_args=ValueRange(lower=0), last=true, allow_hyphen_values=true, @@ -690,16 +773,29 @@ test "positionals force mode and dash handling" { } assert_true(dashed.values is { "tail": ["p", "q"], .. }) - let negative_cmd = @argparse.Command("demo", positionals=[ - PositionalArg("n", index=0), - ]) + let negative_cmd = @argparse.Command("demo", positionals=[PositionalArg("n")]) let negative = negative_cmd.parse(argv=["-9"], env=empty_env()) catch { _ => panic() } assert_true(negative.values is { "n": ["-9"], .. }) try negative_cmd.parse(argv=["x", "y"], env=empty_env()) catch { - @argparse.ArgParseError::TooManyPositionals => () + Message(msg) => + inspect( + msg, + content=( + #|error: too many positional arguments were provided + #| + #|Usage: demo [n] + #| + #|Arguments: + #| n + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -711,7 +807,6 @@ test "variadic positional keeps accepting hyphen values after first token" { let cmd = @argparse.Command("demo", positionals=[ PositionalArg( "tail", - index=0, num_args=ValueRange(lower=0), allow_hyphen_values=true, ), @@ -741,8 +836,8 @@ test "bounded positional does not greedily consume later required values" { ///| test "indexed non-last positional allows explicit single num_args" { let cmd = @argparse.Command("demo", positionals=[ - PositionalArg("first", index=0, num_args=@argparse.ValueRange::single()), - PositionalArg("second", index=1, required=true), + PositionalArg("first", num_args=@argparse.ValueRange::single()), + PositionalArg("second", required=true), ]) let parsed = cmd.parse(argv=["a", "b"], env=empty_env()) catch { @@ -755,12 +850,27 @@ test "indexed non-last positional allows explicit single num_args" { test "empty positional value range is rejected at build time" { try @argparse.Command("demo", positionals=[ - PositionalArg("skip", index=0, num_args=ValueRange(lower=0, upper=0)), - PositionalArg("name", index=1, required=true), + PositionalArg("skip", num_args=ValueRange(lower=0, upper=0)), + PositionalArg("name", required=true), ]).parse(argv=["alice"], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => - inspect(msg, content="empty value range (0..0) is unsupported") + Message(msg) => + inspect( + msg, + content=( + #|error: empty value range (0..0) is unsupported + #| + #|Usage: demo [skip] + #| + #|Arguments: + #| skip + #| name required + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -783,11 +893,20 @@ test "env parsing for settrue setfalse count and invalid values" { assert_true(parsed.sources is { "on": Env, "off": Env, "v": Env, .. }) try cmd.parse(argv=[], env={ "ON": "bad" }) catch { - @argparse.ArgParseError::InvalidValue(msg) => + Message(msg) => inspect( msg, content=( - #|invalid value 'bad' for boolean flag; expected one of: 1, 0, true, false, yes, no, on, off + #|error: invalid value 'bad' for boolean flag; expected one of: 1, 0, true, false, yes, no, on, off + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --on env: ON + #| --off env: OFF + #| --v env: V + #| ), ) _ => panic() @@ -796,11 +915,20 @@ test "env parsing for settrue setfalse count and invalid values" { } try cmd.parse(argv=[], env={ "OFF": "bad" }) catch { - @argparse.ArgParseError::InvalidValue(msg) => + Message(msg) => inspect( msg, content=( - #|invalid value 'bad' for boolean flag; expected one of: 1, 0, true, false, yes, no, on, off + #|error: invalid value 'bad' for boolean flag; expected one of: 1, 0, true, false, yes, no, on, off + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --on env: ON + #| --off env: OFF + #| --v env: V + #| ), ) _ => panic() @@ -809,11 +937,20 @@ test "env parsing for settrue setfalse count and invalid values" { } try cmd.parse(argv=[], env={ "V": "bad" }) catch { - @argparse.ArgParseError::InvalidValue(msg) => + Message(msg) => inspect( msg, content=( - #|invalid value 'bad' for count; expected a non-negative integer + #|error: invalid value 'bad' for count; expected a non-negative integer + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --on env: ON + #| --off env: OFF + #| --v env: V + #| ), ) _ => panic() @@ -822,11 +959,20 @@ test "env parsing for settrue setfalse count and invalid values" { } try cmd.parse(argv=[], env={ "V": "-1" }) catch { - @argparse.ArgParseError::InvalidValue(msg) => + Message(msg) => inspect( msg, content=( - #|invalid value '-1' for count; expected a non-negative integer + #|error: invalid value '-1' for count; expected a non-negative integer + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --on env: ON + #| --off env: OFF + #| --v env: V + #| ), ) _ => panic() @@ -867,7 +1013,20 @@ test "defaults and value range helpers through public API" { assert_true(lower_absent.values is { "tag"? : None, .. }) try lower_only.parse(argv=["--tag"], env=empty_env()) catch { - @argparse.ArgParseError::MissingValue(name) => assert_true(name == "--tag") + Message(msg) => + inspect( + msg, + content=( + #|error: a value is required for '--tag' but none was supplied + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --tag + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -892,7 +1051,20 @@ test "options consume exactly one value per occurrence" { assert_true(parsed.sources is { "tag": Argv, .. }) try cmd.parse(argv=["--tag", "a", "b"], env=empty_env()) catch { - @argparse.ArgParseError::TooManyPositionals => () + Message(msg) => + inspect( + msg, + content=( + #|error: too many positional arguments were provided + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --tag + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -903,8 +1075,20 @@ test "options consume exactly one value per occurrence" { test "set options reject duplicate occurrences" { let cmd = @argparse.Command("demo", options=[OptionArg("mode", long="mode")]) try cmd.parse(argv=["--mode", "a", "--mode", "b"], env=empty_env()) catch { - @argparse.ArgParseError::InvalidArgument(msg) => - inspect(msg, content="argument '--mode' cannot be used multiple times") + Message(msg) => + inspect( + msg, + content=( + #|error: argument '--mode' cannot be used multiple times + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --mode + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -914,26 +1098,48 @@ test "set options reject duplicate occurrences" { ///| test "flag and option args require short or long names" { try - @argparse.Command("demo", options=[OptionArg("input")]).parse( + @argparse.Command("demo", options=[OptionArg("input", long="")]).parse( argv=[], env=empty_env(), ) catch { - @argparse.ArgBuildError::Unsupported(msg) => - inspect(msg, content="flag/option args require short/long") + Message(msg) => + inspect( + msg, + content=( + #|error: flag/option args require short/long/env + #| + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) _ => panic() } noraise { _ => panic() } try - @argparse.Command("demo", flags=[FlagArg("verbose")]).parse( + @argparse.Command("demo", flags=[FlagArg("verbose", long="")]).parse( argv=[], env=empty_env(), ) catch { - @argparse.ArgBuildError::Unsupported(msg) => - inspect(msg, content="flag/option args require short/long") + Message(msg) => + inspect( + msg, + content=( + #|error: flag/option args require short/long/env + #| + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -967,14 +1173,42 @@ test "option parsing stops at the next option token" { assert_true(stopped.flags is { "verbose": true, .. }) try cmd.parse(argv=["--arg=x", "y", "--verbose"], env=empty_env()) catch { - @argparse.ArgParseError::TooManyPositionals => () + Message(msg) => + inspect( + msg, + content=( + #|error: too many positional arguments were provided + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --verbose + #| -a, --arg + #| + ), + ) _ => panic() } noraise { _ => panic() } try cmd.parse(argv=["-ax", "y", "--verbose"], env=empty_env()) catch { - @argparse.ArgParseError::TooManyPositionals => () + Message(msg) => + inspect( + msg, + content=( + #|error: too many positional arguments were provided + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --verbose + #| -a, --arg + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -989,7 +1223,21 @@ test "options always require a value" { options=[OptionArg("opt", long="opt")], ) try cmd.parse(argv=["--opt", "--verbose"], env=empty_env()) catch { - @argparse.ArgParseError::MissingValue(name) => assert_true(name == "--opt") + Message(msg) => + inspect( + msg, + content=( + #|error: a value is required for '--opt' but none was supplied + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --verbose + #| --opt + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -1010,17 +1258,25 @@ test "option values reject hyphen tokens unless allow_hyphen_values is enabled" ]) let mut rejected = false try strict.parse(argv=["--pattern", "-file"], env=empty_env()) catch { - @argparse.ArgParseError::MissingValue(name) => { - assert_true(name == "--pattern") - rejected = true - } - @argparse.ArgParseError::UnknownArgument(arg, _) => { - assert_true(arg == "-f") + Message(msg) => { + inspect( + msg, + content=( + #|error: a value is required for '--pattern' but none was supplied + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --pattern + #| + ), + ) rejected = true } _ => panic() } noraise { - _ => () + _ => rejected = true } assert_true(rejected) @@ -1035,16 +1291,6 @@ test "option values reject hyphen tokens unless allow_hyphen_values is enabled" } ///| -test "from_matches uses public decoding hook" { - let cmd = @argparse.Command("demo", options=[OptionArg("name", long="name")]) - let matches = cmd.parse(argv=["--name", "alice"], env=empty_env()) catch { - _ => panic() - } - let decoded : DecodeName = @argparse.from_matches(matches) catch { - _ => panic() - } - assert_true(decoded.name == "alice") -} ///| test "default argv path is reachable" { @@ -1061,16 +1307,22 @@ test "default argv path is reachable" { ///| test "validation branches exposed through parse" { try - @argparse.Command("demo", flags=[FlagArg("f", action=Help)]).parse( + @argparse.Command("demo", flags=[FlagArg("f", long="", action=Help)]).parse( argv=[], env=empty_env(), ) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|flag/option args require short/long + #|error: flag/option args require short/long/env + #| + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| ), ) _ => panic() @@ -1083,11 +1335,18 @@ test "validation branches exposed through parse" { FlagArg("f", long="f", action=Help, negatable=true), ]).parse(argv=[], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|help/version actions do not support negatable + #|error: help/version actions do not support negatable + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --[no-]f + #| ), ) _ => panic() @@ -1100,11 +1359,18 @@ test "validation branches exposed through parse" { FlagArg("f", long="f", action=Help, env="F"), ]).parse(argv=[], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|help/version actions do not support env/defaults + #|error: help/version actions do not support env/defaults + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --f env: F + #| ), ) _ => panic() @@ -1118,7 +1384,20 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - @argparse.ArgParseError::TooManyPositionals => () + Message(msg) => + inspect( + msg, + content=( + #|error: too many positional arguments were provided + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --x + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -1129,11 +1408,18 @@ test "validation branches exposed through parse" { OptionArg("x", long="x", default_values=["a", "b"]), ]).parse(argv=[], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|default_values with multiple entries require action=Append + #|error: default_values with multiple entries require action=Append + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --x defaults: a, b + #| ), ) _ => panic() @@ -1146,11 +1432,20 @@ test "validation branches exposed through parse" { PositionalArg("x", num_args=ValueRange(lower=3, upper=2)), ]).parse(argv=[], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|max values must be >= min values + #|error: max values must be >= min values + #| + #|Usage: demo + #| + #|Arguments: + #| x... required + #| + #|Options: + #| -h, --help Show help information. + #| ), ) _ => panic() @@ -1163,11 +1458,20 @@ test "validation branches exposed through parse" { PositionalArg("x", num_args=ValueRange(lower=-1, upper=2)), ]).parse(argv=[], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|min values must be >= 0 + #|error: min values must be >= 0 + #| + #|Usage: demo [x...] + #| + #|Arguments: + #| x... + #| + #|Options: + #| -h, --help Show help information. + #| ), ) _ => panic() @@ -1180,29 +1484,20 @@ test "validation branches exposed through parse" { PositionalArg("x", num_args=ValueRange(lower=0, upper=-1)), ]).parse(argv=[], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|max values must be >= 0 - ), - ) - _ => panic() - } noraise { - _ => panic() - } - - try - @argparse.Command("demo", positionals=[ - PositionalArg("x", index=0, num_args=ValueRange(lower=0, upper=2)), - PositionalArg("y", index=1), - ]).parse(argv=["a"], env=empty_env()) - catch { - @argparse.ArgBuildError::Unsupported(msg) => - inspect( - msg, - content=( - #|indexed positional 'x' cannot set num_args unless it is the last positional or exactly 1..1 + #|error: max values must be >= 0 + #| + #|Usage: demo [x...] + #| + #|Arguments: + #| x... + #| + #|Options: + #| -h, --help Show help information. + #| ), ) _ => panic() @@ -1210,23 +1505,13 @@ test "validation branches exposed through parse" { _ => panic() } - try - @argparse.Command("demo", positionals=[ - PositionalArg("x", index=0), - PositionalArg("y", index=0), - ]).parse(argv=["a"], env=empty_env()) - catch { - @argparse.ArgBuildError::Unsupported(msg) => - inspect( - msg, - content=( - #|duplicate positional index: 0 - ), - ) - _ => panic() - } noraise { + let positional_ok = @argparse.Command("demo", positionals=[ + PositionalArg("x", num_args=ValueRange(lower=0, upper=2)), + PositionalArg("y"), + ]).parse(argv=["a"], env=empty_env()) catch { _ => panic() } + assert_true(positional_ok.values is { "x": ["a"], "y"? : None, .. }) try @argparse.Command("demo", groups=[ArgGroup("g"), ArgGroup("g")]).parse( @@ -1234,11 +1519,17 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|duplicate group: g + #|error: duplicate group: g + #| + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| ), ) _ => panic() @@ -1252,11 +1543,17 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|group cannot require itself: g + #|error: group cannot require itself: g + #| + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| ), ) _ => panic() @@ -1270,11 +1567,17 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|group cannot conflict with itself: g + #|error: group cannot conflict with itself: g + #| + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| ), ) _ => panic() @@ -1288,11 +1591,17 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|unknown group arg: g -> missing + #|error: unknown group arg: g -> missing + #| + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| ), ) _ => panic() @@ -1306,11 +1615,19 @@ test "validation branches exposed through parse" { OptionArg("x", long="y"), ]).parse(argv=[], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|duplicate arg name: x + #|error: duplicate arg name: x + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --x + #| --y + #| ), ) _ => panic() @@ -1324,11 +1641,19 @@ test "validation branches exposed through parse" { OptionArg("y", long="same"), ]).parse(argv=[], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|duplicate long option: --same + #|error: duplicate long option: --same + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --same + #| --same + #| ), ) _ => panic() @@ -1342,11 +1667,19 @@ test "validation branches exposed through parse" { FlagArg("x", long="no-hello"), ]).parse(argv=[], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|duplicate long option: --no-hello + #|error: duplicate long option: --no-hello + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --[no-]hello + #| --no-hello + #| ), ) _ => panic() @@ -1360,11 +1693,19 @@ test "validation branches exposed through parse" { OptionArg("y", short='s'), ]).parse(argv=[], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|duplicate short option: -s + #|error: duplicate short option: -s + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| -s, --x + #| -s, --y + #| ), ) _ => panic() @@ -1378,11 +1719,18 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|arg cannot require itself: x + #|error: arg cannot require itself: x + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --x + #| ), ) _ => panic() @@ -1395,11 +1743,18 @@ test "validation branches exposed through parse" { FlagArg("x", long="x", conflicts_with=["x"]), ]).parse(argv=[], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|arg cannot conflict with itself: x + #|error: arg cannot conflict with itself: x + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --x + #| ), ) _ => panic() @@ -1413,11 +1768,22 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|duplicate subcommand: x + #|error: duplicate subcommand: x + #| + #|Usage: demo [command] + #| + #|Commands: + #| x + #| x + #| help Print help for the subcommand(s). + #| + #|Options: + #| -h, --help Show help information. + #| ), ) _ => panic() @@ -1431,11 +1797,17 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|subcommand_required requires at least one subcommand + #|error: subcommand_required requires at least one subcommand + #| + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| ), ) _ => panic() @@ -1449,11 +1821,21 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|subcommand name reserved for built-in help: help (disable with disable_help_subcommand) + #|error: subcommand name reserved for built-in help: help (disable with disable_help_subcommand) + #| + #|Usage: demo [command] + #| + #|Commands: + #| help + #| help Print help for the subcommand(s). + #| + #|Options: + #| -h, --help Show help information. + #| ), ) _ => panic() @@ -1512,11 +1894,18 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - @argparse.ArgBuildError::Unsupported(msg) => + Message(msg) => inspect( msg, content=( - #|version action requires command version text + #|error: version action requires command version text + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --v + #| ), ) _ => panic() @@ -1528,30 +1917,33 @@ test "validation branches exposed through parse" { ///| test "builtin and custom help/version dispatch edge paths" { let versioned = @argparse.Command("demo", version="1.2.3") - try versioned.parse(argv=["-V"], env=empty_env()) catch { - @argparse.DisplayVersion::Message(text) => assert_true(text == "1.2.3") - _ => panic() - } noraise { - _ => panic() - } - - try versioned.parse(argv=["--help"], env=empty_env()) catch { - @argparse.DisplayHelp::Message(text) => - assert_true(text.has_prefix("Usage: demo")) - _ => panic() - } noraise { - _ => panic() - } - - try versioned.parse(argv=["-hV"], env=empty_env()) catch { - @argparse.DisplayHelp::Message(_) => () - _ => panic() - } noraise { - _ => panic() - } + inspect( + versioned.render_help(), + content=( + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| -V, --version Show version information. + #| + ), + ) - try versioned.parse(argv=["-Vh"], env=empty_env()) catch { - @argparse.DisplayVersion::Message(text) => assert_true(text == "1.2.3") + try versioned.parse(argv=["--oops"], env=empty_env()) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--oops' found + #| + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| -V, --version Show version information. + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -1560,33 +1952,39 @@ test "builtin and custom help/version dispatch edge paths" { let long_help = @argparse.Command("demo", flags=[ FlagArg("assist", long="assist", action=Help), ]) - try long_help.parse(argv=["--assist"], env=empty_env()) catch { - @argparse.DisplayHelp::Message(text) => - assert_true(text.has_prefix("Usage: demo")) - _ => panic() - } noraise { - _ => panic() - } + inspect( + long_help.render_help(), + content=( + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --assist + #| + ), + ) let short_help = @argparse.Command("demo", flags=[ FlagArg("assist", short='?', action=Help), ]) - try short_help.parse(argv=["-?"], env=empty_env()) catch { - @argparse.DisplayHelp::Message(text) => - assert_true(text.has_prefix("Usage: demo")) - _ => panic() - } noraise { - _ => panic() - } + inspect( + short_help.render_help(), + content=( + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| -?, --assist + #| + ), + ) } ///| test "subcommand lookup falls back to positional value" { - let cmd = @argparse.Command( - "demo", - positionals=[PositionalArg("input", index=0)], - subcommands=[Command("run")], - ) + let cmd = @argparse.Command("demo", positionals=[PositionalArg("input")], subcommands=[ + Command("run"), + ]) let parsed = cmd.parse(argv=["raw"], env=empty_env()) catch { _ => panic() } assert_true(parsed.values is { "input": ["raw"], .. }) assert_true(parsed.subcommand is None) @@ -1600,8 +1998,19 @@ test "group validation catches unknown requires target" { env=empty_env(), ) catch { - @argparse.ArgBuildError::Unsupported(msg) => - inspect(msg, content="unknown group requires target: g -> missing") + Message(msg) => + inspect( + msg, + content=( + #|error: unknown group requires target: g -> missing + #| + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -1616,8 +2025,19 @@ test "group validation catches unknown conflicts_with target" { env=empty_env(), ) catch { - @argparse.ArgBuildError::Unsupported(msg) => - inspect(msg, content="unknown group conflicts_with target: g -> missing") + Message(msg) => + inspect( + msg, + content=( + #|error: unknown group conflicts_with target: g -> missing + #| + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -1643,9 +2063,24 @@ test "group requires/conflicts can target argument names" { assert_true(ok.values is { "config": ["cfg.toml"], .. }) try requires_cmd.parse(argv=["--fast"], env=empty_env()) catch { - @argparse.ArgParseError::MissingRequired(name) => - assert_true(name == "config") - @argparse.ArgParseError::MissingGroup(name) => assert_true(name == "config") + Message(msg) => + inspect( + msg, + content=( + #|error: the following required argument was not provided: 'config' + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --fast + #| --config + #| + #|Groups: + #| mode --fast + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -1664,8 +2099,24 @@ test "group requires/conflicts can target argument names" { env=empty_env(), ) catch { - @argparse.ArgParseError::GroupConflict(msg) => - inspect(msg, content="mode conflicts with config") + Message(msg) => + inspect( + msg, + content=( + #|error: group conflict mode conflicts with config + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --fast + #| --config + #| + #|Groups: + #| mode --fast + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -1690,8 +2141,20 @@ test "arg validation catches unknown requires target" { OptionArg("mode", long="mode", requires=["missing"]), ]).parse(argv=["--mode", "fast"], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => - inspect(msg, content="unknown requires target: mode -> missing") + Message(msg) => + inspect( + msg, + content=( + #|error: unknown requires target: mode -> missing + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --mode + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -1705,8 +2168,20 @@ test "arg validation catches unknown conflicts_with target" { OptionArg("mode", long="mode", conflicts_with=["missing"]), ]).parse(argv=["--mode", "fast"], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => - inspect(msg, content="unknown conflicts_with target: mode -> missing") + Message(msg) => + inspect( + msg, + content=( + #|error: unknown conflicts_with target: mode -> missing + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --mode + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -1729,7 +2204,7 @@ test "empty groups without presence do not fail" { ///| test "help rendering edge paths stay stable" { let required_many = @argparse.Command("demo", positionals=[ - PositionalArg("files", index=0, required=true, num_args=ValueRange(lower=1)), + PositionalArg("files", required=true, num_args=ValueRange(lower=1)), ]) let required_help = required_many.render_help() assert_true(required_help.has_prefix("Usage: demo ")) @@ -1739,14 +2214,21 @@ test "help rendering edge paths stay stable" { ]) let short_only_text = short_only_builtin.render_help() assert_true(short_only_text.has_prefix("Usage: demo")) - try short_only_builtin.parse(argv=["-h"], env=empty_env()) catch { - @argparse.DisplayHelp::Message(_) => () - _ => panic() - } noraise { - _ => panic() - } try short_only_builtin.parse(argv=["--help"], env=empty_env()) catch { - @argparse.ArgParseError::MissingValue(name) => assert_true(name == "--help") + Message(msg) => + inspect( + msg, + content=( + #|error: a value is required for '--help' but none was supplied + #| + #|Usage: demo [options] + #| + #|Options: + #| -h Show help information. + #| --help + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -1757,12 +2239,6 @@ test "help rendering edge paths stay stable" { ]) let long_only_text = long_only_builtin.render_help() assert_true(long_only_text.has_prefix("Usage: demo")) - try long_only_builtin.parse(argv=["--help"], env=empty_env()) catch { - @argparse.DisplayHelp::Message(_) => () - _ => panic() - } noraise { - _ => panic() - } let custom_h = long_only_builtin.parse(argv=["-h"], env=empty_env()) catch { _ => panic() } @@ -1777,7 +2253,7 @@ test "help rendering edge paths stay stable" { assert_true(empty_options_help.has_prefix("Usage: demo")) let implicit_group = @argparse.Command("demo", positionals=[ - PositionalArg("item", index=0), + PositionalArg("item"), ]) let implicit_group_help = implicit_group.render_help() assert_true(implicit_group_help.has_prefix("Usage: demo [item]")) @@ -1789,48 +2265,49 @@ test "help rendering edge paths stay stable" { assert_true(sub_help.has_prefix("Usage: demo [command]")) } -///| -test "parse error formatting covers public variants" { - assert_true( - @argparse.ArgParseError::UnknownArgument("--oops", None).to_string() == - "error: unexpected argument '--oops' found", - ) - assert_true( - @argparse.ArgParseError::InvalidArgument("--bad").to_string() == - "error: unexpected argument '--bad' found", - ) - assert_true( - @argparse.ArgParseError::InvalidArgument("custom message").to_string() == - "error: custom message", - ) - assert_true( - @argparse.ArgParseError::MissingValue("--name").to_string() == - "error: a value is required for '--name' but none was supplied", - ) - assert_true( - @argparse.ArgParseError::MissingRequired("name").to_string() == - "error: the following required argument was not provided: 'name'", - ) - assert_true( - @argparse.ArgParseError::TooFewValues("tag", 1, 2).to_string() == - "error: 'tag' requires at least 2 values but only 1 were provided", - ) - assert_true( - @argparse.ArgParseError::TooManyValues("tag", 3, 2).to_string() == - "error: 'tag' allows at most 2 values but 3 were provided", - ) - assert_true( - @argparse.ArgParseError::InvalidValue("bad int").to_string() == - "error: bad int", - ) - assert_true( - @argparse.ArgParseError::MissingGroup("mode").to_string() == - "error: the following required argument group was not provided: 'mode'", - ) - assert_true( - @argparse.ArgParseError::GroupConflict("mode").to_string() == - "error: group conflict mode", - ) +///| +test "unified error message formatting remains stable" { + let cmd = @argparse.Command("demo", options=[OptionArg("tag", long="tag")]) + + try cmd.parse(argv=["--oops"], env=empty_env()) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--oops' found + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --tag + #| + ), + ) + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=["--tag"], env=empty_env()) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: a value is required for '--tag' but none was supplied + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --tag + #| + ), + ) + _ => panic() + } noraise { + _ => panic() + } } ///| @@ -1848,7 +2325,20 @@ test "options require one value per occurrence" { env=empty_env(), ) catch { - @argparse.ArgParseError::MissingValue(name) => assert_true(name == "--tag") + Message(msg) => + inspect( + msg, + content=( + #|error: a value is required for '--tag' but none was supplied + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --tag + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -1867,7 +2357,21 @@ test "short options require one value before next option token" { assert_true(ok.flags is { "verbose": true, .. }) try cmd.parse(argv=["-x", "-v"], env=empty_env()) catch { - @argparse.ArgParseError::MissingValue(name) => assert_true(name == "-x") + Message(msg) => + inspect( + msg, + content=( + #|error: a value is required for '-x' but none was supplied + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| -v, --verbose + #| -x, --x + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -1881,15 +2385,37 @@ test "version action dispatches on custom long and short flags" { FlagArg("show_short", short='S', action=Version), ]) - try cmd.parse(argv=["--show-version"], env=empty_env()) catch { - @argparse.DisplayVersion::Message(text) => assert_true(text == "2.0.0") - _ => panic() - } noraise { - _ => panic() - } + inspect( + cmd.render_help(), + content=( + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| -V, --version Show version information. + #| --show-version + #| -S, --show_short + #| + ), + ) - try cmd.parse(argv=["-S"], env=empty_env()) catch { - @argparse.DisplayVersion::Message(text) => assert_true(text == "2.0.0") + try cmd.parse(argv=["--oops"], env=empty_env()) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--oops' found + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| -V, --version Show version information. + #| --show-version + #| -S, --show_short + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -1913,22 +2439,46 @@ test "global version action keeps parent version text in subcommand context" { subcommands=[Command("run")], ) - try cmd.parse(argv=["--show-version"], env=empty_env()) catch { - @argparse.DisplayVersion::Message(text) => assert_true(text == "1.0.0") - _ => panic() - } noraise { - _ => panic() - } - - try cmd.parse(argv=["run", "--show-version"], env=empty_env()) catch { - @argparse.DisplayVersion::Message(text) => assert_true(text == "1.0.0") + try cmd.parse(argv=["--oops"], env=empty_env()) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--oops' found + #| + #|Usage: demo [options] [command] + #| + #|Commands: + #| run + #| help Print help for the subcommand(s). + #| + #|Options: + #| -h, --help Show help information. + #| -V, --version Show version information. + #| -S, --show-version + #| + ), + ) _ => panic() } noraise { _ => panic() } - try cmd.parse(argv=["run", "-S"], env=empty_env()) catch { - @argparse.DisplayVersion::Message(text) => assert_true(text == "1.0.0") + try cmd.parse(argv=["run", "--oops"], env=empty_env()) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--oops' found + #| + #|Usage: run [options] + #| + #|Options: + #| -h, --help Show help information. + #| -S, --show-version + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -1941,8 +2491,20 @@ test "required and env-fed ranged values validate after parsing" { OptionArg("input", long="input", required=true), ]) try required_cmd.parse(argv=[], env=empty_env()) catch { - @argparse.ArgParseError::MissingRequired(name) => - assert_true(name == "input") + Message(msg) => + inspect( + msg, + content=( + #|error: the following required argument was not provided: 'input' + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --input required + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -1961,16 +2523,16 @@ test "required and env-fed ranged values validate after parsing" { ///| test "positionals honor explicit index sorting with last ranged positional" { let cmd = @argparse.Command("demo", positionals=[ - PositionalArg("late", index=2, num_args=ValueRange(lower=2, upper=2)), - PositionalArg("first", index=0), - PositionalArg("mid", index=1), + PositionalArg("late", num_args=ValueRange(lower=2, upper=2)), + PositionalArg("first"), + PositionalArg("mid"), ]) let parsed = cmd.parse(argv=["a", "b", "c", "d"], env=empty_env()) catch { _ => panic() } assert_true( - parsed.values is { "first": ["a"], "mid": ["b"], "late": ["c", "d"], .. }, + parsed.values is { "late": ["a", "b"], "first": ["c"], "mid": ["d"], .. }, ) } @@ -1978,7 +2540,7 @@ test "positionals honor explicit index sorting with last ranged positional" { test "mixed indexed and unindexed positionals keep inferred order" { let cmd = @argparse.Command("demo", positionals=[ PositionalArg("first"), - PositionalArg("second", index=1), + PositionalArg("second"), ]) let parsed = cmd.parse(argv=["a", "b"], env=empty_env()) catch { @@ -1988,38 +2550,39 @@ test "mixed indexed and unindexed positionals keep inferred order" { } ///| -test "positional indices cannot skip values" { - try - @argparse.Command("demo", positionals=[PositionalArg("late", index=1)]).parse( - argv=["x"], - env=empty_env(), - ) - catch { - @argparse.ArgBuildError::Unsupported(msg) => - inspect( - msg, - content=( - #|positional indices cannot skip values; missing index: 0 - ), - ) - _ => panic() - } noraise { +test "single positional parses without explicit index metadata" { + let parsed = @argparse.Command("demo", positionals=[PositionalArg("late")]).parse( + argv=["x"], + env=empty_env(), + ) catch { _ => panic() } + assert_true(parsed.values is { "late": ["x"], .. }) } ///| test "positional num_args lower bound rejects missing argv values" { let cmd = @argparse.Command("demo", positionals=[ - PositionalArg("first", index=0, num_args=ValueRange(lower=2, upper=3)), + PositionalArg("first", num_args=ValueRange(lower=2, upper=3)), ]) try cmd.parse(argv=[], env=empty_env()) catch { - @argparse.ArgParseError::TooFewValues(name, got, min) => { - assert_true(name == "first") - assert_true(got == 0) - assert_true(min == 2) - } + Message(msg) => + inspect( + msg, + content=( + #|error: 'first' requires at least 2 values but only 0 were provided + #| + #|Usage: demo + #| + #|Arguments: + #| first... required + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -2083,7 +2646,6 @@ test "options with allow_hyphen_values accept option-like single values" { positionals=[ PositionalArg( "rest", - index=0, num_args=ValueRange(lower=0), allow_hyphen_values=true, ), @@ -2128,7 +2690,21 @@ test "missing option values are reported when next token is another option" { assert_true(ok.flags is { "verbose": true, .. }) try cmd.parse(argv=["--arg", "--verbose"], env=empty_env()) catch { - @argparse.ArgParseError::MissingValue(name) => assert_true(name == "--arg") + Message(msg) => + inspect( + msg, + content=( + #|error: a value is required for '--arg' but none was supplied + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --verbose + #| --arg + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -2139,8 +2715,20 @@ test "missing option values are reported when next token is another option" { test "short-only set options use short label in duplicate errors" { let cmd = @argparse.Command("demo", options=[OptionArg("mode", short='m')]) try cmd.parse(argv=["-m", "a", "-m", "b"], env=empty_env()) catch { - @argparse.ArgParseError::InvalidArgument(msg) => - inspect(msg, content="argument '-m' cannot be used multiple times") + Message(msg) => + inspect( + msg, + content=( + #|error: argument '--mode' cannot be used multiple times + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| -m, --mode + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -2154,10 +2742,19 @@ test "unknown short suggestion can be absent" { ]) try cmd.parse(argv=["-x"], env=empty_env()) catch { - @argparse.ArgParseError::UnknownArgument(arg, hint) => { - assert_true(arg == "-x") - assert_true(hint is None) - } + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '-x' found + #| + #|Usage: demo [options] + #| + #|Options: + #| --name + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -2179,7 +2776,7 @@ test "setfalse flags apply false when present" { ///| test "allow_hyphen positional treats unknown long token as value" { let cmd = @argparse.Command("demo", flags=[FlagArg("known", long="known")], positionals=[ - PositionalArg("input", index=0, allow_hyphen_values=true), + PositionalArg("input", allow_hyphen_values=true), ]) let parsed = cmd.parse(argv=["--mystery"], env=empty_env()) catch { _ => panic() @@ -2249,8 +2846,24 @@ test "child local arg shadowing inherited global is rejected at build time" { subcommands=[Command("run", options=[OptionArg("mode", long="mode")])], ).parse(argv=["run"], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => - assert_true(msg.contains("shadow")) + Message(msg) => + inspect( + msg, + content=( + #|error: arg 'mode' shadows an inherited global; rename the arg or mark it global + #| + #|Usage: demo [options] [command] + #| + #|Commands: + #| run + #| help Print help for the subcommand(s). + #| + #|Options: + #| -h, --help Show help information. + #| --mode env: MODE, default: safe + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -2342,10 +2955,24 @@ test "global set option rejects duplicate occurrences across subcommands" { try cmd.parse(argv=["--mode", "a", "run", "--mode", "b"], env=empty_env()) catch { - @argparse.ArgParseError::InvalidArgument(msg) => { - assert_true(msg.contains("--mode")) - assert_true(msg.contains("cannot be used multiple times")) - } + Message(msg) => + inspect( + msg, + content=( + #|error: argument '--mode' cannot be used multiple times + #| + #|Usage: demo [options] [command] + #| + #|Commands: + #| run + #| help Print help for the subcommand(s). + #| + #|Options: + #| -h, --help Show help information. + #| --mode + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -2363,8 +2990,24 @@ test "global override with incompatible inherited type is rejected" { ], ).parse(argv=["run", "--mode"], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => - assert_true(msg.contains("incompatible")) + Message(msg) => + inspect( + msg, + content=( + #|error: global arg 'mode' is incompatible with inherited global definition + #| + #|Usage: demo [options] [command] + #| + #|Commands: + #| run + #| help Print help for the subcommand(s). + #| + #|Options: + #| -h, --help Show help information. + #| --mode required + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -2380,10 +3023,24 @@ test "child local long alias collision with inherited global is rejected" { subcommands=[Command("run", options=[OptionArg("local", long="verbose")])], ).parse(argv=["run", "--verbose"], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => { - assert_true(msg.contains("long option")) - assert_true(msg.contains("inherited global")) - } + Message(msg) => + inspect( + msg, + content=( + #|error: arg 'local' long option --verbose conflicts with inherited global 'verbose' + #| + #|Usage: demo [options] [command] + #| + #|Commands: + #| run + #| help Print help for the subcommand(s). + #| + #|Options: + #| -h, --help Show help information. + #| --verbose + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -2399,10 +3056,24 @@ test "child local short alias collision with inherited global is rejected" { subcommands=[Command("run", options=[OptionArg("local", short='v')])], ).parse(argv=["run", "-v"], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => { - assert_true(msg.contains("short option")) - assert_true(msg.contains("inherited global")) - } + Message(msg) => + inspect( + msg, + content=( + #|error: arg 'local' short option -v conflicts with inherited global 'verbose' + #| + #|Usage: demo [options] [command] + #| + #|Commands: + #| run + #| help Print help for the subcommand(s). + #| + #|Options: + #| -h, --help Show help information. + #| -v, --verbose + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -2441,14 +3112,24 @@ test "non-bmp short option token does not panic" { ///| test "non-bmp hyphen token reports unknown argument without panic" { - let cmd = @argparse.Command("demo", positionals=[ - PositionalArg("value", index=0), - ]) + let cmd = @argparse.Command("demo", positionals=[PositionalArg("value")]) try cmd.parse(argv=["-🎉"], env=empty_env()) catch { - @argparse.ArgParseError::UnknownArgument(arg, hint) => { - assert_true(arg == "-🎉") - assert_true(hint is None) - } + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '-🎉' found + #| + #|Usage: demo [value] + #| + #|Arguments: + #| value + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -2534,8 +3215,24 @@ test "global override with different negatable setting is rejected" { ], ).parse(argv=["run"], env=empty_env()) catch { - @argparse.ArgBuildError::Unsupported(msg) => - assert_true(msg.contains("incompatible")) + Message(msg) => + inspect( + msg, + content=( + #|error: global arg 'verbose' is incompatible with inherited global definition + #| + #|Usage: demo [options] [command] + #| + #|Commands: + #| run + #| help Print help for the subcommand(s). + #| + #|Options: + #| -h, --help Show help information. + #| --[no-]verbose + #| + ), + ) _ => panic() } noraise { _ => panic() diff --git a/argparse/argparse_test.mbt b/argparse/argparse_test.mbt index 8be1612f9..372524ad4 100644 --- a/argparse/argparse_test.mbt +++ b/argparse/argparse_test.mbt @@ -23,29 +23,267 @@ test "declarative parse basics" { "demo", flags=[FlagArg("verbose", short='v', long="verbose")], options=[OptionArg("count", long="count", env="COUNT")], - positionals=[PositionalArg("name", index=0)], + positionals=[PositionalArg("name")], ) let matches = cmd.parse(argv=["-v", "--count", "3", "alice"], env=empty_env()) catch { _ => panic() } assert_true(matches.flags is { "verbose": true, .. }) assert_true(matches.values is { "count": ["3"], "name": ["alice"], .. }) + assert_true( + matches.sources is { "verbose": Argv, "count": Argv, "name": Argv, .. }, + ) } ///| -test "display help and version" { - let cmd = @argparse.Command("demo", about="demo app", version="1.2.3") +test "long defaults to name when omitted" { + let cmd = @argparse.Command("demo", flags=[FlagArg("verbose")], options=[ + OptionArg("count"), + ]) + let matches = cmd.parse(argv=["--verbose", "--count", "3"], env=empty_env()) catch { + _ => panic() + } + assert_true(matches.flags is { "verbose": true, .. }) + assert_true(matches.values is { "count": ["3"], .. }) +} - let mut help = "" - try cmd.parse(argv=["-h"], env=empty_env()) catch { - @argparse.DisplayHelp::Message(text) => help = text +///| +test "long empty string disables long alias" { + let cmd = @argparse.Command( + "demo", + flags=[FlagArg("verbose", short='v', long="")], + options=[OptionArg("count", short='c', long="")], + ) + + let matches = cmd.parse(argv=["-v", "-c", "3"], env=empty_env()) catch { _ => panic() + } + assert_true(matches.flags is { "verbose": true, .. }) + assert_true(matches.values is { "count": ["3"], .. }) + + try cmd.parse(argv=["--verbose"], env=empty_env()) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--verbose' found + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| -v + #| -c + #| + ), + ) } noraise { _ => panic() } - inspect( - help, - content=( + + try cmd.parse(argv=["--count", "3"], env=empty_env()) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--count' found + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| -v + #| -c + #| + ), + ) + } noraise { + _ => panic() + } +} + +///| +test "declaration order controls positional parsing" { + let cmd = @argparse.Command("demo", positionals=[ + PositionalArg("first"), + PositionalArg("second"), + ]) + + let parsed = cmd.parse(argv=["a", "b"], env=empty_env()) catch { + _ => panic() + } + assert_true(parsed.values is { "first": ["a"], "second": ["b"], .. }) +} + +///| +test "bounded non-last positional remains supported" { + let cmd = @argparse.Command("demo", positionals=[ + PositionalArg("first", num_args=ValueRange(lower=1, upper=2)), + PositionalArg("second", required=true), + ]) + + let two = cmd.parse(argv=["a", "b"], env=empty_env()) catch { _ => panic() } + assert_true(two.values is { "first": ["a"], "second": ["b"], .. }) + + let three = cmd.parse(argv=["a", "b", "c"], env=empty_env()) catch { + _ => panic() + } + assert_true(three.values is { "first": ["a", "b"], "second": ["c"], .. }) +} + +///| +test "negatable flag preserves false state" { + let cmd = @argparse.Command("demo", flags=[ + FlagArg("cache", long="cache", negatable=true), + ]) + + let no_cache = cmd.parse(argv=["--no-cache"], env=empty_env()) catch { + _ => panic() + } + assert_true(no_cache.flags is { "cache": false, .. }) + assert_true(no_cache.flag_counts.get("cache") is None) +} + +///| +test "parse failure message contains error and contextual help" { + let cmd = @argparse.Command("demo", options=[ + OptionArg("count", long="count", about="repeat count"), + ]) + + try cmd.parse(argv=["--bad"], env=empty_env()) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--bad' found + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --count repeat count + #| + ), + ) + } noraise { + _ => panic() + } +} + +///| +test "subcommand parse errors include subcommand help" { + let cmd = @argparse.Command("demo", subcommands=[ + Command("echo", options=[OptionArg("times", long="times")]), + ]) + + try cmd.parse(argv=["echo", "--bad"], env=empty_env()) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--bad' found + #| + #|Usage: echo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --times + #| + ), + ) + } noraise { + _ => panic() + } +} + +///| +test "build errors are surfaced as ArgError message with help" { + let cmd = @argparse.Command("demo", flags=[ + FlagArg("fast", long="fast", requires=["missing"]), + ]) + + try cmd.parse(argv=[], env=empty_env()) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: unknown requires target: fast -> missing + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --fast + #| + ), + ) + } noraise { + _ => panic() + } +} + +///| +test "unknown argument keeps suggestion in final message" { + let cmd = @argparse.Command("demo", flags=[FlagArg("verbose", long="verbose")]) + + try cmd.parse(argv=["--verbse"], env=empty_env()) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--verbse' found + #| + #| tip: a similar argument exists: '--verbose' + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --verbose + #| + ), + ) + } noraise { + _ => panic() + } +} + +///| +test "render_help remains available for pure formatting" { + let cmd = @argparse.Command( + "demo", + about="Demo command", + flags=[FlagArg("verbose", short='v', long="verbose")], + options=[OptionArg("count", long="count")], + positionals=[PositionalArg("name")], + subcommands=[Command("echo")], + ) + + let help = cmd.render_help() + assert_true(help.contains("Usage: demo [options] [name] [command]")) + assert_true(help.contains("Commands:")) + assert_true(help.contains("Options:")) +} + +///| +test "display help and version" { + let cmd = @argparse.Command("demo", about="demo app", version="1.2.3") + + inspect(cmd.render_help(), content=( + #|Usage: demo + #| + #|demo app + #| + #|Options: + #| -h, --help Show help information. + #| -V, --version Show version information. + #| + )) + + try cmd.parse(argv=["--oops"], env=empty_env()) catch { + Message(msg) => inspect(msg, content=( + #|error: unexpected argument '--oops' found + #| #|Usage: demo #| #|demo app @@ -54,34 +292,60 @@ test "display help and version" { #| -h, --help Show help information. #| -V, --version Show version information. #| - ), - ) - let mut version = "" - try cmd.parse(argv=["--version"], env=empty_env()) catch { - @argparse.DisplayVersion::Message(text) => version = text + )) _ => panic() } noraise { _ => panic() } - inspect(version, content="1.2.3") } ///| test "parse error show is readable" { - inspect( - @argparse.ArgParseError::UnknownArgument("--verbse", Some("--verbose")).to_string(), - content=( + let cmd = @argparse.Command( + "demo", + flags=[FlagArg("verbose", long="verbose")], + positionals=[PositionalArg("name")], + ) + + try cmd.parse(argv=["--verbse"], env=empty_env()) catch { + Message(msg) => inspect(msg, content=( #|error: unexpected argument '--verbse' found #| #| tip: a similar argument exists: '--verbose' - ), - ) - inspect( - @argparse.ArgParseError::TooManyPositionals.to_string(), - content=( + #| + #|Usage: demo [options] [name] + #| + #|Arguments: + #| name + #| + #|Options: + #| -h, --help Show help information. + #| --verbose + #| + )) + _ => panic() + } noraise { + _ => panic() + } + + try cmd.parse(argv=["alice", "bob"], env=empty_env()) catch { + Message(msg) => inspect(msg, content=( #|error: too many positional arguments were provided - ), - ) + #| + #|Usage: demo [options] [name] + #| + #|Arguments: + #| name + #| + #|Options: + #| -h, --help Show help information. + #| --verbose + #| + )) + _ => panic() + } noraise { + _ => panic() + } } ///| @@ -92,8 +356,17 @@ test "relationships and num args" { ]) try requires_cmd.parse(argv=["--mode", "fast"], env=empty_env()) catch { - @argparse.ArgParseError::MissingRequired(name) => - inspect(name, content="config") + Message(msg) => inspect(msg, content=( + #|error: the following required argument was not provided: 'config' + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --mode + #| --config + #| + )) _ => panic() } noraise { _ => panic() @@ -118,15 +391,40 @@ test "arg groups required and multiple" { ) try cmd.parse(argv=[], env=empty_env()) catch { - @argparse.ArgParseError::MissingGroup(name) => inspect(name, content="mode") + Message(msg) => inspect(msg, content=( + #|error: the following required argument group was not provided: 'mode' + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --fast + #| --slow + #| + #|Groups: + #| mode (required, exclusive) --fast, --slow + #| + )) _ => panic() } noraise { _ => panic() } try cmd.parse(argv=["--fast", "--slow"], env=empty_env()) catch { - @argparse.ArgParseError::GroupConflict(name) => - inspect(name, content="mode") + Message(msg) => inspect(msg, content=( + #|error: group conflict mode + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --fast + #| --slow + #| + #|Groups: + #| mode (required, exclusive) --fast, --slow + #| + )) _ => panic() } noraise { _ => panic() @@ -145,8 +443,21 @@ test "arg groups requires and conflicts" { ) try requires_cmd.parse(argv=["--fast"], env=empty_env()) catch { - @argparse.ArgParseError::MissingGroup(name) => - inspect(name, content="output") + Message(msg) => inspect(msg, content=( + #|error: the following required argument group was not provided: 'output' + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --fast + #| --json + #| + #|Groups: + #| mode --fast + #| output --json + #| + )) _ => panic() } noraise { _ => panic() @@ -162,13 +473,21 @@ test "arg groups requires and conflicts" { ) try conflict_cmd.parse(argv=["--fast", "--json"], env=empty_env()) catch { - @argparse.ArgParseError::GroupConflict(msg) => - inspect( - msg, - content=( - #|mode conflicts with output - ), - ) + Message(msg) => inspect(msg, content=( + #|error: group conflict mode conflicts with output + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --fast + #| --json + #| + #|Groups: + #| mode --fast + #| output --json + #| + )) _ => panic() } noraise { _ => panic() @@ -177,9 +496,7 @@ test "arg groups requires and conflicts" { ///| test "subcommand parsing" { - let echo = @argparse.Command("echo", positionals=[ - PositionalArg("msg", index=0), - ]) + let echo = @argparse.Command("echo", positionals=[PositionalArg("msg")]) let root = @argparse.Command("root", subcommands=[echo]) let matches = root.parse(argv=["echo", "hi"], env=empty_env()) catch { @@ -204,30 +521,27 @@ test "full help snapshot" { "1", ]), ], - positionals=[PositionalArg("name", index=0, about="Target name")], + positionals=[PositionalArg("name", about="Target name")], subcommands=[Command("echo", about="Echo a message")], ) - inspect( - cmd.render_help(), - content=( - #|Usage: demo [options] [name] [command] - #| - #|Demo command - #| - #|Commands: - #| echo Echo a message - #| help Print help for the subcommand(s). - #| - #|Arguments: - #| name Target name - #| - #|Options: - #| -h, --help Show help information. - #| -v, --verbose Enable verbose mode - #| --count Repeat count (default: 1) - #| - ), - ) + inspect(cmd.render_help(), content=( + #|Usage: demo [options] [name] [command] + #| + #|Demo command + #| + #|Commands: + #| echo Echo a message + #| help Print help for the subcommand(s). + #| + #|Arguments: + #| name Target name + #| + #|Options: + #| -h, --help Show help information. + #| -v, --verbose Enable verbose mode + #| --count Repeat count (default: 1) + #| + )) } ///| @@ -315,13 +629,19 @@ test "negatable and conflicts" { assert_true(no_failfast.flags is { "failfast": true, .. }) try cmd.parse(argv=["--verbose", "--quiet"], env=empty_env()) catch { - @argparse.ArgParseError::InvalidArgument(msg) => - inspect( - msg, - content=( - #|conflicting arguments: verbose and quiet - ), - ) + Message(msg) => inspect(msg, content=( + #|error: conflicting arguments: verbose and quiet + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --[no-]cache + #| --[no-]failfast + #| --verbose + #| --quiet + #| + )) _ => panic() } noraise { _ => panic() @@ -332,8 +652,16 @@ test "negatable and conflicts" { test "flag does not accept inline value" { let cmd = @argparse.Command("demo", flags=[FlagArg("verbose", long="verbose")]) try cmd.parse(argv=["--verbose=true"], env=empty_env()) catch { - @argparse.ArgParseError::InvalidArgument(arg) => - inspect(arg, content="--verbose=true") + Message(msg) => inspect(msg, content=( + #|error: unexpected argument '--verbose=true' found + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --verbose + #| + )) _ => panic() } noraise { _ => panic() @@ -345,16 +673,32 @@ test "built-in long flags do not accept inline value" { let cmd = @argparse.Command("demo", version="1.2.3") try cmd.parse(argv=["--help=1"], env=empty_env()) catch { - @argparse.ArgParseError::InvalidArgument(arg) => - inspect(arg, content="--help=1") + Message(msg) => inspect(msg, content=( + #|error: unexpected argument '--help=1' found + #| + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| -V, --version Show version information. + #| + )) _ => panic() } noraise { _ => panic() } try cmd.parse(argv=["--version=1"], env=empty_env()) catch { - @argparse.ArgParseError::InvalidArgument(arg) => - inspect(arg, content="--version=1") + Message(msg) => inspect(msg, content=( + #|error: unexpected argument '--version=1' found + #| + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| -V, --version Show version information. + #| + )) _ => panic() } noraise { _ => panic() @@ -364,30 +708,42 @@ test "built-in long flags do not accept inline value" { ///| test "command policies" { let help_cmd = @argparse.Command("demo", arg_required_else_help=true) - try help_cmd.parse(argv=[], env=empty_env()) catch { - @argparse.DisplayHelp::Message(text) => - inspect( - text, - content=( - #|Usage: demo - #| - #|Options: - #| -h, --help Show help information. - #| - ), - ) - _ => panic() - } noraise { - _ => panic() - } + inspect(help_cmd.render_help(), content=( + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| + )) let sub_cmd = @argparse.Command("demo", subcommand_required=true, subcommands=[ Command("echo"), ]) - assert_true(sub_cmd.render_help().has_prefix("Usage: demo ")) + inspect(sub_cmd.render_help(), content=( + #|Usage: demo + #| + #|Commands: + #| echo + #| help Print help for the subcommand(s). + #| + #|Options: + #| -h, --help Show help information. + #| + )) try sub_cmd.parse(argv=[], env=empty_env()) catch { - @argparse.ArgParseError::MissingRequired(name) => - inspect(name, content="subcommand") + Message(msg) => inspect(msg, content=( + #|error: the following required argument was not provided: 'subcommand' + #| + #|Usage: demo + #| + #|Commands: + #| echo + #| help Print help for the subcommand(s). + #| + #|Options: + #| -h, --help Show help information. + #| + )) _ => panic() } noraise { _ => panic() diff --git a/argparse/command.mbt b/argparse/command.mbt index 343837683..92af52d08 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -105,11 +105,11 @@ pub fn Command::render_help(self : Command) -> String { ///| /// Parse argv/environment according to this command spec. /// -/// Error and event model: -/// - Raises `DisplayHelp::Message` / `DisplayVersion::Message` for display -/// actions instead of exiting the process. -/// - Raises `ArgBuildError` when the command definition is invalid. -/// - Raises `ArgParseError` when user input does not satisfy the definition. +/// Behavior: +/// - Help/version requests print output immediately and terminate with exit code +/// `0`. +/// - Parse/build failures raise `ArgError::Message` where the payload includes +/// the error text and full contextual help output. /// /// Value precedence is `argv > env > default_values`. #as_free_fn @@ -117,9 +117,16 @@ pub fn Command::parse( self : Command, argv? : ArrayView[String] = default_argv(), env? : Map[String, String] = {}, -) -> Matches raise { - let raw = parse_command(self, argv, env, [], {}, {}) - build_matches(self, raw, []) +) -> Matches raise ArgError { + try { + let raw = parse_command(self, argv, env, [], {}, {}) + build_matches(self, raw, []) + } catch { + DisplayHelp::Message(text) => print_and_exit_success(text) + DisplayVersion::Message(text) => print_and_exit_success(text) + ArgError::Message(msg) => raise Message(msg) + _ => panic() + } } ///| diff --git a/argparse/error.mbt b/argparse/error.mbt index af67cae31..d01e2870f 100644 --- a/argparse/error.mbt +++ b/argparse/error.mbt @@ -13,8 +13,21 @@ // limitations under the License. ///| -/// Errors raised during parsing or decoding of arguments. -pub(all) suberror ArgParseError { +/// Unified error surface exposed by argparse. +pub suberror ArgError { + Message(String) +} + +///| +pub impl Show for ArgError with output(self : ArgError, logger) { + match self { + Message(msg) => logger.write_string(msg) + } +} + +///| +/// Internal parse error variants used while parsing. +priv suberror ArgParseError { UnknownArgument(String, String?) InvalidArgument(String) MissingValue(String) @@ -27,11 +40,6 @@ pub(all) suberror ArgParseError { GroupConflict(String) } -///| -pub impl Show for ArgParseError with output(self : ArgParseError, logger) { - logger.write_string(self.arg_parse_error_message()) -} - ///| fn ArgParseError::arg_parse_error_message(self : ArgParseError) -> String { match self { @@ -65,19 +73,26 @@ fn ArgParseError::arg_parse_error_message(self : ArgParseError) -> String { } ///| -/// Errors raised when building argument specifications. -pub suberror ArgBuildError { +/// Internal build errors raised while validating command definitions. +priv suberror ArgBuildError { Unsupported(String) -} derive(Show) +} + +///| +fn ArgBuildError::arg_build_error_message(self : ArgBuildError) -> String { + match self { + Unsupported(msg) => "error: \{msg}" + } +} ///| -/// Errors raised when help information is requested. -pub suberror DisplayHelp { +/// Internal control-flow event for displaying help. +priv suberror DisplayHelp { Message(String) -} derive(Show) +} ///| -/// Errors raised when version information is requested. -pub suberror DisplayVersion { +/// Internal control-flow event for displaying version text. +priv suberror DisplayVersion { Message(String) -} derive(Show) +} diff --git a/argparse/matches.mbt b/argparse/matches.mbt index b172101a8..4ba56189a 100644 --- a/argparse/matches.mbt +++ b/argparse/matches.mbt @@ -51,17 +51,3 @@ fn new_matches_parse_state() -> Matches { parsed_subcommand: None, } } - -///| -/// Decode a full argument struct/enum from `Matches`. -pub(open) trait FromMatches { - from_matches(matches : Matches) -> Self raise ArgParseError -} - -///| -/// Decode a full argument struct/enum from `Matches`. -pub fn[T : FromMatches] from_matches( - matches : Matches, -) -> T raise ArgParseError { - T::from_matches(matches) -} diff --git a/argparse/parser.mbt b/argparse/parser.mbt index 261d73ef1..f4dd857f4 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -93,6 +93,45 @@ fn merge_global_defs( merged } +///| +fn format_error_with_help( + msg : String, + cmd : Command, + inherited_globals : Array[Arg], +) -> String { + "\{msg}\n\n\{render_help_for_context(cmd, inherited_globals)}" +} + +///| +fn arg_error_for_parse_failure( + err : ArgParseError, + cmd : Command, + inherited_globals : Array[Arg], +) -> ArgError { + Message( + format_error_with_help( + err.arg_parse_error_message(), + cmd, + inherited_globals, + ), + ) +} + +///| +fn arg_error_for_build_failure( + err : ArgBuildError, + cmd : Command, + inherited_globals : Array[Arg], +) -> ArgError { + Message( + format_error_with_help( + err.arg_build_error_message(), + cmd, + inherited_globals, + ), + ) +} + ///| fn parse_command( cmd : Command, @@ -101,6 +140,88 @@ fn parse_command( inherited_globals : Array[Arg], inherited_version_long : Map[String, String], inherited_version_short : Map[Char, String], +) -> Matches raise { + parse_command_impl( + cmd, argv, env, inherited_globals, inherited_version_long, inherited_version_short, + ) catch { + UnknownArgument(arg, hint) => + raise arg_error_for_parse_failure( + UnknownArgument(arg, hint), + cmd, + inherited_globals, + ) + InvalidArgument(msg) => + raise arg_error_for_parse_failure( + InvalidArgument(msg), + cmd, + inherited_globals, + ) + MissingValue(name) => + raise arg_error_for_parse_failure( + MissingValue(name), + cmd, + inherited_globals, + ) + MissingRequired(name) => + raise arg_error_for_parse_failure( + MissingRequired(name), + cmd, + inherited_globals, + ) + TooFewValues(name, got, min) => + raise arg_error_for_parse_failure( + TooFewValues(name, got, min), + cmd, + inherited_globals, + ) + TooManyValues(name, got, max) => + raise arg_error_for_parse_failure( + TooManyValues(name, got, max), + cmd, + inherited_globals, + ) + TooManyPositionals => + raise arg_error_for_parse_failure( + TooManyPositionals, + cmd, + inherited_globals, + ) + InvalidValue(msg) => + raise arg_error_for_parse_failure( + InvalidValue(msg), + cmd, + inherited_globals, + ) + MissingGroup(name) => + raise arg_error_for_parse_failure( + MissingGroup(name), + cmd, + inherited_globals, + ) + GroupConflict(name) => + raise arg_error_for_parse_failure( + GroupConflict(name), + cmd, + inherited_globals, + ) + Unsupported(msg) => + raise arg_error_for_build_failure( + Unsupported(msg), + cmd, + inherited_globals, + ) + err => raise err + } +} + +///| +fn parse_command_impl( + cmd : Command, + argv : ArrayView[String], + env : Map[String, String], + inherited_globals : Array[Arg], + inherited_version_long : Map[String, String], + inherited_version_short : Map[Char, String], ) -> Matches raise { match cmd.build_error { Some(err) => raise err @@ -139,9 +260,7 @@ fn parse_command( long_index.get("version") is None let positionals = positional_args(args) let positional_values = [] - let last_pos_idx = positionals.search_by(arg => { - arg.info is Positional(last=true, ..) - }) + let last_pos_idx = last_positional_index(positionals) let mut i = 0 let mut positional_arg_found = false while i < argv.length() { diff --git a/argparse/parser_positionals.mbt b/argparse/parser_positionals.mbt index 847c8986a..036e5f4e3 100644 --- a/argparse/parser_positionals.mbt +++ b/argparse/parser_positionals.mbt @@ -13,40 +13,25 @@ // limitations under the License. ///| -fn positional_indexed_args(args : Array[Arg]) -> Array[(Int, Arg)] { - let entries = [] - let mut next_index = 0 +fn positional_args(args : Array[Arg]) -> Array[Arg] { + let ordered = [] for arg in args { - if arg.info is Positional(index~, ..) { - let resolved = match index { - Some(value) => { - let candidate = value + 1 - if candidate > next_index { - next_index = candidate - } - value - } - None => { - let value = next_index - next_index = value + 1 - value - } - } - entries.push((resolved, arg)) + if arg.info is Positional(_) { + ordered.push(arg) } } - entries.sort_by_key(pair => pair.0) - entries + ordered } ///| -fn positional_args(args : Array[Arg]) -> Array[Arg] { - let ordered = [] - for item in positional_indexed_args(args) { - let (_, arg) = item - ordered.push(arg) +fn last_positional_index(positionals : Array[Arg]) -> Int? { + for idx in 0.. - if index is Some(index) && - !self.seen_positional_indices.add_and_check(index) { - raise Unsupported("duplicate positional index: \{index}") - } + Positional(_) => () } self.args.push(arg) } @@ -91,8 +85,6 @@ fn ValidationCtx::record_arg( ///| fn ValidationCtx::finalize(self : ValidationCtx) -> Unit raise ArgBuildError { validate_requires_conflicts_targets(self.args, self.seen_names) - validate_positional_index_layout(self.args) - validate_indexed_positional_num_args(self.args) } ///| @@ -282,49 +274,6 @@ fn inherited_global_short_owner( None } -///| -fn validate_indexed_positional_num_args( - args : Array[Arg], -) -> Unit raise ArgBuildError { - let positionals = positional_args(args) - if positionals.length() <= 1 { - return - } - let mut idx = 0 - while idx + 1 < positionals.length() { - let arg = positionals[idx] - guard arg.info is Positional(index~, num_args~, ..) - if index is Some(_) && num_args is Some(range) { - if !(range is { lower: 1, upper: Some(1) }) { - raise Unsupported( - "indexed positional '\{arg.name}' cannot set num_args unless it is the last positional or exactly 1..1", - ) - } - } - idx = idx + 1 - } -} - -///| -fn validate_positional_index_layout( - args : Array[Arg], -) -> Unit raise ArgBuildError { - let indexed = positional_indexed_args(args) - let mut expected = 0 - for item in indexed { - let (index, _) = item - if index < expected { - raise Unsupported("duplicate positional index: \{index}") - } - if index > expected { - raise Unsupported( - "positional indices cannot skip values; missing index: \{expected}", - ) - } - expected = expected + 1 - } -} - ///| fn validate_flag_arg( arg : Arg, @@ -374,8 +323,8 @@ fn validate_positional_arg( ///| fn validate_named_option_arg(arg : Arg) -> Unit raise ArgBuildError { guard arg.info is (Flag(long~, short~, ..) | Option(long~, short~, ..)) - guard long is Some(_) || short is Some(_) else { - raise Unsupported("flag/option args require short/long") + guard long is Some(_) || short is Some(_) || arg.env is Some(_) else { + raise Unsupported("flag/option args require short/long/env") } } diff --git a/argparse/pkg.generated.mbti b/argparse/pkg.generated.mbti index f0104e42a..53be1bc70 100644 --- a/argparse/pkg.generated.mbti +++ b/argparse/pkg.generated.mbti @@ -6,37 +6,12 @@ import { } // Values -pub fn[T : FromMatches] from_matches(Matches) -> T raise ArgParseError // Errors -pub suberror ArgBuildError { - Unsupported(String) -} -pub impl Show for ArgBuildError - -pub(all) suberror ArgParseError { - UnknownArgument(String, String?) - InvalidArgument(String) - MissingValue(String) - MissingRequired(String) - TooFewValues(String, Int, Int) - TooManyValues(String, Int, Int) - TooManyPositionals - InvalidValue(String) - MissingGroup(String) - GroupConflict(String) -} -pub impl Show for ArgParseError - -pub suberror DisplayHelp { +pub suberror ArgError { Message(String) } -pub impl Show for DisplayHelp - -pub suberror DisplayVersion { - Message(String) -} -pub impl Show for DisplayVersion +pub impl Show for ArgError // Types and methods pub struct ArgGroup { @@ -53,7 +28,7 @@ pub struct Command { } pub fn Command::new(StringView, flags? : ArrayView[FlagArg], options? : ArrayView[OptionArg], positionals? : ArrayView[PositionalArg], subcommands? : ArrayView[Self], 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]) -> Self #as_free_fn -pub fn Command::parse(Self, argv? : ArrayView[String], env? : Map[String, String]) -> Matches raise +pub fn Command::parse(Self, argv? : ArrayView[String], env? : Map[String, String]) -> Matches raise ArgError pub fn Command::render_help(Self) -> String pub(all) enum FlagAction { @@ -100,9 +75,9 @@ pub fn OptionArg::new(StringView, short? : Char, long? : StringView, about? : St pub struct PositionalArg { // private fields - fn new(StringView, index? : Int, about? : StringView, env? : StringView, default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : ArrayView[String], conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, hidden? : Bool) -> PositionalArg + fn new(StringView, about? : StringView, env? : StringView, default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : ArrayView[String], conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, hidden? : Bool) -> PositionalArg } -pub fn PositionalArg::new(StringView, index? : Int, about? : StringView, env? : StringView, default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : ArrayView[String], conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, hidden? : Bool) -> Self +pub fn PositionalArg::new(StringView, about? : StringView, env? : StringView, default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : ArrayView[String], conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, hidden? : Bool) -> Self pub struct ValueRange { // private fields @@ -126,7 +101,4 @@ pub impl @debug.Debug for ValueSource // Type aliases // Traits -pub(open) trait FromMatches { - from_matches(Matches) -> Self raise ArgParseError -} diff --git a/argparse/runtime_exit.mbt b/argparse/runtime_exit.mbt new file mode 100644 index 000000000..8f796fe59 --- /dev/null +++ b/argparse/runtime_exit.mbt @@ -0,0 +1,55 @@ +// 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. + +///| +fn println_runtime(s : String) -> Unit = "%println" + +///| +fn trim_single_trailing_newline(text : String) -> String { + match text.strip_suffix("\n") { + Some(view) => view.to_string() + None => text + } +} + +///| +fn[T] print_and_exit_success(text : String) -> T { + println_runtime(trim_single_trailing_newline(text)) + runtime_exit_success() + panic() +} + +///| +#cfg(target="native") +fn runtime_exit_success() -> Unit { + runtime_exit_success_native() +} + +///| +#cfg(target="js") +fn runtime_exit_success() -> Unit { + runtime_exit_success_js() +} + +///| +#cfg(target="wasm-gc") +fn runtime_exit_success() -> Unit { + runtime_exit_success_wasm() +} + +///| +#cfg(target="wasm") +fn runtime_exit_success() -> Unit { + runtime_exit_success_wasm() +} diff --git a/argparse/runtime_exit_js.mbt b/argparse/runtime_exit_js.mbt new file mode 100644 index 000000000..627077ba6 --- /dev/null +++ b/argparse/runtime_exit_js.mbt @@ -0,0 +1,32 @@ +// 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. + +///| +#cfg(target="js") +extern "js" fn runtime_js_try_exit(code : Int) -> Bool = + #| function(code) { + #| if (typeof process !== "undefined" && typeof process.exit === "function") { + #| process.exit(code); + #| return true; + #| } + #| return false; + #| } + +///| +#cfg(target="js") +fn runtime_exit_success_js() -> Unit { + if !runtime_js_try_exit(0) { + panic() + } +} diff --git a/argparse/runtime_exit_native.mbt b/argparse/runtime_exit_native.mbt new file mode 100644 index 000000000..f05586b96 --- /dev/null +++ b/argparse/runtime_exit_native.mbt @@ -0,0 +1,23 @@ +// 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. + +///| +#cfg(target="native") +extern "c" fn runtime_native_exit(code : Int) -> Unit = "exit" + +///| +#cfg(target="native") +fn runtime_exit_success_native() -> Unit { + runtime_native_exit(0) +} diff --git a/argparse/runtime_exit_wasm.mbt b/argparse/runtime_exit_wasm.mbt new file mode 100644 index 000000000..d97fee243 --- /dev/null +++ b/argparse/runtime_exit_wasm.mbt @@ -0,0 +1,33 @@ +// 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. + +///| +#cfg(target="wasm-gc") +fn runtime_wasm_exit(code : Int) -> Unit = "__moonbit_sys_unstable" "exit" + +///| +#cfg(target="wasm-gc") +fn runtime_exit_success_wasm() -> Unit { + runtime_wasm_exit(0) +} + +///| +#cfg(target="wasm") +fn runtime_wasm_exit(code : Int) -> Unit = "__moonbit_sys_unstable" "exit" + +///| +#cfg(target="wasm") +fn runtime_exit_success_wasm() -> Unit { + runtime_wasm_exit(0) +} From 6ae10709711f897e828f9482c53bb7341300f12c Mon Sep 17 00:00:00 2001 From: zihang Date: Sat, 28 Feb 2026 17:46:04 +0800 Subject: [PATCH 21/40] fix(argparse): include parent path in subcommand help output --- argparse/README.mbt.md | 4 +- argparse/argparse_blackbox_test.mbt | 4 +- argparse/argparse_test.mbt | 460 ++++++++++++++++------------ argparse/command.mbt | 2 +- argparse/parser.mbt | 60 ++-- argparse/parser_lookup.mbt | 11 +- 6 files changed, 318 insertions(+), 223 deletions(-) diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index a3726b32a..20fae1c27 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -154,7 +154,7 @@ test "subcommand context failure snapshot" { content=( #|error: unexpected argument '--oops' found #| - #|Usage: run [options] + #|Usage: demo run [options] #| #|Options: #| -h, --help Show help information. @@ -264,7 +264,7 @@ test "subcommand invalid option snapshot" { content=( #|error: unexpected argument '--oops' found #| - #|Usage: echo [options] + #|Usage: demo echo [options] #| #|Options: #| -h, --help Show help information. diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index f6ef32147..57ddfe19e 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -501,7 +501,7 @@ test "subcommand help includes inherited global options" { content=( #|error: unexpected argument '--bad' found #| - #|Usage: echo [options] + #|Usage: demo echo [options] #| #|echo #| @@ -2471,7 +2471,7 @@ test "global version action keeps parent version text in subcommand context" { content=( #|error: unexpected argument '--oops' found #| - #|Usage: run [options] + #|Usage: demo run [options] #| #|Options: #| -h, --help Show help information. diff --git a/argparse/argparse_test.mbt b/argparse/argparse_test.mbt index 372524ad4..3894b7b83 100644 --- a/argparse/argparse_test.mbt +++ b/argparse/argparse_test.mbt @@ -183,7 +183,7 @@ test "subcommand parse errors include subcommand help" { content=( #|error: unexpected argument '--bad' found #| - #|Usage: echo [options] + #|Usage: demo echo [options] #| #|Options: #| -h, --help Show help information. @@ -269,21 +269,9 @@ test "render_help remains available for pure formatting" { test "display help and version" { let cmd = @argparse.Command("demo", about="demo app", version="1.2.3") - inspect(cmd.render_help(), content=( - #|Usage: demo - #| - #|demo app - #| - #|Options: - #| -h, --help Show help information. - #| -V, --version Show version information. - #| - )) - - try cmd.parse(argv=["--oops"], env=empty_env()) catch { - Message(msg) => inspect(msg, content=( - #|error: unexpected argument '--oops' found - #| + inspect( + cmd.render_help(), + content=( #|Usage: demo #| #|demo app @@ -292,7 +280,26 @@ test "display help and version" { #| -h, --help Show help information. #| -V, --version Show version information. #| - )) + ), + ) + + try cmd.parse(argv=["--oops"], env=empty_env()) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--oops' found + #| + #|Usage: demo + #| + #|demo app + #| + #|Options: + #| -h, --help Show help information. + #| -V, --version Show version information. + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -308,40 +315,48 @@ test "parse error show is readable" { ) try cmd.parse(argv=["--verbse"], env=empty_env()) catch { - Message(msg) => inspect(msg, content=( - #|error: unexpected argument '--verbse' found - #| - #| tip: a similar argument exists: '--verbose' - #| - #|Usage: demo [options] [name] - #| - #|Arguments: - #| name - #| - #|Options: - #| -h, --help Show help information. - #| --verbose - #| - )) + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--verbse' found + #| + #| tip: a similar argument exists: '--verbose' + #| + #|Usage: demo [options] [name] + #| + #|Arguments: + #| name + #| + #|Options: + #| -h, --help Show help information. + #| --verbose + #| + ), + ) _ => panic() } noraise { _ => panic() } try cmd.parse(argv=["alice", "bob"], env=empty_env()) catch { - Message(msg) => inspect(msg, content=( - #|error: too many positional arguments were provided - #| - #|Usage: demo [options] [name] - #| - #|Arguments: - #| name - #| - #|Options: - #| -h, --help Show help information. - #| --verbose - #| - )) + Message(msg) => + inspect( + msg, + content=( + #|error: too many positional arguments were provided + #| + #|Usage: demo [options] [name] + #| + #|Arguments: + #| name + #| + #|Options: + #| -h, --help Show help information. + #| --verbose + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -356,17 +371,21 @@ test "relationships and num args" { ]) try requires_cmd.parse(argv=["--mode", "fast"], env=empty_env()) catch { - Message(msg) => inspect(msg, content=( - #|error: the following required argument was not provided: 'config' - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --mode - #| --config - #| - )) + Message(msg) => + inspect( + msg, + content=( + #|error: the following required argument was not provided: 'config' + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --mode + #| --config + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -391,40 +410,48 @@ test "arg groups required and multiple" { ) try cmd.parse(argv=[], env=empty_env()) catch { - Message(msg) => inspect(msg, content=( - #|error: the following required argument group was not provided: 'mode' - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --fast - #| --slow - #| - #|Groups: - #| mode (required, exclusive) --fast, --slow - #| - )) + Message(msg) => + inspect( + msg, + content=( + #|error: the following required argument group was not provided: 'mode' + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --fast + #| --slow + #| + #|Groups: + #| mode (required, exclusive) --fast, --slow + #| + ), + ) _ => panic() } noraise { _ => panic() } try cmd.parse(argv=["--fast", "--slow"], env=empty_env()) catch { - Message(msg) => inspect(msg, content=( - #|error: group conflict mode - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --fast - #| --slow - #| - #|Groups: - #| mode (required, exclusive) --fast, --slow - #| - )) + Message(msg) => + inspect( + msg, + content=( + #|error: group conflict mode + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --fast + #| --slow + #| + #|Groups: + #| mode (required, exclusive) --fast, --slow + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -443,21 +470,25 @@ test "arg groups requires and conflicts" { ) try requires_cmd.parse(argv=["--fast"], env=empty_env()) catch { - Message(msg) => inspect(msg, content=( - #|error: the following required argument group was not provided: 'output' - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --fast - #| --json - #| - #|Groups: - #| mode --fast - #| output --json - #| - )) + Message(msg) => + inspect( + msg, + content=( + #|error: the following required argument group was not provided: 'output' + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --fast + #| --json + #| + #|Groups: + #| mode --fast + #| output --json + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -473,21 +504,25 @@ test "arg groups requires and conflicts" { ) try conflict_cmd.parse(argv=["--fast", "--json"], env=empty_env()) catch { - Message(msg) => inspect(msg, content=( - #|error: group conflict mode conflicts with output - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --fast - #| --json - #| - #|Groups: - #| mode --fast - #| output --json - #| - )) + Message(msg) => + inspect( + msg, + content=( + #|error: group conflict mode conflicts with output + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --fast + #| --json + #| + #|Groups: + #| mode --fast + #| output --json + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -524,24 +559,27 @@ test "full help snapshot" { positionals=[PositionalArg("name", about="Target name")], subcommands=[Command("echo", about="Echo a message")], ) - inspect(cmd.render_help(), content=( - #|Usage: demo [options] [name] [command] - #| - #|Demo command - #| - #|Commands: - #| echo Echo a message - #| help Print help for the subcommand(s). - #| - #|Arguments: - #| name Target name - #| - #|Options: - #| -h, --help Show help information. - #| -v, --verbose Enable verbose mode - #| --count Repeat count (default: 1) - #| - )) + inspect( + cmd.render_help(), + content=( + #|Usage: demo [options] [name] [command] + #| + #|Demo command + #| + #|Commands: + #| echo Echo a message + #| help Print help for the subcommand(s). + #| + #|Arguments: + #| name Target name + #| + #|Options: + #| -h, --help Show help information. + #| -v, --verbose Enable verbose mode + #| --count Repeat count (default: 1) + #| + ), + ) } ///| @@ -629,19 +667,23 @@ test "negatable and conflicts" { assert_true(no_failfast.flags is { "failfast": true, .. }) try cmd.parse(argv=["--verbose", "--quiet"], env=empty_env()) catch { - Message(msg) => inspect(msg, content=( - #|error: conflicting arguments: verbose and quiet - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --[no-]cache - #| --[no-]failfast - #| --verbose - #| --quiet - #| - )) + Message(msg) => + inspect( + msg, + content=( + #|error: conflicting arguments: verbose and quiet + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --[no-]cache + #| --[no-]failfast + #| --verbose + #| --quiet + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -652,16 +694,20 @@ test "negatable and conflicts" { test "flag does not accept inline value" { let cmd = @argparse.Command("demo", flags=[FlagArg("verbose", long="verbose")]) try cmd.parse(argv=["--verbose=true"], env=empty_env()) catch { - Message(msg) => inspect(msg, content=( - #|error: unexpected argument '--verbose=true' found - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --verbose - #| - )) + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--verbose=true' found + #| + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --verbose + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -673,32 +719,40 @@ test "built-in long flags do not accept inline value" { let cmd = @argparse.Command("demo", version="1.2.3") try cmd.parse(argv=["--help=1"], env=empty_env()) catch { - Message(msg) => inspect(msg, content=( - #|error: unexpected argument '--help=1' found - #| - #|Usage: demo - #| - #|Options: - #| -h, --help Show help information. - #| -V, --version Show version information. - #| - )) + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--help=1' found + #| + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| -V, --version Show version information. + #| + ), + ) _ => panic() } noraise { _ => panic() } try cmd.parse(argv=["--version=1"], env=empty_env()) catch { - Message(msg) => inspect(msg, content=( - #|error: unexpected argument '--version=1' found - #| - #|Usage: demo - #| - #|Options: - #| -h, --help Show help information. - #| -V, --version Show version information. - #| - )) + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--version=1' found + #| + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| -V, --version Show version information. + #| + ), + ) _ => panic() } noraise { _ => panic() @@ -708,32 +762,23 @@ test "built-in long flags do not accept inline value" { ///| test "command policies" { let help_cmd = @argparse.Command("demo", arg_required_else_help=true) - inspect(help_cmd.render_help(), content=( - #|Usage: demo - #| - #|Options: - #| -h, --help Show help information. - #| - )) + inspect( + help_cmd.render_help(), + content=( + #|Usage: demo + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) let sub_cmd = @argparse.Command("demo", subcommand_required=true, subcommands=[ Command("echo"), ]) - inspect(sub_cmd.render_help(), content=( - #|Usage: demo - #| - #|Commands: - #| echo - #| help Print help for the subcommand(s). - #| - #|Options: - #| -h, --help Show help information. - #| - )) - try sub_cmd.parse(argv=[], env=empty_env()) catch { - Message(msg) => inspect(msg, content=( - #|error: the following required argument was not provided: 'subcommand' - #| + inspect( + sub_cmd.render_help(), + content=( #|Usage: demo #| #|Commands: @@ -743,7 +788,26 @@ test "command policies" { #|Options: #| -h, --help Show help information. #| - )) + ), + ) + try sub_cmd.parse(argv=[], env=empty_env()) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: the following required argument was not provided: 'subcommand' + #| + #|Usage: demo + #| + #|Commands: + #| echo + #| help Print help for the subcommand(s). + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) _ => panic() } noraise { _ => panic() diff --git a/argparse/command.mbt b/argparse/command.mbt index 92af52d08..b36664865 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -119,7 +119,7 @@ pub fn Command::parse( env? : Map[String, String] = {}, ) -> Matches raise ArgError { try { - let raw = parse_command(self, argv, env, [], {}, {}) + let raw = parse_command(self, argv, env, [], {}, {}, self.name) build_matches(self, raw, []) } catch { DisplayHelp::Message(text) => print_and_exit_success(text) diff --git a/argparse/parser.mbt b/argparse/parser.mbt index f4dd857f4..5e2da2c33 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -51,12 +51,10 @@ fn[T] raise_subcommand_conflict(name : String) -> T raise ArgParseError { fn render_help_for_context( cmd : Command, inherited_globals : Array[Arg], + command_path : String, ) -> String { - let help_cmd = if inherited_globals.length() == 0 { - cmd - } else { - { ..cmd, args: inherited_globals + cmd.args } - } + let help_name = if command_path == "" { cmd.name } else { command_path } + let help_cmd = { ..cmd, args: inherited_globals + cmd.args, name: help_name } render_help(help_cmd) } @@ -64,8 +62,9 @@ fn render_help_for_context( fn raise_context_help( cmd : Command, inherited_globals : Array[Arg], + command_path : String, ) -> Unit raise DisplayHelp { - raise_help(render_help_for_context(cmd, inherited_globals)) + raise_help(render_help_for_context(cmd, inherited_globals, command_path)) } ///| @@ -98,8 +97,9 @@ fn format_error_with_help( msg : String, cmd : Command, inherited_globals : Array[Arg], + command_path : String, ) -> String { - "\{msg}\n\n\{render_help_for_context(cmd, inherited_globals)}" + "\{msg}\n\n\{render_help_for_context(cmd, inherited_globals, command_path)}" } ///| @@ -107,12 +107,14 @@ fn arg_error_for_parse_failure( err : ArgParseError, cmd : Command, inherited_globals : Array[Arg], + command_path : String, ) -> ArgError { Message( format_error_with_help( err.arg_parse_error_message(), cmd, inherited_globals, + command_path, ), ) } @@ -122,12 +124,14 @@ fn arg_error_for_build_failure( err : ArgBuildError, cmd : Command, inherited_globals : Array[Arg], + command_path : String, ) -> ArgError { Message( format_error_with_help( err.arg_build_error_message(), cmd, inherited_globals, + command_path, ), ) } @@ -140,75 +144,88 @@ fn parse_command( inherited_globals : Array[Arg], inherited_version_long : Map[String, String], inherited_version_short : Map[Char, String], + command_path : String, ) -> Matches raise { parse_command_impl( cmd, argv, env, inherited_globals, inherited_version_long, inherited_version_short, + command_path, ) catch { UnknownArgument(arg, hint) => raise arg_error_for_parse_failure( UnknownArgument(arg, hint), cmd, inherited_globals, + command_path, ) InvalidArgument(msg) => raise arg_error_for_parse_failure( InvalidArgument(msg), cmd, inherited_globals, + command_path, ) MissingValue(name) => raise arg_error_for_parse_failure( MissingValue(name), cmd, inherited_globals, + command_path, ) MissingRequired(name) => raise arg_error_for_parse_failure( MissingRequired(name), cmd, inherited_globals, + command_path, ) TooFewValues(name, got, min) => raise arg_error_for_parse_failure( TooFewValues(name, got, min), cmd, inherited_globals, + command_path, ) TooManyValues(name, got, max) => raise arg_error_for_parse_failure( TooManyValues(name, got, max), cmd, inherited_globals, + command_path, ) TooManyPositionals => raise arg_error_for_parse_failure( TooManyPositionals, cmd, inherited_globals, + command_path, ) InvalidValue(msg) => raise arg_error_for_parse_failure( InvalidValue(msg), cmd, inherited_globals, + command_path, ) MissingGroup(name) => raise arg_error_for_parse_failure( MissingGroup(name), cmd, inherited_globals, + command_path, ) GroupConflict(name) => raise arg_error_for_parse_failure( GroupConflict(name), cmd, inherited_globals, + command_path, ) Unsupported(msg) => raise arg_error_for_build_failure( Unsupported(msg), cmd, inherited_globals, + command_path, ) err => raise err } @@ -222,6 +239,7 @@ fn parse_command_impl( inherited_globals : Array[Arg], inherited_version_long : Map[String, String], inherited_version_short : Map[Char, String], + command_path : String, ) -> Matches raise { match cmd.build_error { Some(err) => raise err @@ -231,7 +249,7 @@ fn parse_command_impl( let groups = cmd.groups let subcommands = cmd.subcommands if cmd.arg_required_else_help && argv.length() == 0 { - raise_context_help(cmd, inherited_globals) + raise_context_help(cmd, inherited_globals, command_path) } let matches = new_matches_parse_state() let globals_here = collect_globals(args) @@ -285,10 +303,10 @@ fn parse_command_impl( continue } if builtin_help_short && arg == "-h" { - raise_context_help(cmd, inherited_globals) + raise_context_help(cmd, inherited_globals, command_path) } if builtin_help_long && arg == "--help" { - raise_context_help(cmd, inherited_globals) + raise_context_help(cmd, inherited_globals, command_path) } if builtin_version_short && arg == "-V" { raise_version(command_version(cmd)) @@ -310,7 +328,7 @@ fn parse_command_impl( if inline is Some(_) { raise ArgParseError::InvalidArgument(arg) } - raise_context_help(cmd, inherited_globals) + raise_context_help(cmd, inherited_globals, command_path) } if builtin_version_long && name == "version" { if inline is Some(_) { @@ -365,7 +383,7 @@ fn parse_command_impl( } match spec.info { Flag(action=Help, ..) => - raise_context_help(cmd, inherited_globals) + raise_context_help(cmd, inherited_globals, command_path) Flag(action=Version, ..) => raise_version( version_text_for_long_action( @@ -385,7 +403,7 @@ fn parse_command_impl( while pos < arg.length() { let short = arg.get_char(pos).unwrap() if short == 'h' && builtin_help_short { - raise_context_help(cmd, inherited_globals) + raise_context_help(cmd, inherited_globals, command_path) } if short == 'V' && builtin_version_short { raise_version(command_version(cmd)) @@ -421,7 +439,8 @@ fn parse_command_impl( break } else { match spec.info { - Flag(action=Help, ..) => raise_context_help(cmd, inherited_globals) + Flag(action=Help, ..) => + raise_context_help(cmd, inherited_globals, command_path) Flag(action=Version, ..) => raise_version( version_text_for_short_action( @@ -441,10 +460,10 @@ fn parse_command_impl( raise_subcommand_conflict("help") } let rest = argv[i + 1:].to_array() - let (target, target_globals) = resolve_help_target( - cmd, rest, builtin_help_short, builtin_help_long, inherited_globals, + let (target, target_globals, target_path) = resolve_help_target( + cmd, rest, builtin_help_short, builtin_help_long, inherited_globals, command_path, ) - let text = render_help_for_context(target, target_globals) + let text = render_help_for_context(target, target_globals, target_path) raise_help(text) } if subcommands.iter().find_first(sub => sub.name == arg) is Some(sub) { @@ -452,8 +471,13 @@ fn parse_command_impl( raise_subcommand_conflict(sub.name) } let rest = argv[i + 1:].to_array() + let sub_path = if command_path == "" { + sub.name + } else { + "\{command_path} \{sub.name}" + } let sub_matches = parse_command( - sub, rest, env, child_globals, child_version_long, child_version_short, + sub, rest, env, child_globals, child_version_long, child_version_short, sub_path, ) let child_local_non_globals = collect_non_global_names(sub.args) matches.parsed_subcommand = Some((sub.name, sub_matches)) diff --git a/argparse/parser_lookup.mbt b/argparse/parser_lookup.mbt index ca8082b08..38282e75c 100644 --- a/argparse/parser_lookup.mbt +++ b/argparse/parser_lookup.mbt @@ -66,7 +66,8 @@ fn resolve_help_target( builtin_help_short : Bool, builtin_help_long : Bool, inherited_globals : Array[Arg], -) -> (Command, Array[Arg]) raise ArgParseError { + command_path : String, +) -> (Command, Array[Arg], String) raise ArgParseError { let targets = if argv.length() == 0 { argv } else { @@ -79,6 +80,7 @@ fn resolve_help_target( } } let mut current = cmd + let mut current_path = command_path let mut current_globals = inherited_globals let mut subs = cmd.subcommands for name in targets { @@ -90,9 +92,14 @@ fn resolve_help_target( } current_globals = current_globals + collect_globals(current.args) current = sub + current_path = if current_path == "" { + sub.name + } else { + "\{current_path} \{sub.name}" + } subs = sub.subcommands } - (current, current_globals) + (current, current_globals, current_path) } ///| From dceb58763cf508161d32f900772a08f953461045 Mon Sep 17 00:00:00 2001 From: zihang Date: Sat, 28 Feb 2026 18:14:39 +0800 Subject: [PATCH 22/40] fix(argparse): include requires source in missing argument errors --- argparse/argparse_test.mbt | 3 ++- argparse/error.mbt | 10 +++++++--- argparse/help_render.mbt | 1 - argparse/parser.mbt | 4 ++-- argparse/parser_validate.mbt | 8 ++++---- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/argparse/argparse_test.mbt b/argparse/argparse_test.mbt index 3894b7b83..20af392c6 100644 --- a/argparse/argparse_test.mbt +++ b/argparse/argparse_test.mbt @@ -375,7 +375,7 @@ test "relationships and num args" { inspect( msg, content=( - #|error: the following required argument was not provided: 'config' + #|error: the following required argument was not provided: 'config' (required by 'mode') #| #|Usage: demo [options] #| @@ -384,6 +384,7 @@ test "relationships and num args" { #| --mode #| --config #| + ), ) _ => panic() diff --git a/argparse/error.mbt b/argparse/error.mbt index d01e2870f..8db171886 100644 --- a/argparse/error.mbt +++ b/argparse/error.mbt @@ -31,7 +31,7 @@ priv suberror ArgParseError { UnknownArgument(String, String?) InvalidArgument(String) MissingValue(String) - MissingRequired(String) + MissingRequired(String, String?) TooFewValues(String, Int, Int) TooManyValues(String, Int, Int) TooManyPositionals @@ -58,8 +58,12 @@ fn ArgParseError::arg_parse_error_message(self : ArgParseError) -> String { } MissingValue(arg) => "error: a value is required for '\{arg}' but none was supplied" - MissingRequired(name) => - "error: the following required argument was not provided: '\{name}'" + MissingRequired(name, by) => + if by is Some(source) { + "error: the following required argument was not provided: '\{name}' (required by '\{source}')" + } else { + "error: the following required argument was not provided: '\{name}'" + } TooFewValues(name, got, min) => "error: '\{name}' requires at least \{min} values but only \{got} were provided" TooManyValues(name, got, max) => diff --git a/argparse/help_render.mbt b/argparse/help_render.mbt index 4de1c581d..b2fcbef3c 100644 --- a/argparse/help_render.mbt +++ b/argparse/help_render.mbt @@ -339,7 +339,6 @@ fn arg_doc(arg : Arg) -> String { } } -///| fn has_subcommands_for_help(cmd : Command) -> Bool { if help_subcommand_enabled(cmd) { return true diff --git a/argparse/parser.mbt b/argparse/parser.mbt index 5e2da2c33..0296ee68c 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -171,9 +171,9 @@ fn parse_command( inherited_globals, command_path, ) - MissingRequired(name) => + MissingRequired(name, by) => raise arg_error_for_parse_failure( - MissingRequired(name), + MissingRequired(name, by), cmd, inherited_globals, command_path, diff --git a/argparse/parser_validate.mbt b/argparse/parser_validate.mbt index cec032afc..cd3d8e0c1 100644 --- a/argparse/parser_validate.mbt +++ b/argparse/parser_validate.mbt @@ -476,7 +476,7 @@ fn validate_command_policies( if cmd.subcommand_required && cmd.subcommands.length() > 0 && matches.parsed_subcommand is None { - raise MissingRequired("subcommand") + raise MissingRequired("subcommand", None) } } @@ -528,7 +528,7 @@ fn validate_groups( } } else if arg_seen.contains(required) { if !matches_has_value_or_flag(matches, required) { - raise MissingRequired(required) + raise MissingRequired(required, None) } } } @@ -559,7 +559,7 @@ fn validate_values( for arg in args { let present = matches_has_value_or_flag(matches, arg.name) if arg.required && !present { - raise MissingRequired(arg.name) + raise MissingRequired(arg.name, None) } guard arg.info is (Option(_) | Positional(_)) else { continue } if !present { @@ -597,7 +597,7 @@ fn validate_relationships( } for required in arg.requires { if !matches_has_value_or_flag(matches, required) { - raise MissingRequired(required) + raise MissingRequired(required, Some(arg.name)) } } for conflict in arg.conflicts_with { From 1a8c2b01ba77e27daf8b92a4ba517b714084ae98 Mon Sep 17 00:00:00 2001 From: zihang Date: Mon, 2 Mar 2026 09:54:37 +0800 Subject: [PATCH 23/40] refactor(argparse): rename arg constructors to Flag/Option/Positional --- argparse/README.mbt.md | 43 ++-- argparse/arg_action.mbt | 4 +- argparse/arg_spec.mbt | 41 +-- argparse/argparse_blackbox_test.mbt | 377 +++++++++++++--------------- argparse/argparse_test.mbt | 87 +++---- argparse/command.mbt | 22 +- argparse/help_render.mbt | 29 ++- argparse/parser.mbt | 21 +- argparse/parser_globals_merge.mbt | 18 +- argparse/parser_lookup.mbt | 12 +- argparse/parser_positionals.mbt | 10 +- argparse/parser_validate.mbt | 51 ++-- argparse/parser_values.mbt | 53 ++-- argparse/pkg.generated.mbti | 38 +-- 14 files changed, 385 insertions(+), 421 deletions(-) diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index 20fae1c27..305ca3d2a 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -13,8 +13,8 @@ small, predictable feature set. ///| test "basic option + positional success snapshot" { let matches = @argparse.parse( - Command("demo", options=[OptionArg("name", long="name")], positionals=[ - PositionalArg("target"), + Command("demo", options=[Option("name", long="name")], positionals=[ + Positional("target"), ]), argv=["--name", "alice", "file.txt"], ) @@ -28,11 +28,9 @@ test "basic option + positional success snapshot" { ///| test "basic option + positional failure snapshot" { - let cmd = @argparse.Command( - "demo", - options=[OptionArg("name", long="name")], - positionals=[PositionalArg("target")], - ) + let cmd = @argparse.Command("demo", options=[Option("name", long="name")], positionals=[ + Positional("target"), + ]) try cmd.parse(argv=["--bad"], env={}) catch { Message(msg) => inspect( @@ -66,7 +64,7 @@ states. ///| test "negatable flag success snapshot" { let cmd = @argparse.Command("demo", flags=[ - FlagArg("cache", long="cache", negatable=true), + Flag("cache", long="cache", negatable=true), ]) let parsed = cmd.parse(argv=["--no-cache"], env={}) catch { _ => panic() } @@ -81,7 +79,7 @@ test "negatable flag success snapshot" { ///| test "negatable flag failure snapshot" { let cmd = @argparse.Command("demo", flags=[ - FlagArg("cache", long="cache", negatable=true), + Flag("cache", long="cache", negatable=true), ]) try cmd.parse(argv=["--oops"], env={}) catch { Message(msg) => @@ -111,9 +109,7 @@ test "negatable flag failure snapshot" { test "global count flag success snapshot" { let cmd = @argparse.Command( "demo", - flags=[ - FlagArg("verbose", short='v', long="verbose", action=Count, global=true), - ], + flags=[Flag("verbose", short='v', action=Count, global=true)], subcommands=[Command("run")], ) @@ -126,10 +122,7 @@ test "global count flag success snapshot" { #|{ "verbose": 2 } ), ) - let child = match parsed.subcommand { - Some(("run", sub)) => sub - _ => panic() - } + guard parsed.subcommand is Some(("run", child)) else { panic() } @debug.debug_inspect( child.flag_counts, content=( @@ -142,9 +135,7 @@ test "global count flag success snapshot" { test "subcommand context failure snapshot" { let cmd = @argparse.Command( "demo", - flags=[ - FlagArg("verbose", short='v', long="verbose", action=Count, global=true), - ], + flags=[Flag("verbose", short='v', action=Count, global=true)], subcommands=[Command("run")], ) try cmd.parse(argv=["run", "--oops"], env={}) catch { @@ -176,8 +167,8 @@ Positionals are parsed in declaration order (no explicit index). ///| test "bounded non-last positional success snapshot" { let cmd = @argparse.Command("demo", positionals=[ - PositionalArg("first", num_args=ValueRange(lower=1, upper=2)), - PositionalArg("second", required=true), + Positional("first", num_args=ValueRange(lower=1, upper=2)), + Positional("second", required=true), ]) let parsed = cmd.parse(argv=["a", "b", "c"], env={}) catch { _ => panic() } @@ -192,8 +183,8 @@ test "bounded non-last positional success snapshot" { ///| test "bounded non-last positional failure snapshot" { let cmd = @argparse.Command("demo", positionals=[ - PositionalArg("first", num_args=ValueRange(lower=1, upper=2)), - PositionalArg("second", required=true), + Positional("first", num_args=ValueRange(lower=1, upper=2)), + Positional("second", required=true), ]) try cmd.parse(argv=["a", "b", "c", "d"], env={}) catch { Message(msg) => @@ -228,7 +219,7 @@ contains the full contextual help text. ///| test "root invalid option snapshot" { let cmd = @argparse.Command("demo", options=[ - OptionArg("count", long="count", about="repeat count"), + Option("count", long="count", about="repeat count"), ]) try cmd.parse(argv=["--bad"], env={}) catch { @@ -254,7 +245,7 @@ test "root invalid option snapshot" { ///| test "subcommand invalid option snapshot" { let cmd = @argparse.Command("demo", subcommands=[ - Command("echo", options=[OptionArg("times", long="times")]), + Command("echo", options=[Option("times", long="times")]), ]) try cmd.parse(argv=["echo", "--oops"], env={}) catch { @@ -284,7 +275,7 @@ test "subcommand invalid option snapshot" { ///| test "render_help remains pure" { let cmd = @argparse.Command("demo", about="demo app", options=[ - OptionArg("count", long="count"), + Option("count", long="count"), ]) let help = cmd.render_help() inspect( diff --git a/argparse/arg_action.mbt b/argparse/arg_action.mbt index 751eba4d4..2123cd5e6 100644 --- a/argparse/arg_action.mbt +++ b/argparse/arg_action.mbt @@ -14,7 +14,7 @@ ///| fn arg_min_max_for_validate(arg : Arg) -> (Int, Int?) raise ArgBuildError { - if arg.info is Positional(num_args=Some(range), ..) { + if arg.info is PositionalInfo(num_args=Some(range), ..) { if range.lower < 0 { raise Unsupported("min values must be >= 0") } @@ -38,7 +38,7 @@ fn arg_min_max_for_validate(arg : Arg) -> (Int, Int?) raise ArgBuildError { ///| fn arg_min_max(arg : Arg) -> (Int, Int?) { match arg.info { - Positional(num_args=Some(range), ..) => (range.lower, range.upper) + PositionalInfo(num_args=Some(range), ..) => (range.lower, range.upper) _ => (0, None) } } diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt index a0acb55c0..d881456fb 100644 --- a/argparse/arg_spec.mbt +++ b/argparse/arg_spec.mbt @@ -47,15 +47,20 @@ priv struct Arg { ///| priv enum ArgInfo { - Flag(short~ : Char?, long~ : String?, action~ : FlagAction, negatable~ : Bool) - Option( + FlagInfo( + short~ : Char?, + long~ : String?, + action~ : FlagAction, + negatable~ : Bool + ) + OptionInfo( short~ : Char?, long~ : String?, action~ : OptionAction, default_values~ : Array[String]?, allow_hyphen_values~ : Bool ) - Positional( + PositionalInfo( num_args~ : ValueRange?, last~ : Bool, default_values~ : Array[String]?, @@ -65,7 +70,7 @@ priv enum ArgInfo { ///| /// Declarative flag constructor wrapper. -pub struct FlagArg { +pub struct Flag { priv arg : Arg /// Create a flag argument. @@ -82,7 +87,7 @@ pub struct FlagArg { global? : Bool, negatable? : Bool, hidden? : Bool, - ) -> FlagArg + ) -> Flag } ///| @@ -97,7 +102,7 @@ pub struct FlagArg { /// `global=true` makes the flag available in subcommands. /// /// If `negatable=true`, `--no-` is accepted for long flags. -pub fn FlagArg::new( +pub fn Flag::new( name : StringView, short? : Char, long? : StringView = name, @@ -110,7 +115,7 @@ pub fn FlagArg::new( global? : Bool = false, negatable? : Bool = false, hidden? : Bool = false, -) -> FlagArg { +) -> Flag { let name = name.to_string() let long = if long == "" { None } else { Some(long.to_string()) } let about = about.map(v => v.to_string()) @@ -126,7 +131,7 @@ pub fn FlagArg::new( conflicts_with: conflicts_with.to_array(), required, // - info: Flag(short~, long~, action~, negatable~), + info: FlagInfo(short~, long~, action~, negatable~), // // option_action: OptionAction::Set, // @@ -144,7 +149,7 @@ pub fn FlagArg::new( ///| /// Declarative option constructor wrapper. -pub struct OptionArg { +pub struct Option { priv arg : Arg /// Create an option argument. @@ -162,7 +167,7 @@ pub struct OptionArg { required? : Bool, global? : Bool, hidden? : Bool, - ) -> OptionArg + ) -> Option } ///| @@ -177,7 +182,7 @@ pub struct OptionArg { /// Use `action=Append` for repeated occurrences. /// /// `global=true` makes the option available in subcommands. -pub fn OptionArg::new( +pub fn Option::new( name : StringView, short? : Char, long? : StringView = name, @@ -191,7 +196,7 @@ pub fn OptionArg::new( required? : Bool = false, global? : Bool = false, hidden? : Bool = false, -) -> OptionArg { +) -> Option { let name = name.to_string() let long = if long == "" { None } else { Some(long.to_string()) } let about = about.map(v => v.to_string()) @@ -207,7 +212,7 @@ pub fn OptionArg::new( global, hidden, // - info: Option( + info: OptionInfo( short~, long~, action~, @@ -230,7 +235,7 @@ pub fn OptionArg::new( ///| /// Declarative positional constructor wrapper. -pub struct PositionalArg { +pub struct Positional { priv arg : Arg /// Create a positional argument. @@ -247,7 +252,7 @@ pub struct PositionalArg { required? : Bool, global? : Bool, hidden? : Bool, - ) -> PositionalArg + ) -> Positional } ///| @@ -256,7 +261,7 @@ pub struct PositionalArg { /// Positional ordering is declaration order. /// /// `num_args` controls the accepted value count. -pub fn PositionalArg::new( +pub fn Positional::new( name : StringView, about? : StringView, env? : StringView, @@ -269,7 +274,7 @@ pub fn PositionalArg::new( required? : Bool = false, global? : Bool = false, hidden? : Bool = false, -) -> PositionalArg { +) -> Positional { let name = name.to_string() let about = about.map(v => v.to_string()) let env = env.map(v => v.to_string()) @@ -284,7 +289,7 @@ pub fn PositionalArg::new( global, hidden, // - info: Positional( + info: PositionalInfo( num_args~, last~, default_values=default_values.map(values => values.to_array()), diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 57ddfe19e..440d28c1e 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -26,12 +26,12 @@ test "render help snapshot with groups and hidden entries" { Command("hidden", about="hidden", hidden=true), ], flags=[ - FlagArg("fast", short='f', long="fast"), - FlagArg("slow", long="slow", hidden=true), - FlagArg("cache", long="cache", negatable=true, about="cache"), + Flag("fast", short='f', long="fast"), + Flag("slow", long="slow", hidden=true), + Flag("cache", long="cache", negatable=true, about="cache"), ], options=[ - OptionArg( + Option( "path", short='p', long="path", @@ -41,9 +41,9 @@ test "render help snapshot with groups and hidden entries" { ), ], positionals=[ - PositionalArg("target", required=true), - PositionalArg("rest", num_args=ValueRange(lower=0)), - PositionalArg("secret", hidden=true), + Positional("target", required=true), + Positional("rest", num_args=ValueRange(lower=0)), + Positional("secret", hidden=true), ], ) inspect( @@ -78,7 +78,7 @@ test "render help conversion coverage snapshot" { "shape", groups=[ArgGroup("grp", args=["f", "opt", "pos"])], flags=[ - FlagArg( + Flag( "f", short='f', about="f", @@ -90,7 +90,7 @@ test "render help conversion coverage snapshot" { ), ], options=[ - OptionArg( + Option( "opt", short='o', about="opt", @@ -104,7 +104,7 @@ test "render help conversion coverage snapshot" { ), ], positionals=[ - PositionalArg( + Positional( "pos", about="pos", env="POS_ENV", @@ -135,7 +135,7 @@ test "render help conversion coverage snapshot" { ///| test "count flags and sources with pattern matching" { let cmd = @argparse.Command("demo", flags=[ - FlagArg("verbose", short='v', long="verbose", action=Count), + Flag("verbose", short='v', long="verbose", action=Count), ]) let matches = cmd.parse(argv=["-v", "-v", "-v"], env=empty_env()) catch { _ => panic() @@ -151,13 +151,7 @@ test "global option merges parent and child values" { let cmd = @argparse.Command( "demo", options=[ - OptionArg( - "profile", - short='p', - long="profile", - action=Append, - global=true, - ), + Option("profile", short='p', long="profile", action=Append, global=true), ], subcommands=[child], ) @@ -181,8 +175,8 @@ test "global requires is validated after parent-child merge" { let cmd = @argparse.Command( "demo", options=[ - OptionArg("mode", long="mode", requires=["config"], global=true), - OptionArg("config", long="config", global=true), + Option("mode", long="mode", requires=["config"], global=true), + Option("config", long="config", global=true), ], subcommands=[Command("run")], ) @@ -206,7 +200,7 @@ test "global append keeps parent argv over child env/default" { let cmd = @argparse.Command( "demo", options=[ - OptionArg( + Option( "profile", long="profile", action=Append, @@ -238,7 +232,7 @@ test "global scalar keeps parent argv over child env/default" { let cmd = @argparse.Command( "demo", options=[ - OptionArg( + Option( "profile", long="profile", env="PROFILE", @@ -268,9 +262,7 @@ test "global count merges parent and child occurrences" { let child = @argparse.Command("run") let cmd = @argparse.Command( "demo", - flags=[ - FlagArg("verbose", short='v', long="verbose", action=Count, global=true), - ], + flags=[Flag("verbose", short='v', action=Count, global=true)], subcommands=[child], ) @@ -290,7 +282,7 @@ test "global count keeps parent argv over child env fallback" { let cmd = @argparse.Command( "demo", flags=[ - FlagArg( + Flag( "verbose", short='v', long="verbose", @@ -319,7 +311,7 @@ test "global flag keeps parent argv over child env fallback" { let child = @argparse.Command("run") let cmd = @argparse.Command( "demo", - flags=[FlagArg("verbose", long="verbose", env="VERBOSE", global=true)], + flags=[Flag("verbose", long="verbose", env="VERBOSE", global=true)], subcommands=[child], ) @@ -337,7 +329,7 @@ test "global flag keeps parent argv over child env fallback" { ///| test "subcommand cannot follow positional arguments" { - let cmd = @argparse.Command("demo", positionals=[PositionalArg("input")], subcommands=[ + let cmd = @argparse.Command("demo", positionals=[Positional("input")], subcommands=[ Command("run"), ]) try cmd.parse(argv=["raw", "run"], env=empty_env()) catch { @@ -373,7 +365,7 @@ test "global count source keeps env across subcommand merge" { let cmd = @argparse.Command( "demo", flags=[ - FlagArg( + Flag( "verbose", short='v', long="verbose", @@ -483,7 +475,7 @@ test "subcommand help includes inherited global options" { let cmd = @argparse.Command( "demo", flags=[ - FlagArg( + Flag( "verbose", short='v', long="verbose", @@ -520,7 +512,7 @@ test "subcommand help includes inherited global options" { ///| test "unknown argument suggestions are exposed" { let cmd = @argparse.Command("demo", flags=[ - FlagArg("verbose", short='v', long="verbose"), + Flag("verbose", short='v', long="verbose"), ]) try cmd.parse(argv=["--verbse"], env=empty_env()) catch { @@ -591,7 +583,7 @@ test "unknown argument suggestions are exposed" { ///| test "long and short value parsing branches" { let cmd = @argparse.Command("demo", options=[ - OptionArg("count", short='c', long="count"), + Option("count", short='c', long="count"), ]) let long_inline = cmd.parse(argv=["--count=2"], env=empty_env()) catch { @@ -653,7 +645,7 @@ test "long and short value parsing branches" { ///| test "append option action is publicly selectable" { let cmd = @argparse.Command("demo", options=[ - OptionArg("tag", long="tag", action=Append), + Option("tag", long="tag", action=Append), ]) let appended = cmd.parse(argv=["--tag", "a", "--tag", "b"], env=empty_env()) catch { _ => panic() @@ -666,8 +658,8 @@ test "append option action is publicly selectable" { test "negation parsing and invalid negation forms" { let cmd = @argparse.Command( "demo", - flags=[FlagArg("cache", long="cache", negatable=true)], - options=[OptionArg("path", long="path")], + flags=[Flag("cache", long="cache", negatable=true)], + options=[Option("path", long="path")], ) let off = cmd.parse(argv=["--no-cache"], env=empty_env()) catch { @@ -740,7 +732,7 @@ test "negation parsing and invalid negation forms" { } let count_cmd = @argparse.Command("demo", flags=[ - FlagArg("verbose", long="verbose", action=Count, negatable=true), + Flag("verbose", long="verbose", action=Count, negatable=true), ]) let reset = count_cmd.parse( argv=["--verbose", "--no-verbose"], @@ -756,7 +748,7 @@ test "negation parsing and invalid negation forms" { ///| test "positionals force mode and dash handling" { let force_cmd = @argparse.Command("demo", positionals=[ - PositionalArg( + Positional( "tail", num_args=ValueRange(lower=0), last=true, @@ -773,7 +765,7 @@ test "positionals force mode and dash handling" { } assert_true(dashed.values is { "tail": ["p", "q"], .. }) - let negative_cmd = @argparse.Command("demo", positionals=[PositionalArg("n")]) + let negative_cmd = @argparse.Command("demo", positionals=[Positional("n")]) let negative = negative_cmd.parse(argv=["-9"], env=empty_env()) catch { _ => panic() } @@ -805,11 +797,7 @@ test "positionals force mode and dash handling" { ///| test "variadic positional keeps accepting hyphen values after first token" { let cmd = @argparse.Command("demo", positionals=[ - PositionalArg( - "tail", - num_args=ValueRange(lower=0), - allow_hyphen_values=true, - ), + Positional("tail", num_args=ValueRange(lower=0), allow_hyphen_values=true), ]) let parsed = cmd.parse(argv=["a", "-b", "--mystery"], env=empty_env()) catch { _ => panic() @@ -820,8 +808,8 @@ test "variadic positional keeps accepting hyphen values after first token" { ///| test "bounded positional does not greedily consume later required values" { let cmd = @argparse.Command("demo", positionals=[ - PositionalArg("first", num_args=ValueRange(lower=1, upper=2)), - PositionalArg("second", required=true), + Positional("first", num_args=ValueRange(lower=1, upper=2)), + Positional("second", required=true), ]) let two = cmd.parse(argv=["a", "b"], env=empty_env()) catch { _ => panic() } @@ -836,8 +824,8 @@ test "bounded positional does not greedily consume later required values" { ///| test "indexed non-last positional allows explicit single num_args" { let cmd = @argparse.Command("demo", positionals=[ - PositionalArg("first", num_args=@argparse.ValueRange::single()), - PositionalArg("second", required=true), + Positional("first", num_args=@argparse.ValueRange::single()), + Positional("second", required=true), ]) let parsed = cmd.parse(argv=["a", "b"], env=empty_env()) catch { @@ -850,8 +838,8 @@ test "indexed non-last positional allows explicit single num_args" { test "empty positional value range is rejected at build time" { try @argparse.Command("demo", positionals=[ - PositionalArg("skip", num_args=ValueRange(lower=0, upper=0)), - PositionalArg("name", required=true), + Positional("skip", num_args=ValueRange(lower=0, upper=0)), + Positional("name", required=true), ]).parse(argv=["alice"], env=empty_env()) catch { Message(msg) => @@ -880,9 +868,9 @@ test "empty positional value range is rejected at build time" { ///| test "env parsing for settrue setfalse count and invalid values" { let cmd = @argparse.Command("demo", flags=[ - FlagArg("on", long="on", action=SetTrue, env="ON"), - FlagArg("off", long="off", action=SetFalse, env="OFF"), - FlagArg("v", long="v", action=Count, env="V"), + Flag("on", long="on", action=SetTrue, env="ON"), + Flag("off", long="off", action=SetFalse, env="OFF"), + Flag("v", long="v", action=Count, env="V"), ]) let parsed = cmd.parse(argv=[], env={ "ON": "true", "OFF": "true", "V": "3" }) catch { @@ -984,8 +972,8 @@ test "env parsing for settrue setfalse count and invalid values" { ///| test "defaults and value range helpers through public API" { let defaults = @argparse.Command("demo", options=[ - OptionArg("mode", long="mode", action=Append, default_values=["a", "b"]), - OptionArg("one", long="one", default_values=["x"]), + Option("mode", long="mode", action=Append, default_values=["a", "b"]), + Option("one", long="one", default_values=["x"]), ]) let by_default = defaults.parse(argv=[], env=empty_env()) catch { _ => panic() @@ -994,7 +982,7 @@ test "defaults and value range helpers through public API" { assert_true(by_default.sources is { "mode": Default, "one": Default, .. }) let upper_only = @argparse.Command("demo", options=[ - OptionArg("tag", long="tag", action=Append), + Option("tag", long="tag", action=Append), ]) let upper_parsed = upper_only.parse( argv=["--tag", "a", "--tag", "b", "--tag", "c"], @@ -1004,9 +992,7 @@ test "defaults and value range helpers through public API" { } assert_true(upper_parsed.values is { "tag": ["a", "b", "c"], .. }) - let lower_only = @argparse.Command("demo", options=[ - OptionArg("tag", long="tag"), - ]) + let lower_only = @argparse.Command("demo", options=[Option("tag", long="tag")]) let lower_absent = lower_only.parse(argv=[], env=empty_env()) catch { _ => panic() } @@ -1043,7 +1029,7 @@ test "defaults and value range helpers through public API" { ///| test "options consume exactly one value per occurrence" { - let cmd = @argparse.Command("demo", options=[OptionArg("tag", long="tag")]) + let cmd = @argparse.Command("demo", options=[Option("tag", long="tag")]) let parsed = cmd.parse(argv=["--tag", "a"], env=empty_env()) catch { _ => panic() } @@ -1073,7 +1059,7 @@ test "options consume exactly one value per occurrence" { ///| test "set options reject duplicate occurrences" { - let cmd = @argparse.Command("demo", options=[OptionArg("mode", long="mode")]) + let cmd = @argparse.Command("demo", options=[Option("mode", long="mode")]) try cmd.parse(argv=["--mode", "a", "--mode", "b"], env=empty_env()) catch { Message(msg) => inspect( @@ -1098,7 +1084,7 @@ test "set options reject duplicate occurrences" { ///| test "flag and option args require short or long names" { try - @argparse.Command("demo", options=[OptionArg("input", long="")]).parse( + @argparse.Command("demo", options=[Option("input", long="")]).parse( argv=[], env=empty_env(), ) @@ -1122,7 +1108,7 @@ test "flag and option args require short or long names" { } try - @argparse.Command("demo", flags=[FlagArg("verbose", long="")]).parse( + @argparse.Command("demo", flags=[Flag("verbose", long="")]).parse( argv=[], env=empty_env(), ) @@ -1149,7 +1135,7 @@ test "flag and option args require short or long names" { ///| test "append options collect values across repeated occurrences" { let cmd = @argparse.Command("demo", options=[ - OptionArg("arg", long="arg", action=Append), + Option("arg", long="arg", action=Append), ]) let parsed = cmd.parse(argv=["--arg", "x", "--arg", "y"], env=empty_env()) catch { _ => panic() @@ -1160,11 +1146,9 @@ test "append options collect values across repeated occurrences" { ///| test "option parsing stops at the next option token" { - let cmd = @argparse.Command( - "demo", - flags=[FlagArg("verbose", long="verbose")], - options=[OptionArg("arg", short='a', long="arg")], - ) + let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")], options=[ + Option("arg", short='a', long="arg"), + ]) let stopped = cmd.parse(argv=["--arg", "x", "--verbose"], env=empty_env()) catch { _ => panic() @@ -1217,11 +1201,9 @@ test "option parsing stops at the next option token" { ///| test "options always require a value" { - let cmd = @argparse.Command( - "demo", - flags=[FlagArg("verbose", long="verbose")], - options=[OptionArg("opt", long="opt")], - ) + let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")], options=[ + Option("opt", long="opt"), + ]) try cmd.parse(argv=["--opt", "--verbose"], env=empty_env()) catch { Message(msg) => inspect( @@ -1244,7 +1226,7 @@ test "options always require a value" { } let zero_value_required = @argparse.Command("demo", options=[ - OptionArg("opt", long="opt", required=true), + Option("opt", long="opt", required=true), ]).parse(argv=["--opt", "x"], env=empty_env()) catch { _ => panic() } @@ -1254,7 +1236,7 @@ test "options always require a value" { ///| test "option values reject hyphen tokens unless allow_hyphen_values is enabled" { let strict = @argparse.Command("demo", options=[ - OptionArg("pattern", long="pattern"), + Option("pattern", long="pattern"), ]) let mut rejected = false try strict.parse(argv=["--pattern", "-file"], env=empty_env()) catch { @@ -1281,7 +1263,7 @@ test "option values reject hyphen tokens unless allow_hyphen_values is enabled" assert_true(rejected) let permissive = @argparse.Command("demo", options=[ - OptionArg("pattern", long="pattern", allow_hyphen_values=true), + Option("pattern", long="pattern", allow_hyphen_values=true), ]) let parsed = permissive.parse(argv=["--pattern", "-file"], env=empty_env()) catch { _ => panic() @@ -1295,11 +1277,7 @@ test "option values reject hyphen tokens unless allow_hyphen_values is enabled" ///| test "default argv path is reachable" { let cmd = @argparse.Command("demo", positionals=[ - PositionalArg( - "rest", - num_args=ValueRange(lower=0), - allow_hyphen_values=true, - ), + Positional("rest", num_args=ValueRange(lower=0), allow_hyphen_values=true), ]) let _ = cmd.parse(env=empty_env()) catch { _ => panic() } } @@ -1307,7 +1285,7 @@ test "default argv path is reachable" { ///| test "validation branches exposed through parse" { try - @argparse.Command("demo", flags=[FlagArg("f", long="", action=Help)]).parse( + @argparse.Command("demo", flags=[Flag("f", long="", action=Help)]).parse( argv=[], env=empty_env(), ) @@ -1332,7 +1310,7 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", flags=[ - FlagArg("f", long="f", action=Help, negatable=true), + Flag("f", long="f", action=Help, negatable=true), ]).parse(argv=[], env=empty_env()) catch { Message(msg) => @@ -1355,9 +1333,10 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", flags=[ - FlagArg("f", long="f", action=Help, env="F"), - ]).parse(argv=[], env=empty_env()) + @argparse.Command("demo", flags=[Flag("f", long="f", action=Help, env="F")]).parse( + argv=[], + env=empty_env(), + ) catch { Message(msg) => inspect( @@ -1379,7 +1358,7 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", options=[OptionArg("x", long="x")]).parse( + @argparse.Command("demo", options=[Option("x", long="x")]).parse( argv=["--x", "a", "b"], env=empty_env(), ) @@ -1405,7 +1384,7 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", options=[ - OptionArg("x", long="x", default_values=["a", "b"]), + Option("x", long="x", default_values=["a", "b"]), ]).parse(argv=[], env=empty_env()) catch { Message(msg) => @@ -1429,7 +1408,7 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", positionals=[ - PositionalArg("x", num_args=ValueRange(lower=3, upper=2)), + Positional("x", num_args=ValueRange(lower=3, upper=2)), ]).parse(argv=[], env=empty_env()) catch { Message(msg) => @@ -1455,7 +1434,7 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", positionals=[ - PositionalArg("x", num_args=ValueRange(lower=-1, upper=2)), + Positional("x", num_args=ValueRange(lower=-1, upper=2)), ]).parse(argv=[], env=empty_env()) catch { Message(msg) => @@ -1481,7 +1460,7 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", positionals=[ - PositionalArg("x", num_args=ValueRange(lower=0, upper=-1)), + Positional("x", num_args=ValueRange(lower=0, upper=-1)), ]).parse(argv=[], env=empty_env()) catch { Message(msg) => @@ -1506,8 +1485,8 @@ test "validation branches exposed through parse" { } let positional_ok = @argparse.Command("demo", positionals=[ - PositionalArg("x", num_args=ValueRange(lower=0, upper=2)), - PositionalArg("y"), + Positional("x", num_args=ValueRange(lower=0, upper=2)), + Positional("y"), ]).parse(argv=["a"], env=empty_env()) catch { _ => panic() } @@ -1611,8 +1590,8 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", options=[ - OptionArg("x", long="x"), - OptionArg("x", long="y"), + Option("x", long="x"), + Option("x", long="y"), ]).parse(argv=[], env=empty_env()) catch { Message(msg) => @@ -1637,8 +1616,8 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", options=[ - OptionArg("x", long="same"), - OptionArg("y", long="same"), + Option("x", long="same"), + Option("y", long="same"), ]).parse(argv=[], env=empty_env()) catch { Message(msg) => @@ -1663,8 +1642,8 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", flags=[ - FlagArg("hello", long="hello", negatable=true), - FlagArg("x", long="no-hello"), + Flag("hello", long="hello", negatable=true), + Flag("x", long="no-hello"), ]).parse(argv=[], env=empty_env()) catch { Message(msg) => @@ -1689,8 +1668,8 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", options=[ - OptionArg("x", short='s'), - OptionArg("y", short='s'), + Option("x", short='s'), + Option("y", short='s'), ]).parse(argv=[], env=empty_env()) catch { Message(msg) => @@ -1714,7 +1693,7 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", flags=[FlagArg("x", long="x", requires=["x"])]).parse( + @argparse.Command("demo", flags=[Flag("x", long="x", requires=["x"])]).parse( argv=[], env=empty_env(), ) @@ -1739,9 +1718,10 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", flags=[ - FlagArg("x", long="x", conflicts_with=["x"]), - ]).parse(argv=[], env=empty_env()) + @argparse.Command("demo", flags=[Flag("x", long="x", conflicts_with=["x"])]).parse( + argv=[], + env=empty_env(), + ) catch { Message(msg) => inspect( @@ -1844,7 +1824,7 @@ test "validation branches exposed through parse" { } let custom_help = @argparse.Command("demo", flags=[ - FlagArg("custom_help", short='h', long="help", about="custom help"), + Flag("custom_help", short='h', long="help", about="custom help"), ]) let help_short = custom_help.parse(argv=["-h"], env=empty_env()) catch { _ => panic() @@ -1866,7 +1846,7 @@ test "validation branches exposed through parse" { ) let custom_version = @argparse.Command("demo", version="1.0", flags=[ - FlagArg("custom_version", short='V', long="version", about="custom version"), + Flag("custom_version", short='V', long="version", about="custom version"), ]) let version_short = custom_version.parse(argv=["-V"], env=empty_env()) catch { _ => panic() @@ -1889,7 +1869,7 @@ test "validation branches exposed through parse" { ) try - @argparse.Command("demo", flags=[FlagArg("v", long="v", action=Version)]).parse( + @argparse.Command("demo", flags=[Flag("v", long="v", action=Version)]).parse( argv=[], env=empty_env(), ) @@ -1950,7 +1930,7 @@ test "builtin and custom help/version dispatch edge paths" { } let long_help = @argparse.Command("demo", flags=[ - FlagArg("assist", long="assist", action=Help), + Flag("assist", long="assist", action=Help), ]) inspect( long_help.render_help(), @@ -1965,7 +1945,7 @@ test "builtin and custom help/version dispatch edge paths" { ) let short_help = @argparse.Command("demo", flags=[ - FlagArg("assist", short='?', action=Help), + Flag("assist", short='?', action=Help), ]) inspect( short_help.render_help(), @@ -1982,7 +1962,7 @@ test "builtin and custom help/version dispatch edge paths" { ///| test "subcommand lookup falls back to positional value" { - let cmd = @argparse.Command("demo", positionals=[PositionalArg("input")], subcommands=[ + let cmd = @argparse.Command("demo", positionals=[Positional("input")], subcommands=[ Command("run"), ]) let parsed = cmd.parse(argv=["raw"], env=empty_env()) catch { _ => panic() } @@ -2049,8 +2029,8 @@ test "group requires/conflicts can target argument names" { let requires_cmd = @argparse.Command( "demo", groups=[ArgGroup("mode", args=["fast"], requires=["config"])], - flags=[FlagArg("fast", long="fast")], - options=[OptionArg("config", long="config")], + flags=[Flag("fast", long="fast")], + options=[Option("config", long="config")], ) let ok = requires_cmd.parse( @@ -2089,8 +2069,8 @@ test "group requires/conflicts can target argument names" { let conflicts_cmd = @argparse.Command( "demo", groups=[ArgGroup("mode", args=["fast"], conflicts_with=["config"])], - flags=[FlagArg("fast", long="fast")], - options=[OptionArg("config", long="config")], + flags=[Flag("fast", long="fast")], + options=[Option("config", long="config")], ) try @@ -2126,7 +2106,7 @@ test "group requires/conflicts can target argument names" { ///| test "group without members has no parse effect" { let cmd = @argparse.Command("demo", groups=[ArgGroup("known")], flags=[ - FlagArg("x", long="x"), + Flag("x", long="x"), ]) let parsed = cmd.parse(argv=["--x"], env=empty_env()) catch { _ => panic() } assert_true(parsed.flags is { "x": true, .. }) @@ -2138,7 +2118,7 @@ test "group without members has no parse effect" { test "arg validation catches unknown requires target" { try @argparse.Command("demo", options=[ - OptionArg("mode", long="mode", requires=["missing"]), + Option("mode", long="mode", requires=["missing"]), ]).parse(argv=["--mode", "fast"], env=empty_env()) catch { Message(msg) => @@ -2165,7 +2145,7 @@ test "arg validation catches unknown requires target" { test "arg validation catches unknown conflicts_with target" { try @argparse.Command("demo", options=[ - OptionArg("mode", long="mode", conflicts_with=["missing"]), + Option("mode", long="mode", conflicts_with=["missing"]), ]).parse(argv=["--mode", "fast"], env=empty_env()) catch { Message(msg) => @@ -2193,7 +2173,7 @@ test "empty groups without presence do not fail" { let grouped_ok = @argparse.Command( "demo", groups=[ArgGroup("left", args=["l"]), ArgGroup("right", args=["r"])], - flags=[FlagArg("l", long="left"), FlagArg("r", long="right")], + flags=[Flag("l", long="left"), Flag("r", long="right")], ) let parsed = grouped_ok.parse(argv=["--left"], env=empty_env()) catch { _ => panic() @@ -2204,13 +2184,13 @@ test "empty groups without presence do not fail" { ///| test "help rendering edge paths stay stable" { let required_many = @argparse.Command("demo", positionals=[ - PositionalArg("files", required=true, num_args=ValueRange(lower=1)), + Positional("files", required=true, num_args=ValueRange(lower=1)), ]) let required_help = required_many.render_help() assert_true(required_help.has_prefix("Usage: demo ")) let short_only_builtin = @argparse.Command("demo", options=[ - OptionArg("helpopt", long="help"), + Option("helpopt", long="help"), ]) let short_only_text = short_only_builtin.render_help() assert_true(short_only_text.has_prefix("Usage: demo")) @@ -2235,7 +2215,7 @@ test "help rendering edge paths stay stable" { } let long_only_builtin = @argparse.Command("demo", flags=[ - FlagArg("custom_h", short='h'), + Flag("custom_h", short='h'), ]) let long_only_text = long_only_builtin.render_help() assert_true(long_only_text.has_prefix("Usage: demo")) @@ -2253,7 +2233,7 @@ test "help rendering edge paths stay stable" { assert_true(empty_options_help.has_prefix("Usage: demo")) let implicit_group = @argparse.Command("demo", positionals=[ - PositionalArg("item"), + Positional("item"), ]) let implicit_group_help = implicit_group.render_help() assert_true(implicit_group_help.has_prefix("Usage: demo [item]")) @@ -2267,7 +2247,7 @@ test "help rendering edge paths stay stable" { ///| test "unified error message formatting remains stable" { - let cmd = @argparse.Command("demo", options=[OptionArg("tag", long="tag")]) + let cmd = @argparse.Command("demo", options=[Option("tag", long="tag")]) try cmd.parse(argv=["--oops"], env=empty_env()) catch { Message(msg) => @@ -2312,15 +2292,16 @@ test "unified error message formatting remains stable" { ///| test "options require one value per occurrence" { - let with_value = @argparse.Command("demo", options=[ - OptionArg("tag", long="tag"), - ]).parse(argv=["--tag", "x"], env=empty_env()) catch { + let with_value = @argparse.Command("demo", options=[Option("tag", long="tag")]).parse( + argv=["--tag", "x"], + env=empty_env(), + ) catch { _ => panic() } assert_true(with_value.values is { "tag": ["x"], .. }) try - @argparse.Command("demo", options=[OptionArg("tag", long="tag")]).parse( + @argparse.Command("demo", options=[Option("tag", long="tag")]).parse( argv=["--tag"], env=empty_env(), ) @@ -2347,8 +2328,8 @@ test "options require one value per occurrence" { ///| test "short options require one value before next option token" { - let cmd = @argparse.Command("demo", flags=[FlagArg("verbose", short='v')], options=[ - OptionArg("x", short='x'), + let cmd = @argparse.Command("demo", flags=[Flag("verbose", short='v')], options=[ + Option("x", short='x'), ]) let ok = cmd.parse(argv=["-x", "a", "-v"], env=empty_env()) catch { _ => panic() @@ -2381,8 +2362,8 @@ test "short options require one value before next option token" { ///| test "version action dispatches on custom long and short flags" { let cmd = @argparse.Command("demo", version="2.0.0", flags=[ - FlagArg("show_long", long="show-version", action=Version), - FlagArg("show_short", short='S', action=Version), + Flag("show_long", long="show-version", action=Version), + Flag("show_short", short='S', action=Version), ]) inspect( @@ -2428,7 +2409,7 @@ test "global version action keeps parent version text in subcommand context" { "demo", version="1.0.0", flags=[ - FlagArg( + Flag( "show_version", short='S', long="show-version", @@ -2488,7 +2469,7 @@ test "global version action keeps parent version text in subcommand context" { ///| test "required and env-fed ranged values validate after parsing" { let required_cmd = @argparse.Command("demo", options=[ - OptionArg("input", long="input", required=true), + Option("input", long="input", required=true), ]) try required_cmd.parse(argv=[], env=empty_env()) catch { Message(msg) => @@ -2511,7 +2492,7 @@ test "required and env-fed ranged values validate after parsing" { } let env_min_cmd = @argparse.Command("demo", options=[ - OptionArg("pair", long="pair", env="PAIR"), + Option("pair", long="pair", env="PAIR"), ]) let env_value = env_min_cmd.parse(argv=[], env={ "PAIR": "one" }) catch { _ => panic() @@ -2523,9 +2504,9 @@ test "required and env-fed ranged values validate after parsing" { ///| test "positionals honor explicit index sorting with last ranged positional" { let cmd = @argparse.Command("demo", positionals=[ - PositionalArg("late", num_args=ValueRange(lower=2, upper=2)), - PositionalArg("first"), - PositionalArg("mid"), + Positional("late", num_args=ValueRange(lower=2, upper=2)), + Positional("first"), + Positional("mid"), ]) let parsed = cmd.parse(argv=["a", "b", "c", "d"], env=empty_env()) catch { @@ -2539,8 +2520,8 @@ test "positionals honor explicit index sorting with last ranged positional" { ///| test "mixed indexed and unindexed positionals keep inferred order" { let cmd = @argparse.Command("demo", positionals=[ - PositionalArg("first"), - PositionalArg("second"), + Positional("first"), + Positional("second"), ]) let parsed = cmd.parse(argv=["a", "b"], env=empty_env()) catch { @@ -2551,7 +2532,7 @@ test "mixed indexed and unindexed positionals keep inferred order" { ///| test "single positional parses without explicit index metadata" { - let parsed = @argparse.Command("demo", positionals=[PositionalArg("late")]).parse( + let parsed = @argparse.Command("demo", positionals=[Positional("late")]).parse( argv=["x"], env=empty_env(), ) catch { @@ -2563,7 +2544,7 @@ test "single positional parses without explicit index metadata" { ///| test "positional num_args lower bound rejects missing argv values" { let cmd = @argparse.Command("demo", positionals=[ - PositionalArg("first", num_args=ValueRange(lower=2, upper=3)), + Positional("first", num_args=ValueRange(lower=2, upper=3)), ]) try cmd.parse(argv=[], env=empty_env()) catch { @@ -2592,8 +2573,8 @@ test "positional num_args lower bound rejects missing argv values" { ///| test "positional max clamp leaves trailing value for next positional" { let cmd = @argparse.Command("demo", positionals=[ - PositionalArg("items", num_args=ValueRange(lower=0, upper=2)), - PositionalArg("tail"), + Positional("items", num_args=ValueRange(lower=0, upper=2)), + Positional("tail"), ]) let parsed = cmd.parse(argv=["a", "b", "c"], env=empty_env()) catch { @@ -2607,11 +2588,11 @@ test "options with allow_hyphen_values accept option-like single values" { let cmd = @argparse.Command( "demo", flags=[ - FlagArg("verbose", long="verbose"), - FlagArg("cache", long="cache", negatable=true), - FlagArg("quiet", short='q'), + Flag("verbose", long="verbose"), + Flag("cache", long="cache", negatable=true), + Flag("quiet", short='q'), ], - options=[OptionArg("arg", long="arg", allow_hyphen_values=true)], + options=[Option("arg", long="arg", allow_hyphen_values=true)], ) let known_long = cmd.parse(argv=["--arg", "--verbose"], env=empty_env()) catch { @@ -2642,13 +2623,9 @@ test "options with allow_hyphen_values accept option-like single values" { let cmd_with_rest = @argparse.Command( "demo", - options=[OptionArg("arg", long="arg", allow_hyphen_values=true)], + options=[Option("arg", long="arg", allow_hyphen_values=true)], positionals=[ - PositionalArg( - "rest", - num_args=ValueRange(lower=0), - allow_hyphen_values=true, - ), + Positional("rest", num_args=ValueRange(lower=0), allow_hyphen_values=true), ], ) let sentinel_stop = cmd_with_rest.parse( @@ -2662,11 +2639,9 @@ test "options with allow_hyphen_values accept option-like single values" { ///| test "single-value options avoid consuming additional option values" { - let cmd = @argparse.Command( - "demo", - flags=[FlagArg("verbose", long="verbose")], - options=[OptionArg("one", long="one")], - ) + let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")], options=[ + Option("one", long="one"), + ]) let parsed = cmd.parse(argv=["--one", "x", "--verbose"], env=empty_env()) catch { _ => panic() @@ -2677,11 +2652,9 @@ test "single-value options avoid consuming additional option values" { ///| test "missing option values are reported when next token is another option" { - let cmd = @argparse.Command( - "demo", - flags=[FlagArg("verbose", long="verbose")], - options=[OptionArg("arg", long="arg")], - ) + let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")], options=[ + Option("arg", long="arg"), + ]) let ok = cmd.parse(argv=["--arg", "x", "--verbose"], env=empty_env()) catch { _ => panic() @@ -2713,7 +2686,7 @@ test "missing option values are reported when next token is another option" { ///| test "short-only set options use short label in duplicate errors" { - let cmd = @argparse.Command("demo", options=[OptionArg("mode", short='m')]) + let cmd = @argparse.Command("demo", options=[Option("mode", short='m')]) try cmd.parse(argv=["-m", "a", "-m", "b"], env=empty_env()) catch { Message(msg) => inspect( @@ -2738,7 +2711,7 @@ test "short-only set options use short label in duplicate errors" { ///| test "unknown short suggestion can be absent" { let cmd = @argparse.Command("demo", disable_help_flag=true, options=[ - OptionArg("name", long="name"), + Option("name", long="name"), ]) try cmd.parse(argv=["-x"], env=empty_env()) catch { @@ -2764,7 +2737,7 @@ test "unknown short suggestion can be absent" { ///| test "setfalse flags apply false when present" { let cmd = @argparse.Command("demo", flags=[ - FlagArg("failfast", long="failfast", action=SetFalse), + Flag("failfast", long="failfast", action=SetFalse), ]) let parsed = cmd.parse(argv=["--failfast"], env=empty_env()) catch { _ => panic() @@ -2775,8 +2748,8 @@ test "setfalse flags apply false when present" { ///| test "allow_hyphen positional treats unknown long token as value" { - let cmd = @argparse.Command("demo", flags=[FlagArg("known", long="known")], positionals=[ - PositionalArg("input", allow_hyphen_values=true), + let cmd = @argparse.Command("demo", flags=[Flag("known", long="known")], positionals=[ + Positional("input", allow_hyphen_values=true), ]) let parsed = cmd.parse(argv=["--mystery"], env=empty_env()) catch { _ => panic() @@ -2789,8 +2762,8 @@ test "global value from child default is merged back to parent" { let cmd = @argparse.Command( "demo", options=[ - OptionArg("mode", long="mode", default_values=["safe"], global=true), - OptionArg("unused", long="unused", global=true), + Option("mode", long="mode", default_values=["safe"], global=true), + Option("unused", long="unused", global=true), ], subcommands=[Command("run")], ) @@ -2809,11 +2782,9 @@ test "global value from child default is merged back to parent" { test "child global arg with inherited global name updates parent global" { let cmd = @argparse.Command( "demo", - options=[ - OptionArg("mode", long="mode", default_values=["safe"], global=true), - ], + options=[Option("mode", long="mode", default_values=["safe"], global=true)], subcommands=[ - Command("run", options=[OptionArg("mode", long="mode", global=true)]), + Command("run", options=[Option("mode", long="mode", global=true)]), ], ) @@ -2835,7 +2806,7 @@ test "child local arg shadowing inherited global is rejected at build time" { @argparse.Command( "demo", options=[ - OptionArg( + Option( "mode", long="mode", env="MODE", @@ -2843,7 +2814,7 @@ test "child local arg shadowing inherited global is rejected at build time" { global=true, ), ], - subcommands=[Command("run", options=[OptionArg("mode", long="mode")])], + subcommands=[Command("run", options=[Option("mode", long="mode")])], ).parse(argv=["run"], env=empty_env()) catch { Message(msg) => @@ -2874,9 +2845,7 @@ test "child local arg shadowing inherited global is rejected at build time" { test "global append env value from child is merged back to parent" { let cmd = @argparse.Command( "demo", - options=[ - OptionArg("tag", long="tag", action=Append, env="TAG", global=true), - ], + options=[Option("tag", long="tag", action=Append, env="TAG", global=true)], subcommands=[Command("run")], ) @@ -2896,7 +2865,7 @@ test "global append env value from child is merged back to parent" { test "global flag set in child argv is merged back to parent" { let cmd = @argparse.Command( "demo", - flags=[FlagArg("verbose", long="verbose", global=true)], + flags=[Flag("verbose", long="verbose", global=true)], subcommands=[Command("run")], ) @@ -2917,13 +2886,7 @@ test "global count negation after subcommand resets merged state" { let cmd = @argparse.Command( "demo", flags=[ - FlagArg( - "verbose", - long="verbose", - action=Count, - negatable=true, - global=true, - ), + Flag("verbose", long="verbose", action=Count, negatable=true, global=true), ], subcommands=[Command("run")], ) @@ -2949,7 +2912,7 @@ test "global count negation after subcommand resets merged state" { test "global set option rejects duplicate occurrences across subcommands" { let cmd = @argparse.Command( "demo", - options=[OptionArg("mode", long="mode", global=true)], + options=[Option("mode", long="mode", global=true)], subcommands=[Command("run")], ) try @@ -2984,9 +2947,9 @@ test "global override with incompatible inherited type is rejected" { try @argparse.Command( "demo", - options=[OptionArg("mode", long="mode", required=true, global=true)], + options=[Option("mode", long="mode", required=true, global=true)], subcommands=[ - Command("run", flags=[FlagArg("mode", long="mode", global=true)]), + Command("run", flags=[Flag("mode", long="mode", global=true)]), ], ).parse(argv=["run", "--mode"], env=empty_env()) catch { @@ -3019,8 +2982,8 @@ test "child local long alias collision with inherited global is rejected" { try @argparse.Command( "demo", - flags=[FlagArg("verbose", long="verbose", global=true)], - subcommands=[Command("run", options=[OptionArg("local", long="verbose")])], + flags=[Flag("verbose", long="verbose", global=true)], + subcommands=[Command("run", options=[Option("local", long="verbose")])], ).parse(argv=["run", "--verbose"], env=empty_env()) catch { Message(msg) => @@ -3050,11 +3013,9 @@ test "child local long alias collision with inherited global is rejected" { ///| test "child local short alias collision with inherited global is rejected" { try - @argparse.Command( - "demo", - flags=[FlagArg("verbose", short='v', global=true)], - subcommands=[Command("run", options=[OptionArg("local", short='v')])], - ).parse(argv=["run", "-v"], env=empty_env()) + @argparse.Command("demo", flags=[Flag("verbose", short='v', global=true)], subcommands=[ + Command("run", options=[Option("local", short='v')]), + ]).parse(argv=["run", "-v"], env=empty_env()) catch { Message(msg) => inspect( @@ -3086,7 +3047,7 @@ test "nested subcommands inherit finalized globals from ancestors" { let mid = @argparse.Command("mid", subcommands=[leaf]) let cmd = @argparse.Command( "demo", - flags=[FlagArg("verbose", long="verbose", global=true)], + flags=[Flag("verbose", long="verbose", global=true)], subcommands=[mid], ) @@ -3105,14 +3066,14 @@ test "nested subcommands inherit finalized globals from ancestors" { ///| test "non-bmp short option token does not panic" { - let cmd = @argparse.Command("demo", flags=[FlagArg("party", short='🎉')]) + let cmd = @argparse.Command("demo", flags=[Flag("party", short='🎉')]) let parsed = cmd.parse(argv=["-🎉"], env=empty_env()) catch { _ => panic() } assert_true(parsed.flags is { "party": true, .. }) } ///| test "non-bmp hyphen token reports unknown argument without panic" { - let cmd = @argparse.Command("demo", positionals=[PositionalArg("value")]) + let cmd = @argparse.Command("demo", positionals=[Positional("value")]) try cmd.parse(argv=["-🎉"], env=empty_env()) catch { Message(msg) => inspect( @@ -3139,7 +3100,7 @@ test "non-bmp hyphen token reports unknown argument without panic" { ///| test "option env values remain string values instead of flags" { let cmd = @argparse.Command("demo", options=[ - OptionArg("mode", long="mode", env="MODE"), + Option("mode", long="mode", env="MODE"), ]) let parsed = cmd.parse(argv=[], env={ "MODE": "fast" }) catch { _ => panic() } assert_true(parsed.values is { "mode": ["fast"], .. }) @@ -3152,12 +3113,12 @@ test "nested global override deduplicates count merge by name" { let leaf = @argparse.Command("leaf") let mid = @argparse.Command( "mid", - flags=[FlagArg("verbose", long="verbose", action=Count, global=true)], + flags=[Flag("verbose", long="verbose", action=Count, global=true)], subcommands=[leaf], ) let root = @argparse.Command( "root", - flags=[FlagArg("verbose", long="verbose", action=Count, global=true)], + flags=[Flag("verbose", long="verbose", action=Count, global=true)], subcommands=[mid], ) @@ -3178,12 +3139,12 @@ test "nested global override keeps single set value without false duplicate erro let leaf = @argparse.Command("leaf") let mid = @argparse.Command( "mid", - options=[OptionArg("mode", long="mode", global=true)], + options=[Option("mode", long="mode", global=true)], subcommands=[leaf], ) let root = @argparse.Command( "root", - options=[OptionArg("mode", long="mode", global=true)], + options=[Option("mode", long="mode", global=true)], subcommands=[mid], ) @@ -3207,10 +3168,10 @@ test "global override with different negatable setting is rejected" { try @argparse.Command( "demo", - flags=[FlagArg("verbose", long="verbose", negatable=true, global=true)], + flags=[Flag("verbose", long="verbose", negatable=true, global=true)], subcommands=[ Command("run", flags=[ - FlagArg("verbose", long="verbose", negatable=false, global=true), + Flag("verbose", long="verbose", negatable=false, global=true), ]), ], ).parse(argv=["run"], env=empty_env()) diff --git a/argparse/argparse_test.mbt b/argparse/argparse_test.mbt index 20af392c6..0802e9ff1 100644 --- a/argparse/argparse_test.mbt +++ b/argparse/argparse_test.mbt @@ -21,9 +21,9 @@ fn empty_env() -> Map[String, String] { test "declarative parse basics" { let cmd = @argparse.Command( "demo", - flags=[FlagArg("verbose", short='v', long="verbose")], - options=[OptionArg("count", long="count", env="COUNT")], - positionals=[PositionalArg("name")], + flags=[Flag("verbose", short='v', long="verbose")], + options=[Option("count", long="count", env="COUNT")], + positionals=[Positional("name")], ) let matches = cmd.parse(argv=["-v", "--count", "3", "alice"], env=empty_env()) catch { _ => panic() @@ -37,8 +37,8 @@ test "declarative parse basics" { ///| test "long defaults to name when omitted" { - let cmd = @argparse.Command("demo", flags=[FlagArg("verbose")], options=[ - OptionArg("count"), + let cmd = @argparse.Command("demo", flags=[Flag("verbose")], options=[ + Option("count"), ]) let matches = cmd.parse(argv=["--verbose", "--count", "3"], env=empty_env()) catch { _ => panic() @@ -51,8 +51,8 @@ test "long defaults to name when omitted" { test "long empty string disables long alias" { let cmd = @argparse.Command( "demo", - flags=[FlagArg("verbose", short='v', long="")], - options=[OptionArg("count", short='c', long="")], + flags=[Flag("verbose", short='v', long="")], + options=[Option("count", short='c', long="")], ) let matches = cmd.parse(argv=["-v", "-c", "3"], env=empty_env()) catch { @@ -105,8 +105,8 @@ test "long empty string disables long alias" { ///| test "declaration order controls positional parsing" { let cmd = @argparse.Command("demo", positionals=[ - PositionalArg("first"), - PositionalArg("second"), + Positional("first"), + Positional("second"), ]) let parsed = cmd.parse(argv=["a", "b"], env=empty_env()) catch { @@ -118,8 +118,8 @@ test "declaration order controls positional parsing" { ///| test "bounded non-last positional remains supported" { let cmd = @argparse.Command("demo", positionals=[ - PositionalArg("first", num_args=ValueRange(lower=1, upper=2)), - PositionalArg("second", required=true), + Positional("first", num_args=ValueRange(lower=1, upper=2)), + Positional("second", required=true), ]) let two = cmd.parse(argv=["a", "b"], env=empty_env()) catch { _ => panic() } @@ -134,7 +134,7 @@ test "bounded non-last positional remains supported" { ///| test "negatable flag preserves false state" { let cmd = @argparse.Command("demo", flags=[ - FlagArg("cache", long="cache", negatable=true), + Flag("cache", long="cache", negatable=true), ]) let no_cache = cmd.parse(argv=["--no-cache"], env=empty_env()) catch { @@ -147,7 +147,7 @@ test "negatable flag preserves false state" { ///| test "parse failure message contains error and contextual help" { let cmd = @argparse.Command("demo", options=[ - OptionArg("count", long="count", about="repeat count"), + Option("count", long="count", about="repeat count"), ]) try cmd.parse(argv=["--bad"], env=empty_env()) catch { @@ -173,7 +173,7 @@ test "parse failure message contains error and contextual help" { ///| test "subcommand parse errors include subcommand help" { let cmd = @argparse.Command("demo", subcommands=[ - Command("echo", options=[OptionArg("times", long="times")]), + Command("echo", options=[Option("times", long="times")]), ]) try cmd.parse(argv=["echo", "--bad"], env=empty_env()) catch { @@ -199,7 +199,7 @@ test "subcommand parse errors include subcommand help" { ///| test "build errors are surfaced as ArgError message with help" { let cmd = @argparse.Command("demo", flags=[ - FlagArg("fast", long="fast", requires=["missing"]), + Flag("fast", long="fast", requires=["missing"]), ]) try cmd.parse(argv=[], env=empty_env()) catch { @@ -224,7 +224,7 @@ test "build errors are surfaced as ArgError message with help" { ///| test "unknown argument keeps suggestion in final message" { - let cmd = @argparse.Command("demo", flags=[FlagArg("verbose", long="verbose")]) + let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")]) try cmd.parse(argv=["--verbse"], env=empty_env()) catch { Message(msg) => @@ -253,9 +253,9 @@ test "render_help remains available for pure formatting" { let cmd = @argparse.Command( "demo", about="Demo command", - flags=[FlagArg("verbose", short='v', long="verbose")], - options=[OptionArg("count", long="count")], - positionals=[PositionalArg("name")], + flags=[Flag("verbose", short='v', long="verbose")], + options=[Option("count", long="count")], + positionals=[Positional("name")], subcommands=[Command("echo")], ) @@ -308,11 +308,9 @@ test "display help and version" { ///| test "parse error show is readable" { - let cmd = @argparse.Command( - "demo", - flags=[FlagArg("verbose", long="verbose")], - positionals=[PositionalArg("name")], - ) + let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")], positionals=[ + Positional("name"), + ]) try cmd.parse(argv=["--verbse"], env=empty_env()) catch { Message(msg) => @@ -366,8 +364,8 @@ test "parse error show is readable" { ///| test "relationships and num args" { let requires_cmd = @argparse.Command("demo", options=[ - OptionArg("mode", long="mode", requires=["config"]), - OptionArg("config", long="config"), + Option("mode", long="mode", requires=["config"]), + Option("config", long="config"), ]) try requires_cmd.parse(argv=["--mode", "fast"], env=empty_env()) catch { @@ -384,7 +382,6 @@ test "relationships and num args" { #| --mode #| --config #| - ), ) _ => panic() @@ -393,7 +390,7 @@ test "relationships and num args" { } let appended = @argparse.Command("demo", options=[ - OptionArg("tag", long="tag", action=Append), + Option("tag", long="tag", action=Append), ]).parse(argv=["--tag", "a", "--tag", "b", "--tag", "c"], env=empty_env()) catch { _ => panic() } @@ -407,7 +404,7 @@ test "arg groups required and multiple" { groups=[ ArgGroup("mode", required=true, multiple=false, args=["fast", "slow"]), ], - flags=[FlagArg("fast", long="fast"), FlagArg("slow", long="slow")], + flags=[Flag("fast", long="fast"), Flag("slow", long="slow")], ) try cmd.parse(argv=[], env=empty_env()) catch { @@ -467,7 +464,7 @@ test "arg groups requires and conflicts" { ArgGroup("mode", args=["fast"], requires=["output"]), ArgGroup("output", args=["json"]), ], - flags=[FlagArg("fast", long="fast"), FlagArg("json", long="json")], + flags=[Flag("fast", long="fast"), Flag("json", long="json")], ) try requires_cmd.parse(argv=["--fast"], env=empty_env()) catch { @@ -501,7 +498,7 @@ test "arg groups requires and conflicts" { ArgGroup("mode", args=["fast"], conflicts_with=["output"]), ArgGroup("output", args=["json"]), ], - flags=[FlagArg("fast", long="fast"), FlagArg("json", long="json")], + flags=[Flag("fast", long="fast"), Flag("json", long="json")], ) try conflict_cmd.parse(argv=["--fast", "--json"], env=empty_env()) catch { @@ -532,7 +529,7 @@ test "arg groups requires and conflicts" { ///| test "subcommand parsing" { - let echo = @argparse.Command("echo", positionals=[PositionalArg("msg")]) + let echo = @argparse.Command("echo", positionals=[Positional("msg")]) let root = @argparse.Command("root", subcommands=[echo]) let matches = root.parse(argv=["echo", "hi"], env=empty_env()) catch { @@ -550,14 +547,12 @@ test "full help snapshot" { "demo", about="Demo command", flags=[ - FlagArg("verbose", short='v', long="verbose", about="Enable verbose mode"), + Flag("verbose", short='v', long="verbose", about="Enable verbose mode"), ], options=[ - OptionArg("count", long="count", about="Repeat count", default_values=[ - "1", - ]), + Option("count", long="count", about="Repeat count", default_values=["1"]), ], - positionals=[PositionalArg("name", about="Target name")], + positionals=[Positional("name", about="Target name")], subcommands=[Command("echo", about="Echo a message")], ) inspect( @@ -586,7 +581,7 @@ test "full help snapshot" { ///| test "value source precedence argv env default" { let cmd = @argparse.Command("demo", options=[ - OptionArg("level", long="level", env="LEVEL", default_values=["1"]), + Option("level", long="level", env="LEVEL", default_values=["1"]), ]) let from_default = cmd.parse(argv=[], env=empty_env()) catch { _ => panic() } @@ -607,7 +602,7 @@ test "value source precedence argv env default" { ///| test "omitted env does not read process environment by default" { let cmd = @argparse.Command("demo", options=[ - OptionArg("count", long="count", env="COUNT"), + Option("count", long="count", env="COUNT"), ]) let matches = cmd.parse(argv=[]) catch { _ => panic() } assert_true(matches.values is { "count"? : None, .. }) @@ -620,8 +615,8 @@ test "options and multiple values" { let cmd = @argparse.Command( "demo", options=[ - OptionArg("count", short='c', long="count"), - OptionArg("tag", long="tag", action=Append), + Option("count", short='c', long="count"), + Option("tag", long="tag", action=Append), ], subcommands=[serve], ) @@ -650,10 +645,10 @@ test "options and multiple values" { ///| test "negatable and conflicts" { let cmd = @argparse.Command("demo", flags=[ - FlagArg("cache", long="cache", negatable=true), - FlagArg("failfast", long="failfast", action=SetFalse, negatable=true), - FlagArg("verbose", long="verbose", conflicts_with=["quiet"]), - FlagArg("quiet", long="quiet"), + Flag("cache", long="cache", negatable=true), + Flag("failfast", long="failfast", action=SetFalse, negatable=true), + Flag("verbose", long="verbose", conflicts_with=["quiet"]), + Flag("quiet", long="quiet"), ]) let no_cache = cmd.parse(argv=["--no-cache"], env=empty_env()) catch { @@ -693,7 +688,7 @@ test "negatable and conflicts" { ///| test "flag does not accept inline value" { - let cmd = @argparse.Command("demo", flags=[FlagArg("verbose", long="verbose")]) + let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")]) try cmd.parse(argv=["--verbose=true"], env=empty_env()) catch { Message(msg) => inspect( diff --git a/argparse/command.mbt b/argparse/command.mbt index b36664865..36e6756c7 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -32,9 +32,9 @@ pub struct Command { /// Create a declarative command specification. fn new( name : StringView, - flags? : ArrayView[FlagArg], - options? : ArrayView[OptionArg], - positionals? : ArrayView[PositionalArg], + flags? : ArrayView[Flag], + options? : ArrayView[Option], + positionals? : ArrayView[Positional], subcommands? : ArrayView[Command], about? : StringView, version? : StringView, @@ -57,9 +57,9 @@ pub struct Command { /// - Built-in `--help`/`--version` behavior can be disabled with the flags below. pub fn Command::new( name : StringView, - flags? : ArrayView[FlagArg] = [], - options? : ArrayView[OptionArg] = [], - positionals? : ArrayView[PositionalArg] = [], + flags? : ArrayView[Flag] = [], + options? : ArrayView[Option] = [], + positionals? : ArrayView[Positional] = [], subcommands? : ArrayView[Command] = [], about? : StringView, version? : StringView, @@ -156,7 +156,7 @@ fn build_matches( } if source is Some(source) { sources[name] = source - if spec.info is Flag(action~, ..) { + if spec.info is FlagInfo(action~, ..) { if action is Count { flags[name] = count > 0 } else { @@ -168,7 +168,7 @@ fn build_matches( let child_globals = inherited_globals + cmd.args.filter(arg => { arg.global && - arg.info is (Option(long~, short~, ..) | Flag(long~, short~, ..)) && + arg.info is (OptionInfo(long~, short~, ..) | FlagInfo(long~, short~, ..)) && (long is Some(_) || short is Some(_)) }) @@ -222,9 +222,9 @@ fn find_decl_subcommand(subs : Array[Command], name : String) -> Command? { ///| fn collect_args( - flags : ArrayView[FlagArg], - options : ArrayView[OptionArg], - positionals : ArrayView[PositionalArg], + flags : ArrayView[Flag], + options : ArrayView[Option], + positionals : ArrayView[Positional], ) -> (Array[Arg], ArgBuildError?) { let args : Array[Arg] = [] for flag in flags { diff --git a/argparse/help_render.mbt b/argparse/help_render.mbt index b2fcbef3c..987cba0ac 100644 --- a/argparse/help_render.mbt +++ b/argparse/help_render.mbt @@ -75,7 +75,7 @@ fn has_options(cmd : Command) -> Bool { if arg.hidden { continue } - if arg.info is (Option(long~, short~, ..) | Flag(long~, short~, ..)) && + if arg.info is (OptionInfo(long~, short~, ..) | FlagInfo(long~, short~, ..)) && (long is Some(_) || short is Some(_)) { return true } @@ -131,14 +131,15 @@ fn option_entries(cmd : Command) -> Array[String] { display.push((label, "Show version information.")) } for arg in args { - guard arg.info is (Option(long~, short~, ..) | Flag(long~, short~, ..)) && + guard arg.info + is (OptionInfo(long~, short~, ..) | FlagInfo(long~, short~, ..)) && (long is Some(_) || short is Some(_)) else { continue } if arg.hidden { continue } - let name = if arg.info is Option(_) { + let name = if arg.info is OptionInfo(_) { "\{arg_display(arg)} <\{arg.name}>" } else { arg_display(arg) @@ -151,7 +152,7 @@ fn option_entries(cmd : Command) -> Array[String] { ///| fn has_long_option(args : Array[Arg], name : String) -> Bool { for arg in args { - if arg.info is (Option(long~, ..) | Flag(long~, ..)) && + if arg.info is (OptionInfo(long~, ..) | FlagInfo(long~, ..)) && long is Some(long) && long == name { return true @@ -163,7 +164,7 @@ fn has_long_option(args : Array[Arg], name : String) -> Bool { ///| fn has_short_option(args : Array[Arg], value : Char) -> Bool { for arg in args { - if arg.info is (Option(short~, ..) | Flag(short~, ..)) && + if arg.info is (OptionInfo(short~, ..) | FlagInfo(short~, ..)) && short is Some(short) && short == value { return true @@ -279,15 +280,15 @@ fn format_entries(display : Array[(String, String)]) -> Array[String] { fn arg_display(arg : Arg) -> String { let parts = Array::new(capacity=2) let (short, long) = match arg.info { - Option(short~, long~, ..) => (short, long) - Flag(short~, long~, ..) => (short, long) - Positional(_) => (None, None) + OptionInfo(short~, long~, ..) => (short, long) + FlagInfo(short~, long~, ..) => (short, long) + PositionalInfo(_) => (None, None) } if short is Some(short) { parts.push("-\{short}") } if long is Some(long) { - if arg.info is Flag(negatable=true, ..) { + if arg.info is FlagInfo(negatable=true, ..) { parts.push("--[no-]\{long}") } else { parts.push("--\{long}") @@ -316,7 +317,8 @@ fn arg_doc(arg : Arg) -> String { Some(env_name) => notes.push("env: \{env_name}") None => () } - if arg.info is (Option(default_values~, ..) | Positional(default_values~, ..)) && + if arg.info + is (OptionInfo(default_values~, ..) | PositionalInfo(default_values~, ..)) && default_values is Some(values) { if values.length() > 1 { let defaults = values.join(", ") @@ -339,6 +341,7 @@ fn arg_doc(arg : Arg) -> String { } } +///| fn has_subcommands_for_help(cmd : Command) -> Bool { if help_subcommand_enabled(cmd) { return true @@ -396,8 +399,8 @@ fn group_members(cmd : Command, group : ArgGroup) -> String { fn group_member_display(arg : Arg) -> String { let base = arg_display(arg) match arg.info { - Flag(_) => base - Option(_) => "\{base} <\{arg.name}>" - Positional(_) => base + FlagInfo(_) => base + OptionInfo(_) => "\{base} <\{arg.name}>" + PositionalInfo(_) => base } } diff --git a/argparse/parser.mbt b/argparse/parser.mbt index 0296ee68c..6d59117ed 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -257,7 +257,7 @@ fn parse_command_impl( let child_version_long = inherited_version_long.copy() let child_version_short = inherited_version_short.copy() for global in globals_here { - if global.info is Flag(long~, short~, action=Version, ..) { + if global.info is FlagInfo(long~, short~, action=Version, ..) { if long is Some(name) { child_version_long[name] = command_version(cmd) } @@ -341,7 +341,7 @@ fn parse_command_impl( // Support `--no-` when the underlying flag is marked `negatable`. if name is [.. "no-", .. target] && long_index.get(target.to_string()) - is Some({ info: Flag(negatable=true, action~, ..), .. } as spec) { + is Some({ info: FlagInfo(negatable=true, action~, ..), .. } as spec) { if inline is Some(_) { raise ArgParseError::InvalidArgument(arg) } @@ -358,7 +358,7 @@ fn parse_command_impl( raise_unknown_long(name, long_index) } Some(spec) => - if spec.info is (Option(_) | Positional(_)) { + if spec.info is (OptionInfo(_) | PositionalInfo(_)) { check_duplicate_set_occurrence(matches, spec) if inline is Some(v) { assign_value(matches, spec, v, Argv) @@ -382,9 +382,9 @@ fn parse_command_impl( raise ArgParseError::InvalidArgument(arg) } match spec.info { - Flag(action=Help, ..) => + FlagInfo(action=Help, ..) => raise_context_help(cmd, inherited_globals, command_path) - Flag(action=Version, ..) => + FlagInfo(action=Version, ..) => raise_version( version_text_for_long_action( cmd, name, inherited_version_long, @@ -412,7 +412,7 @@ fn parse_command_impl( Some(v) => v None => raise_unknown_short(short, short_index) } - if spec.info is (Option(_) | Positional(_)) { + if spec.info is (OptionInfo(_) | PositionalInfo(_)) { check_duplicate_set_occurrence(matches, spec) if pos + 1 < arg.length() { let rest = arg.unsafe_substring(start=pos + 1, end=arg.length()) @@ -439,9 +439,9 @@ fn parse_command_impl( break } else { match spec.info { - Flag(action=Help, ..) => + FlagInfo(action=Help, ..) => raise_context_help(cmd, inherited_globals, command_path) - Flag(action=Version, ..) => + FlagInfo(action=Version, ..) => raise_version( version_text_for_short_action( cmd, short, inherited_version_short, @@ -563,7 +563,7 @@ fn version_text_for_long_action( inherited_version_long : Map[String, String], ) -> String { for arg in cmd.args { - if arg.info is Flag(long=Some(name), action=Version, ..) && name == long { + if arg.info is FlagInfo(long=Some(name), action=Version, ..) && name == long { return command_version(cmd) } } @@ -577,7 +577,8 @@ fn version_text_for_short_action( inherited_version_short : Map[Char, String], ) -> String { for arg in cmd.args { - if arg.info is Flag(short=Some(value), action=Version, ..) && value == short { + if arg.info is FlagInfo(short=Some(value), action=Version, ..) && + value == short { return command_version(cmd) } } diff --git a/argparse/parser_globals_merge.mbt b/argparse/parser_globals_merge.mbt index 458e43d1b..e36978778 100644 --- a/argparse/parser_globals_merge.mbt +++ b/argparse/parser_globals_merge.mbt @@ -69,7 +69,7 @@ fn merge_global_value_from_child( if !has_parent && !has_child { return } - if arg.multiple || arg.info is Option(action=Append, ..) { + if arg.multiple || arg.info is OptionInfo(action=Append, ..) { let both_argv = parent_source is Some(Argv) && child_source is Some(Argv) if both_argv { let merged = [] @@ -111,7 +111,7 @@ fn merge_global_value_from_child( has_child && parent_source is Some(Argv) && child_source is Some(Argv) && - arg.info is Option(action=Set, ..) { + arg.info is OptionInfo(action=Set, ..) { raise InvalidArgument( "argument '\{global_option_conflict_label(arg)}' cannot be used multiple times", ) @@ -145,7 +145,7 @@ fn merge_global_flag_from_child( ) -> Unit { match child.flags.get(name) { Some(v) => - if arg.info is Flag(action=Count, ..) { + if arg.info is FlagInfo(action=Count, ..) { let has_parent = parent.flags.get(name) is Some(_) let parent_source = parent.flag_sources.get(name) let child_source = child.flag_sources.get(name) @@ -211,9 +211,9 @@ fn merge_globals_from_child( continue } match arg.info { - Option(_) | Positional(_) => + OptionInfo(_) | PositionalInfo(_) => merge_global_value_from_child(parent, child, arg, name) - Flag(_) => merge_global_flag_from_child(parent, child, arg, name) + FlagInfo(_) => merge_global_flag_from_child(parent, child, arg, name) } } } @@ -221,7 +221,7 @@ fn merge_globals_from_child( ///| fn global_option_conflict_label(arg : Arg) -> String { match arg.info { - Flag(long~, short~, ..) | Option(long~, short~, ..) => + FlagInfo(long~, short~, ..) | OptionInfo(long~, short~, ..) => match long { Some(name) => "--\{name}" None => @@ -230,7 +230,7 @@ fn global_option_conflict_label(arg : Arg) -> String { None => arg.name } } - Positional(_) => arg.name + PositionalInfo(_) => arg.name } } @@ -246,7 +246,7 @@ fn propagate_globals_to_child( if child_local_non_globals.contains(name) { continue } - if arg.info is (Option(_) | Positional(_)) { + if arg.info is (OptionInfo(_) | PositionalInfo(_)) { match parent.values.get(name) { Some(values) => { child.values[name] = values.copy() @@ -265,7 +265,7 @@ fn propagate_globals_to_child( Some(src) => child.flag_sources[name] = src None => () } - if arg.info is Flag(action=Count, ..) { + if arg.info is FlagInfo(action=Count, ..) { match parent.counts.get(name) { Some(c) => child.counts[name] = c None => () diff --git a/argparse/parser_lookup.mbt b/argparse/parser_lookup.mbt index 38282e75c..87e883806 100644 --- a/argparse/parser_lookup.mbt +++ b/argparse/parser_lookup.mbt @@ -19,12 +19,14 @@ fn build_long_index( ) -> Map[String, Arg] { let index : Map[String, Arg] = {} for arg in globals { - if arg.info is (Flag(long~, ..) | Option(long~, ..)) && long is Some(name) { + if arg.info is (FlagInfo(long~, ..) | OptionInfo(long~, ..)) && + long is Some(name) { index[name] = arg } } for arg in args { - if arg.info is (Flag(long~, ..) | Option(long~, ..)) && long is Some(name) { + if arg.info is (FlagInfo(long~, ..) | OptionInfo(long~, ..)) && + long is Some(name) { index[name] = arg } } @@ -35,13 +37,13 @@ fn build_long_index( fn build_short_index(globals : Array[Arg], args : Array[Arg]) -> Map[Char, Arg] { let index : Map[Char, Arg] = {} for arg in globals { - if arg.info is (Flag(short~, ..) | Option(short~, ..)) && + if arg.info is (FlagInfo(short~, ..) | OptionInfo(short~, ..)) && short is Some(value) { index[value] = arg } } for arg in args { - if arg.info is (Flag(short~, ..) | Option(short~, ..)) && + if arg.info is (FlagInfo(short~, ..) | OptionInfo(short~, ..)) && short is Some(value) { index[value] = arg } @@ -51,7 +53,7 @@ fn build_short_index(globals : Array[Arg], args : Array[Arg]) -> Map[Char, Arg] ///| fn collect_globals(args : Array[Arg]) -> Array[Arg] { - args.filter(arg => arg.global && arg.info is (Flag(_) | Option(_))) + args.filter(arg => arg.global && arg.info is (FlagInfo(_) | OptionInfo(_))) } ///| diff --git a/argparse/parser_positionals.mbt b/argparse/parser_positionals.mbt index 036e5f4e3..9e5293067 100644 --- a/argparse/parser_positionals.mbt +++ b/argparse/parser_positionals.mbt @@ -16,7 +16,7 @@ fn positional_args(args : Array[Arg]) -> Array[Arg] { let ordered = [] for arg in args { - if arg.info is Positional(_) { + if arg.info is PositionalInfo(_) { ordered.push(arg) } } @@ -27,7 +27,7 @@ fn positional_args(args : Array[Arg]) -> Array[Arg] { fn last_positional_index(positionals : Array[Arg]) -> Int? { for idx in 0.. return false } let next_allow = match next.info { - Flag(_) => false - Option(allow_hyphen_values~, ..) | Positional(allow_hyphen_values~, ..) => - allow_hyphen_values + FlagInfo(_) => false + OptionInfo(allow_hyphen_values~, ..) + | PositionalInfo(allow_hyphen_values~, ..) => allow_hyphen_values } let allow = next_allow || is_negative_number(arg) if !allow { diff --git a/argparse/parser_validate.mbt b/argparse/parser_validate.mbt index cd3d8e0c1..d0f1b9a8a 100644 --- a/argparse/parser_validate.mbt +++ b/argparse/parser_validate.mbt @@ -58,7 +58,7 @@ fn ValidationCtx::record_arg( } } match arg.info { - Flag(long~, short~, negatable~, ..) => { + FlagInfo(long~, short~, negatable~, ..) => { if long is Some(name) { check_long(name) if negatable { @@ -69,7 +69,7 @@ fn ValidationCtx::record_arg( check_short(short) } } - Option(long~, short~, ..) => { + OptionInfo(long~, short~, ..) => { if long is Some(name) { check_long(name) } @@ -77,7 +77,7 @@ fn ValidationCtx::record_arg( check_short(short) } } - Positional(_) => () + PositionalInfo(_) => () } self.args.push(arg) } @@ -134,7 +134,7 @@ fn validate_inherited_global_shadowing( } } match arg.info { - Flag(long~, short~, negatable~, ..) => { + FlagInfo(long~, short~, negatable~, ..) => { if long is Some(name) { validate_inherited_global_long_collision(arg, name, inherited_globals) if negatable { @@ -151,7 +151,7 @@ fn validate_inherited_global_shadowing( ) } } - Option(long~, short~, ..) => { + OptionInfo(long~, short~, ..) => { if long is Some(name) { validate_inherited_global_long_collision(arg, name, inherited_globals) } @@ -161,7 +161,7 @@ fn validate_inherited_global_shadowing( ) } } - Positional(_) => () + PositionalInfo(_) => () } } } @@ -184,18 +184,18 @@ fn merge_inherited_globals( ///| fn global_override_compatible(inherited_arg : Arg, arg : Arg) -> Bool { match inherited_arg.info { - Flag(action=inherited_action, negatable=inherited_negatable, ..) => + FlagInfo(action=inherited_action, negatable=inherited_negatable, ..) => match arg.info { - Flag(action~, negatable~, ..) => + FlagInfo(action~, negatable~, ..) => inherited_action == action && inherited_negatable == negatable _ => false } - Option(action=inherited_action, ..) => + OptionInfo(action=inherited_action, ..) => match arg.info { - Option(action~, ..) => inherited_action == action + OptionInfo(action~, ..) => inherited_action == action _ => false } - Positional(_) => false + PositionalInfo(_) => false } } @@ -238,16 +238,16 @@ fn inherited_global_long_owner( continue } match inherited.info { - Flag(long=inherited_long, negatable~, ..) => + FlagInfo(long=inherited_long, negatable~, ..) => if inherited_long is Some(name) && (name == long || (negatable && "no-\{name}" == long)) { return Some(inherited.name) } - Option(long=inherited_long, ..) => + OptionInfo(long=inherited_long, ..) => if inherited_long is Some(name) && name == long { return Some(inherited.name) } - Positional(_) => () + PositionalInfo(_) => () } } None @@ -264,11 +264,12 @@ fn inherited_global_short_owner( continue } match inherited.info { - Flag(short=inherited_short, ..) | Option(short=inherited_short, ..) => + FlagInfo(short=inherited_short, ..) + | OptionInfo(short=inherited_short, ..) => if inherited_short is Some(value) && value == short { return Some(inherited.name) } - Positional(_) => () + PositionalInfo(_) => () } } None @@ -280,7 +281,7 @@ fn validate_flag_arg( ctx : ValidationCtx, ) -> Unit raise ArgBuildError { validate_named_option_arg(arg) - guard arg.info is Flag(action~, negatable~, ..) + guard arg.info is FlagInfo(action~, negatable~, ..) if action is (Help | Version) { guard !negatable else { raise Unsupported("help/version actions do not support negatable") @@ -322,7 +323,8 @@ fn validate_positional_arg( ///| fn validate_named_option_arg(arg : Arg) -> Unit raise ArgBuildError { - guard arg.info is (Flag(long~, short~, ..) | Option(long~, short~, ..)) + guard arg.info + is (FlagInfo(long~, short~, ..) | OptionInfo(long~, short~, ..)) guard long is Some(_) || short is Some(_) || arg.env is Some(_) else { raise Unsupported("flag/option args require short/long/env") } @@ -330,11 +332,12 @@ fn validate_named_option_arg(arg : Arg) -> Unit raise ArgBuildError { ///| fn validate_default_values(arg : Arg) -> Unit raise ArgBuildError { - if arg.info is (Option(default_values~, ..) | Positional(default_values~, ..)) && + if arg.info + is (OptionInfo(default_values~, ..) | PositionalInfo(default_values~, ..)) && default_values is Some(values) && values.length() > 1 && !arg.multiple && - !(arg.info is Option(action=Append, ..)) { + !(arg.info is OptionInfo(action=Append, ..)) { raise Unsupported( "default_values with multiple entries require action=Append", ) @@ -463,7 +466,7 @@ fn validate_help_subcommand(cmd : Command) -> Unit raise ArgBuildError { ///| fn validate_version_actions(cmd : Command) -> Unit raise ArgBuildError { if cmd.version is None && - cmd.args.any(arg => arg.info is Flag(action=Version, ..)) { + cmd.args.any(arg => arg.info is FlagInfo(action=Version, ..)) { raise Unsupported("version action requires command version text") } } @@ -561,9 +564,9 @@ fn validate_values( if arg.required && !present { raise MissingRequired(arg.name, None) } - guard arg.info is (Option(_) | Positional(_)) else { continue } + guard arg.info is (OptionInfo(_) | PositionalInfo(_)) else { continue } if !present { - if arg.info is Positional(_) { + if arg.info is PositionalInfo(_) { let (min, _) = arg_min_max(arg) if min > 0 { raise TooFewValues(arg.name, 0, min) @@ -577,7 +580,7 @@ fn validate_values( if count < min { raise TooFewValues(arg.name, count, min) } - if !(arg.info is Option(action=Append, ..)) { + if !(arg.info is OptionInfo(action=Append, ..)) { match max { Some(max) if count > max => raise TooManyValues(arg.name, count, max) _ => () diff --git a/argparse/parser_values.mbt b/argparse/parser_values.mbt index 45d742864..4a9684eb3 100644 --- a/argparse/parser_values.mbt +++ b/argparse/parser_values.mbt @@ -88,7 +88,7 @@ fn add_value( arg : Arg, source : ValueSource, ) -> Unit { - if arg.multiple || arg.info is Option(action=Append, ..) { + if arg.multiple || arg.info is OptionInfo(action=Append, ..) { let arr = matches.values.get(name).unwrap_or([]) arr.push(value) matches.values[name] = arr @@ -107,29 +107,29 @@ fn assign_value( source : ValueSource, ) -> Unit raise ArgParseError { match arg.info { - Option(action=Append, ..) => + OptionInfo(action=Append, ..) => add_value(matches, arg.name, value, arg, source) - Option(action=Set, ..) | Positional(_) => + OptionInfo(action=Set, ..) | PositionalInfo(_) => add_value(matches, arg.name, value, arg, source) - Flag(action=SetTrue, ..) => { + FlagInfo(action=SetTrue, ..) => { let flag = parse_bool(value) matches.flags[arg.name] = flag matches.flag_sources[arg.name] = source } - Flag(action=SetFalse, ..) => { + FlagInfo(action=SetFalse, ..) => { let flag = parse_bool(value) matches.flags[arg.name] = !flag matches.flag_sources[arg.name] = source } - Flag(action=Count, ..) => { + FlagInfo(action=Count, ..) => { let count = parse_count(value) matches.counts[arg.name] = count matches.flags[arg.name] = count > 0 matches.flag_sources[arg.name] = source } - Flag(action=Help, ..) => + FlagInfo(action=Help, ..) => raise InvalidArgument("help action does not take values") - Flag(action=Version, ..) => + FlagInfo(action=Version, ..) => raise InvalidArgument("version action does not take values") } } @@ -137,7 +137,7 @@ fn assign_value( ///| fn option_conflict_label(arg : Arg) -> String { match arg.info { - Flag(long~, short~, ..) | Option(long~, short~, ..) => + FlagInfo(long~, short~, ..) | OptionInfo(long~, short~, ..) => match long { Some(name) => "--\{name}" None => @@ -146,7 +146,7 @@ fn option_conflict_label(arg : Arg) -> String { None => arg.name } } - Positional(_) => arg.name + PositionalInfo(_) => arg.name } } @@ -155,7 +155,9 @@ fn check_duplicate_set_occurrence( matches : Matches, arg : Arg, ) -> Unit raise ArgParseError { - guard arg.info is (Option(action=Set, ..) | Positional(_)) else { return } + guard arg.info is (OptionInfo(action=Set, ..) | PositionalInfo(_)) else { + return + } if matches.values.get(arg.name) is Some(_) { raise InvalidArgument( "argument '\{option_conflict_label(arg)}' cannot be used multiple times", @@ -174,7 +176,8 @@ fn should_stop_option_value( return false } if arg.info - is (Option(allow_hyphen_values~, ..) | Positional(allow_hyphen_values~, ..)) && + is (OptionInfo(allow_hyphen_values~, ..) + | PositionalInfo(allow_hyphen_values~, ..)) && allow_hyphen_values { // Rust clap parity: // - `clap_builder/src/parser/parser.rs`: `parse_long_arg` / `parse_short_arg` @@ -209,29 +212,29 @@ fn apply_env( Some(v) => v None => continue } - if arg.info is (Option(_) | Positional(_)) { + if arg.info is (OptionInfo(_) | PositionalInfo(_)) { assign_value(matches, arg, value, Env) continue } match arg.info { - Flag(action=Count, ..) => { + FlagInfo(action=Count, ..) => { let count = parse_count(value) matches.counts[name] = count matches.flags[name] = count > 0 matches.flag_sources[name] = Env } - Flag(action=SetFalse, ..) => { + FlagInfo(action=SetFalse, ..) => { let flag = parse_bool(value) matches.flags[name] = !flag matches.flag_sources[name] = Env } - Flag(action=SetTrue, ..) => { + FlagInfo(action=SetTrue, ..) => { let flag = parse_bool(value) matches.flags[name] = flag matches.flag_sources[name] = Env } - Option(_) | Positional(_) => () - Flag(action=Help | Version, ..) => () + OptionInfo(_) | PositionalInfo(_) => () + FlagInfo(action=Help | Version, ..) => () } } } @@ -240,8 +243,8 @@ fn apply_env( fn apply_defaults(matches : Matches, args : Array[Arg]) -> Unit { for arg in args { let default_values = match arg.info { - Flag(_) => continue - Option(default_values~, ..) | Positional(default_values~, ..) => + FlagInfo(_) => continue + OptionInfo(default_values~, ..) | PositionalInfo(default_values~, ..) => default_values } if matches_has_value_or_flag(matches, arg.name) { @@ -265,15 +268,15 @@ fn matches_has_value_or_flag(matches : Matches, name : String) -> Bool { ///| fn apply_flag(matches : Matches, arg : Arg, source : ValueSource) -> Unit { match arg.info { - Flag(action=SetTrue, ..) => matches.flags[arg.name] = true - Flag(action=SetFalse, ..) => matches.flags[arg.name] = false - Flag(action=Count, ..) => { + FlagInfo(action=SetTrue, ..) => matches.flags[arg.name] = true + FlagInfo(action=SetFalse, ..) => matches.flags[arg.name] = false + FlagInfo(action=Count, ..) => { let current = matches.counts.get(arg.name).unwrap_or(0) matches.counts[arg.name] = current + 1 matches.flags[arg.name] = true } - Flag(action=Help, ..) => () - Flag(action=Version, ..) => () + FlagInfo(action=Help, ..) => () + FlagInfo(action=Version, ..) => () _ => matches.flags[arg.name] = true } matches.flag_sources[arg.name] = source diff --git a/argparse/pkg.generated.mbti b/argparse/pkg.generated.mbti index 53be1bc70..a997d27f0 100644 --- a/argparse/pkg.generated.mbti +++ b/argparse/pkg.generated.mbti @@ -24,13 +24,20 @@ pub fn ArgGroup::new(StringView, required? : Bool, multiple? : Bool, args? : Arr pub struct Command { // private fields - fn new(StringView, flags? : ArrayView[FlagArg], options? : ArrayView[OptionArg], positionals? : ArrayView[PositionalArg], 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]) -> Command + fn new(StringView, flags? : ArrayView[Flag], options? : ArrayView[Option], positionals? : ArrayView[Positional], 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]) -> Command } -pub fn Command::new(StringView, flags? : ArrayView[FlagArg], options? : ArrayView[OptionArg], positionals? : ArrayView[PositionalArg], subcommands? : ArrayView[Self], 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]) -> Self +pub fn Command::new(StringView, flags? : ArrayView[Flag], options? : ArrayView[Option], positionals? : ArrayView[Positional], subcommands? : ArrayView[Self], 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]) -> Self #as_free_fn pub fn Command::parse(Self, argv? : ArrayView[String], env? : Map[String, String]) -> Matches raise ArgError pub fn Command::render_help(Self) -> String +pub struct Flag { + // private fields + + fn new(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) -> Flag +} +pub fn Flag::new(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) -> Self + pub(all) enum FlagAction { SetTrue SetFalse @@ -41,13 +48,6 @@ pub(all) enum FlagAction { pub impl Eq for FlagAction pub impl Show for FlagAction -pub struct FlagArg { - // private fields - - fn new(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 -} -pub fn FlagArg::new(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) -> Self - pub struct Matches { flags : Map[String, Bool] values : Map[String, Array[String]] @@ -58,6 +58,13 @@ pub struct Matches { } pub impl @debug.Debug for Matches +pub struct Option { + // private fields + + fn new(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) -> Option +} +pub fn Option::new(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) -> Self + pub(all) enum OptionAction { Set Append @@ -65,19 +72,12 @@ pub(all) enum OptionAction { pub impl Eq for OptionAction pub impl Show for OptionAction -pub struct OptionArg { - // private fields - - fn new(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 -} -pub fn OptionArg::new(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) -> Self - -pub struct PositionalArg { +pub struct Positional { // private fields - fn new(StringView, about? : StringView, env? : StringView, default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : ArrayView[String], conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, hidden? : Bool) -> PositionalArg + fn new(StringView, about? : StringView, env? : StringView, default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : ArrayView[String], conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, hidden? : Bool) -> Positional } -pub fn PositionalArg::new(StringView, about? : StringView, env? : StringView, default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : ArrayView[String], conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, hidden? : Bool) -> Self +pub fn Positional::new(StringView, about? : StringView, env? : StringView, default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : ArrayView[String], conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, hidden? : Bool) -> Self pub struct ValueRange { // private fields From d30458592d8cbaed80e46c51bc5cbd14b40f4e00 Mon Sep 17 00:00:00 2001 From: zihang Date: Mon, 2 Mar 2026 11:40:40 +0800 Subject: [PATCH 24/40] fix(argparse): keep group usage hints in error line only --- argparse/argparse_blackbox_test.mbt | 49 +++++++++-- argparse/argparse_test.mbt | 6 +- argparse/help_render.mbt | 60 +++++++++++++- argparse/parser.mbt | 124 +++++++++++++++++++++++++--- 4 files changed, 218 insertions(+), 21 deletions(-) diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 440d28c1e..21f13d20e 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -49,7 +49,7 @@ test "render help snapshot with groups and hidden entries" { inspect( cmd.render_help(), content=( - #|Usage: render [options] [rest...] [command] + #|Usage: render --path [options] [rest...] [command] #| #|Commands: #| run run @@ -63,7 +63,7 @@ test "render help snapshot with groups and hidden entries" { #| -h, --help Show help information. #| -f, --fast #| --[no-]cache cache - #| -p, --path env: PATH_ENV, defaults: a, b, required + #| -p, --path env: PATH_ENV, defaults: a, b #| #|Groups: #| mode (required, exclusive) -f, --fast, -p, --path @@ -2466,6 +2466,43 @@ test "global version action keeps parent version text in subcommand context" { } } +///| +test "subcommand help puts required options in usage" { + let cmd = @argparse.Command("demo", subcommands=[ + Command( + "run", + about="Run a file", + options=[Option("mode", short='m', required=true)], + positionals=[Positional("file", required=true)], + ), + ]) + + try cmd.parse(argv=["run", "--oops"], env=empty_env()) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: unexpected argument '--oops' found + #| + #|Usage: demo run --mode + #| + #|Run a file + #| + #|Arguments: + #| file required + #| + #|Options: + #| -h, --help Show help information. + #| -m, --mode + #| + ), + ) + _ => panic() + } noraise { + _ => panic() + } +} + ///| test "required and env-fed ranged values validate after parsing" { let required_cmd = @argparse.Command("demo", options=[ @@ -2478,11 +2515,11 @@ test "required and env-fed ranged values validate after parsing" { content=( #|error: the following required argument was not provided: 'input' #| - #|Usage: demo [options] + #|Usage: demo --input #| #|Options: #| -h, --help Show help information. - #| --input required + #| --input #| ), ) @@ -2959,7 +2996,7 @@ test "global override with incompatible inherited type is rejected" { content=( #|error: global arg 'mode' is incompatible with inherited global definition #| - #|Usage: demo [options] [command] + #|Usage: demo --mode [command] #| #|Commands: #| run @@ -2967,7 +3004,7 @@ test "global override with incompatible inherited type is rejected" { #| #|Options: #| -h, --help Show help information. - #| --mode required + #| --mode #| ), ) diff --git a/argparse/argparse_test.mbt b/argparse/argparse_test.mbt index 0802e9ff1..1427da63b 100644 --- a/argparse/argparse_test.mbt +++ b/argparse/argparse_test.mbt @@ -412,7 +412,8 @@ test "arg groups required and multiple" { inspect( msg, content=( - #|error: the following required argument group was not provided: 'mode' + #|error: the following required arguments were not provided: + #| <--fast|--slow> #| #|Usage: demo [options] #| @@ -472,7 +473,8 @@ test "arg groups requires and conflicts" { inspect( msg, content=( - #|error: the following required argument group was not provided: 'output' + #|error: the following required arguments were not provided: + #| <--json> #| #|Usage: demo [options] #| diff --git a/argparse/help_render.mbt b/argparse/help_render.mbt index 987cba0ac..e447f36d6 100644 --- a/argparse/help_render.mbt +++ b/argparse/help_render.mbt @@ -15,7 +15,15 @@ ///| /// Render help text for a clap-style command. fn render_help(cmd : Command) -> String { - let usage_line = "Usage: \{cmd.name}\{usage_tail(cmd)}" + render_help_with_usage_override(cmd, None) +} + +///| +fn render_help_with_usage_override( + cmd : Command, + usage_line : String?, +) -> String { + let usage_line = usage_line.unwrap_or("Usage: \{cmd.name}\{usage_tail(cmd)}") let about = cmd.about.unwrap_or("") let about_section = if about == "" { "" @@ -43,6 +51,10 @@ fn render_help(cmd : Command) -> String { ///| fn usage_tail(cmd : Command) -> String { let mut tail = "" + let required_options = required_option_usage(cmd) + if required_options != "" { + tail = "\{tail} \{required_options}" + } if has_options(cmd) { tail = "\{tail} [options]" } @@ -76,13 +88,55 @@ fn has_options(cmd : Command) -> Bool { continue } if arg.info is (OptionInfo(long~, short~, ..) | FlagInfo(long~, short~, ..)) && - (long is Some(_) || short is Some(_)) { + (long is Some(_) || short is Some(_)) && + !is_required_arg(arg) { return true } } false } +///| +fn required_option_usage(cmd : Command) -> String { + let parts = Array::new(capacity=cmd.args.length()) + for arg in cmd.args { + if arg.hidden || !is_required_arg(arg) { + continue + } + if arg.info is PositionalInfo(_) { + continue + } + if required_option_token(arg) is Some(token) { + parts.push(token) + } + } + parts.join(" ") +} + +///| +fn required_option_token(arg : Arg) -> String? { + let prefix = match arg.info { + OptionInfo(long~, short~, ..) | FlagInfo(long~, short~, ..) => + if long is Some(long) { + Some("--\{long}") + } else if short is Some(short) { + Some("-\{short}") + } else { + None + } + PositionalInfo(_) => None + } + match prefix { + Some(prefix) => + if arg.info is OptionInfo(_) { + Some("\{prefix} <\{arg.name}>") + } else { + Some(prefix) + } + None => None + } +} + ///| fn positional_usage(cmd : Command) -> String { let parts = Array::new(capacity=cmd.args.length()) @@ -327,7 +381,7 @@ fn arg_doc(arg : Arg) -> String { notes.push("default: \{values[0]}") } } - if is_required_arg(arg) { + if is_required_arg(arg) && arg.info is PositionalInfo(_) { notes.push("required") } let help = arg.about.unwrap_or("") diff --git a/argparse/parser.mbt b/argparse/parser.mbt index 6d59117ed..b685d24f8 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -47,14 +47,23 @@ fn[T] raise_subcommand_conflict(name : String) -> T raise ArgParseError { ) } +///| +fn help_context_command( + cmd : Command, + inherited_globals : Array[Arg], + command_path : String, +) -> Command { + let help_name = if command_path == "" { cmd.name } else { command_path } + { ..cmd, args: inherited_globals + cmd.args, name: help_name } +} + ///| fn render_help_for_context( cmd : Command, inherited_globals : Array[Arg], command_path : String, ) -> String { - let help_name = if command_path == "" { cmd.name } else { command_path } - let help_cmd = { ..cmd, args: inherited_globals + cmd.args, name: help_name } + let help_cmd = help_context_command(cmd, inherited_globals, command_path) render_help(help_cmd) } @@ -102,6 +111,78 @@ fn format_error_with_help( "\{msg}\n\n\{render_help_for_context(cmd, inherited_globals, command_path)}" } +///| +fn arg_usage_token_for_group(arg : Arg) -> String { + let base = match arg.info { + OptionInfo(long~, short~, ..) | FlagInfo(long~, short~, ..) => + if long is Some(long) { + "--\{long}" + } else if short is Some(short) { + "-\{short}" + } else { + arg.name + } + PositionalInfo(_) => + if arg.multiple { + "<\{arg.name}...>" + } else { + "<\{arg.name}>" + } + } + if arg.info is OptionInfo(_) { + "\{base} <\{arg.name}>" + } else { + base + } +} + +///| +fn group_usage_expr( + groups : Array[ArgGroup], + args : Array[Arg], + name : String, +) -> String? { + for group in groups { + if group.name != name { + continue + } + let members = [] + for member_name in group.args { + if args.search_by(arg => arg.name == member_name) is Some(idx) { + let item = args[idx] + if item.hidden { + continue + } + members.push(arg_usage_token_for_group(item)) + } + } + if members.length() == 0 { + return None + } + let joined = members.join("|") + return Some("<\{joined}>") + } + None +} + +///| +fn missing_group_error_message( + cmd : Command, + inherited_globals : Array[Arg], + command_path : String, + group_name : String, +) -> String { + let help_cmd = help_context_command(cmd, inherited_globals, command_path) + if group_usage_expr(help_cmd.groups, help_cmd.args, group_name) is Some(expr) { + ( + $|error: the following required arguments were not provided: + $| \{expr} + ) + } else { + MissingGroup(group_name).arg_parse_error_message() + } +} + ///| fn arg_error_for_parse_failure( err : ArgParseError, @@ -109,14 +190,37 @@ fn arg_error_for_parse_failure( inherited_globals : Array[Arg], command_path : String, ) -> ArgError { - Message( - format_error_with_help( - err.arg_parse_error_message(), - cmd, - inherited_globals, - command_path, - ), - ) + match err { + MissingGroup(name) => + Message( + format_error_with_help( + missing_group_error_message( + cmd, inherited_globals, command_path, name, + ), + cmd, + inherited_globals, + command_path, + ), + ) + GroupConflict(_) => + Message( + format_error_with_help( + err.arg_parse_error_message(), + cmd, + inherited_globals, + command_path, + ), + ) + _ => + Message( + format_error_with_help( + err.arg_parse_error_message(), + cmd, + inherited_globals, + command_path, + ), + ) + } } ///| From 51dbd0faab1515dfa3ad9e082e4c47f8fdc44452 Mon Sep 17 00:00:00 2001 From: zihang Date: Mon, 2 Mar 2026 12:01:54 +0800 Subject: [PATCH 25/40] fix(argparse): drop positional required markers from help args section --- argparse/README.mbt.md | 4 ++-- argparse/argparse_blackbox_test.mbt | 10 +++++----- argparse/help_render.mbt | 3 --- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index 305ca3d2a..450694e5a 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -196,8 +196,8 @@ test "bounded non-last positional failure snapshot" { #|Usage: demo #| #|Arguments: - #| first... required - #| second required + #| first... + #| second #| #|Options: #| -h, --help Show help information. diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 21f13d20e..2436f839f 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -56,7 +56,7 @@ test "render help snapshot with groups and hidden entries" { #| help Print help for the subcommand(s). #| #|Arguments: - #| target required + #| target #| rest... #| #|Options: @@ -852,7 +852,7 @@ test "empty positional value range is rejected at build time" { #| #|Arguments: #| skip - #| name required + #| name #| #|Options: #| -h, --help Show help information. @@ -1420,7 +1420,7 @@ test "validation branches exposed through parse" { #|Usage: demo #| #|Arguments: - #| x... required + #| x... #| #|Options: #| -h, --help Show help information. @@ -2489,7 +2489,7 @@ test "subcommand help puts required options in usage" { #|Run a file #| #|Arguments: - #| file required + #| file #| #|Options: #| -h, --help Show help information. @@ -2594,7 +2594,7 @@ test "positional num_args lower bound rejects missing argv values" { #|Usage: demo #| #|Arguments: - #| first... required + #| first... #| #|Options: #| -h, --help Show help information. diff --git a/argparse/help_render.mbt b/argparse/help_render.mbt index e447f36d6..87f3d3dba 100644 --- a/argparse/help_render.mbt +++ b/argparse/help_render.mbt @@ -381,9 +381,6 @@ fn arg_doc(arg : Arg) -> String { notes.push("default: \{values[0]}") } } - if is_required_arg(arg) && arg.info is PositionalInfo(_) { - notes.push("required") - } let help = arg.about.unwrap_or("") if help == "" { notes.join(", ") From 24d72a989d6b62e26ed72b538a5842de8569e4b4 Mon Sep 17 00:00:00 2001 From: zihang Date: Mon, 2 Mar 2026 13:11:17 +0800 Subject: [PATCH 26/40] docs(argparse): reorganize README usage sections --- argparse/README.mbt.md | 318 ++++++++++++++++++++++++++++++++--------- 1 file changed, 249 insertions(+), 69 deletions(-) diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index 450694e5a..47ee6d236 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -7,15 +7,14 @@ small, predictable feature set. `long` defaults to the argument name. Pass `long=""` to disable long alias. -## 1. Basic Command + +## Quick Start ```mbt check ///| test "basic option + positional success snapshot" { let matches = @argparse.parse( - Command("demo", options=[Option("name", long="name")], positionals=[ - Positional("target"), - ]), + Command("demo", options=[Option("name")], positionals=[Positional("target")]), argv=["--name", "alice", "file.txt"], ) @debug.debug_inspect( @@ -28,7 +27,7 @@ test "basic option + positional success snapshot" { ///| test "basic option + positional failure snapshot" { - let cmd = @argparse.Command("demo", options=[Option("name", long="name")], positionals=[ + let cmd = @argparse.Command("demo", options=[Option("name")], positionals=[ Positional("target"), ]) try cmd.parse(argv=["--bad"], env={}) catch { @@ -55,7 +54,8 @@ test "basic option + positional failure snapshot" { } ``` -## 2. Flags And Negation + +## Flags And Negation `flags` stay as `Map[String, Bool]`, so negated flags preserve explicit `false` states. @@ -63,9 +63,7 @@ states. ```mbt check ///| test "negatable flag success snapshot" { - let cmd = @argparse.Command("demo", flags=[ - Flag("cache", long="cache", negatable=true), - ]) + let cmd = @argparse.Command("demo", flags=[Flag("cache", negatable=true)]) let parsed = cmd.parse(argv=["--no-cache"], env={}) catch { _ => panic() } @debug.debug_inspect( @@ -78,9 +76,7 @@ test "negatable flag success snapshot" { ///| test "negatable flag failure snapshot" { - let cmd = @argparse.Command("demo", flags=[ - Flag("cache", long="cache", negatable=true), - ]) + let cmd = @argparse.Command("demo", flags=[Flag("cache", negatable=true)]) try cmd.parse(argv=["--oops"], env={}) catch { Message(msg) => inspect( @@ -102,7 +98,7 @@ test "negatable flag failure snapshot" { } ``` -## 3. Subcommands And Globals +## Subcommands And Globals ```mbt check ///| @@ -159,48 +155,163 @@ test "subcommand context failure snapshot" { } ``` -## 4. Positional Value Ranges +## Value Sources (argv > env > default_values) -Positionals are parsed in declaration order (no explicit index). +Value precedence is `argv > env > default_values`. ```mbt check ///| -test "bounded non-last positional success snapshot" { - let cmd = @argparse.Command("demo", positionals=[ - Positional("first", num_args=ValueRange(lower=1, upper=2)), - Positional("second", required=true), +test "value source precedence snapshots" { + let cmd = @argparse.Command("demo", options=[ + Option("level", env="LEVEL", default_values=["1"]), ]) - let parsed = cmd.parse(argv=["a", "b", "c"], env={}) catch { _ => panic() } + let from_default = cmd.parse(argv=[], env={}) catch { _ => panic() } @debug.debug_inspect( - parsed.values, + from_default.values, content=( - #|{ "first": ["a", "b"], "second": ["c"] } + #|{ "level": ["1"] } + ), + ) + @debug.debug_inspect( + from_default.sources, + content=( + #|{ "level": Default } + ), + ) + + let from_env = cmd.parse(argv=[], env={ "LEVEL": "2" }) catch { _ => panic() } + @debug.debug_inspect( + from_env.values, + content=( + #|{ "level": ["2"] } + ), + ) + @debug.debug_inspect( + from_env.sources, + content=( + #|{ "level": Env } + ), + ) + + let from_argv = cmd.parse(argv=["--level", "3"], env={ "LEVEL": "2" }) catch { + _ => panic() + } + @debug.debug_inspect( + from_argv.values, + content=( + #|{ "level": ["3"] } + ), + ) + @debug.debug_inspect( + from_argv.sources, + content=( + #|{ "level": Argv } ), ) } +``` + +## Input Forms +```mbt check ///| -test "bounded non-last positional failure snapshot" { +test "option input forms snapshot" { + let cmd = @argparse.Command("demo", options=[Option("count", short='c')]) + + let long_split = cmd.parse(argv=["--count", "2"], env={}) catch { + _ => panic() + } + @debug.debug_inspect( + long_split.values, + content=( + #|{ "count": ["2"] } + ), + ) + + let long_inline = cmd.parse(argv=["--count=3"], env={}) catch { _ => panic() } + @debug.debug_inspect( + long_inline.values, + content=( + #|{ "count": ["3"] } + ), + ) + + let short_split = cmd.parse(argv=["-c", "4"], env={}) catch { _ => panic() } + @debug.debug_inspect( + short_split.values, + content=( + #|{ "count": ["4"] } + ), + ) + + let short_attached = cmd.parse(argv=["-c5"], env={}) catch { _ => panic() } + @debug.debug_inspect( + short_attached.values, + content=( + #|{ "count": ["5"] } + ), + ) +} + +///| +test "double-dash separator snapshot" { let cmd = @argparse.Command("demo", positionals=[ - Positional("first", num_args=ValueRange(lower=1, upper=2)), - Positional("second", required=true), + Positional( + "tail", + num_args=ValueRange(lower=0), + last=true, + allow_hyphen_values=true, + ), ]) - try cmd.parse(argv=["a", "b", "c", "d"], env={}) catch { + let parsed = cmd.parse(argv=["--", "--x", "-y"], env={}) catch { + _ => panic() + } + @debug.debug_inspect( + parsed.values, + content=( + #|{ "tail": ["--x", "-y"] } + ), + ) +} +``` + +## Constraints And Policies + +`parse` raises a single string error (`ArgError::Message`) that includes the +error and full contextual help. + +```mbt check +///| +test "requires relationship success and failure snapshots" { + let cmd = @argparse.Command("demo", options=[ + Option("mode", requires=["config"]), + Option("config"), + ]) + + let ok = cmd.parse(argv=["--mode", "fast", "--config", "cfg.toml"], env={}) catch { + _ => panic() + } + @debug.debug_inspect( + ok.values, + content=( + #|{ "mode": ["fast"], "config": ["cfg.toml"] } + ), + ) + + try cmd.parse(argv=["--mode", "fast"], env={}) catch { Message(msg) => inspect( msg, content=( - #|error: too many positional arguments were provided - #| - #|Usage: demo + #|error: the following required argument was not provided: 'config' (required by 'mode') #| - #|Arguments: - #| first... - #| second + #|Usage: demo [options] #| #|Options: - #| -h, --help Show help information. + #| -h, --help Show help information. + #| --mode + #| --config #| ), ) @@ -208,32 +319,63 @@ test "bounded non-last positional failure snapshot" { _ => panic() } } -``` -## 5. Error Snapshot Pattern +///| +test "arg group required and exclusive failure snapshot" { + let cmd = @argparse.Command( + "demo", + groups=[ + ArgGroup("mode", required=true, multiple=false, args=["fast", "slow"]), + ], + flags=[Flag("fast"), Flag("slow")], + ) -`parse` now emits one string error payload (`ArgError::Message`) that already -contains the full contextual help text. + try cmd.parse(argv=[], env={}) catch { + Message(msg) => + inspect( + msg, + 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() + } +} -```mbt check ///| -test "root invalid option snapshot" { - let cmd = @argparse.Command("demo", options=[ - Option("count", long="count", about="repeat count"), +test "subcommand required policy failure snapshot" { + let cmd = @argparse.Command("demo", subcommand_required=true, subcommands=[ + Command("echo"), ]) - try cmd.parse(argv=["--bad"], env={}) catch { + try cmd.parse(argv=[], env={}) catch { Message(msg) => inspect( msg, content=( - #|error: unexpected argument '--bad' found + #|error: the following required argument was not provided: 'subcommand' #| - #|Usage: demo [options] + #|Usage: demo + #| + #|Commands: + #| echo + #| help Print help for the subcommand(s). #| #|Options: - #| -h, --help Show help information. - #| --count repeat count + #| -h, --help Show help information. #| ), ) @@ -241,25 +383,37 @@ test "root invalid option snapshot" { _ => panic() } } +``` +```mbt check ///| -test "subcommand invalid option snapshot" { - let cmd = @argparse.Command("demo", subcommands=[ - Command("echo", options=[Option("times", long="times")]), +test "conflicts_with success and failure snapshots" { + let cmd = @argparse.Command("demo", flags=[ + Flag("verbose", conflicts_with=["quiet"]), + Flag("quiet"), ]) - try cmd.parse(argv=["echo", "--oops"], env={}) catch { + let ok = cmd.parse(argv=["--verbose"], env={}) catch { _ => panic() } + @debug.debug_inspect( + ok.flags, + content=( + #|{ "verbose": true } + ), + ) + + try cmd.parse(argv=["--verbose", "--quiet"], env={}) catch { Message(msg) => inspect( msg, content=( - #|error: unexpected argument '--oops' found + #|error: conflicting arguments: verbose and quiet #| - #|Usage: demo echo [options] + #|Usage: demo [options] #| #|Options: - #| -h, --help Show help information. - #| --times + #| -h, --help Show help information. + #| --verbose + #| --quiet #| ), ) @@ -269,27 +423,53 @@ test "subcommand invalid option snapshot" { } ``` -## 6. Rendering Help Without Parsing +## Positional Value Ranges + +Positionals are parsed in declaration order (no explicit index). ```mbt check ///| -test "render_help remains pure" { - let cmd = @argparse.Command("demo", about="demo app", options=[ - Option("count", long="count"), +test "bounded non-last positional success snapshot" { + let cmd = @argparse.Command("demo", positionals=[ + Positional("first", num_args=ValueRange(lower=1, upper=2)), + Positional("second", required=true), ]) - let help = cmd.render_help() - inspect( - help, + + let parsed = cmd.parse(argv=["a", "b", "c"], env={}) catch { _ => panic() } + @debug.debug_inspect( + parsed.values, content=( - #|Usage: demo [options] - #| - #|demo app - #| - #|Options: - #| -h, --help Show help information. - #| --count - #| + #|{ "first": ["a", "b"], "second": ["c"] } ), ) } + +///| +test "bounded non-last positional failure snapshot" { + let cmd = @argparse.Command("demo", positionals=[ + Positional("first", num_args=ValueRange(lower=1, upper=2)), + Positional("second", required=true), + ]) + try cmd.parse(argv=["a", "b", "c", "d"], env={}) catch { + Message(msg) => + inspect( + msg, + content=( + #|error: too many positional arguments were provided + #| + #|Usage: demo + #| + #|Arguments: + #| first... + #| second + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) + } noraise { + _ => panic() + } +} ``` From 4fb2433cafcbce138e4027434208ba1a0fbe0ab5 Mon Sep 17 00:00:00 2001 From: zihang Date: Mon, 2 Mar 2026 13:52:55 +0800 Subject: [PATCH 27/40] refactor(argparse): remove positional required and last options --- argparse/README.mbt.md | 11 +++-------- argparse/arg_spec.mbt | 10 ++-------- argparse/argparse_blackbox_test.mbt | 26 +++++++++----------------- argparse/argparse_test.mbt | 2 +- argparse/parser.mbt | 11 ----------- argparse/parser_positionals.mbt | 11 ----------- argparse/parser_values.mbt | 2 -- argparse/pkg.generated.mbti | 4 ++-- 8 files changed, 17 insertions(+), 60 deletions(-) diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index 47ee6d236..7f4c8947e 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -257,12 +257,7 @@ test "option input forms snapshot" { ///| test "double-dash separator snapshot" { let cmd = @argparse.Command("demo", positionals=[ - Positional( - "tail", - num_args=ValueRange(lower=0), - last=true, - allow_hyphen_values=true, - ), + Positional("tail", num_args=ValueRange(lower=0), allow_hyphen_values=true), ]) let parsed = cmd.parse(argv=["--", "--x", "-y"], env={}) catch { _ => panic() @@ -432,7 +427,7 @@ Positionals are parsed in declaration order (no explicit index). test "bounded non-last positional success snapshot" { let cmd = @argparse.Command("demo", positionals=[ Positional("first", num_args=ValueRange(lower=1, upper=2)), - Positional("second", required=true), + Positional("second", num_args=@argparse.ValueRange::single()), ]) let parsed = cmd.parse(argv=["a", "b", "c"], env={}) catch { _ => panic() } @@ -448,7 +443,7 @@ test "bounded non-last positional success snapshot" { test "bounded non-last positional failure snapshot" { let cmd = @argparse.Command("demo", positionals=[ Positional("first", num_args=ValueRange(lower=1, upper=2)), - Positional("second", required=true), + Positional("second", num_args=@argparse.ValueRange::single()), ]) try cmd.parse(argv=["a", "b", "c", "d"], env={}) catch { Message(msg) => diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt index d881456fb..2475d9e97 100644 --- a/argparse/arg_spec.mbt +++ b/argparse/arg_spec.mbt @@ -62,7 +62,6 @@ priv enum ArgInfo { ) PositionalInfo( num_args~ : ValueRange?, - last~ : Bool, default_values~ : Array[String]?, allow_hyphen_values~ : Bool ) @@ -140,7 +139,6 @@ pub fn Flag::new( // // default_values: None, // allow_hyphen_values: false, - // last: false, // multiple: false, }, @@ -246,10 +244,8 @@ pub struct Positional { default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool, - last? : Bool, requires? : ArrayView[String], conflicts_with? : ArrayView[String], - required? : Bool, global? : Bool, hidden? : Bool, ) -> Positional @@ -261,6 +257,7 @@ pub struct Positional { /// Positional ordering is declaration order. /// /// `num_args` controls the accepted value count. +/// Use `num_args=ValueRange::single()` for a required single positional. pub fn Positional::new( name : StringView, about? : StringView, @@ -268,10 +265,8 @@ pub fn Positional::new( default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool = false, - last? : Bool = false, requires? : ArrayView[String] = [], conflicts_with? : ArrayView[String] = [], - required? : Bool = false, global? : Bool = false, hidden? : Bool = false, ) -> Positional { @@ -285,13 +280,12 @@ pub fn Positional::new( env, requires: requires.to_array(), conflicts_with: conflicts_with.to_array(), - required, + required: false, global, hidden, // info: PositionalInfo( num_args~, - last~, default_values=default_values.map(values => values.to_array()), allow_hyphen_values~, ), diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 2436f839f..24e258cad 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -41,7 +41,7 @@ test "render help snapshot with groups and hidden entries" { ), ], positionals=[ - Positional("target", required=true), + Positional("target", num_args=@argparse.ValueRange::single()), Positional("rest", num_args=ValueRange(lower=0)), Positional("secret", hidden=true), ], @@ -84,7 +84,6 @@ test "render help conversion coverage snapshot" { about="f", env="F_ENV", requires=["opt"], - required=true, global=true, hidden=true, ), @@ -111,10 +110,8 @@ test "render help conversion coverage snapshot" { default_values=["p1", "p2"], num_args=ValueRange(lower=0, upper=2), allow_hyphen_values=true, - last=true, requires=["opt"], conflicts_with=["f"], - required=true, global=true, hidden=true, ), @@ -746,14 +743,9 @@ test "negation parsing and invalid negation forms" { } ///| -test "positionals force mode and dash handling" { +test "positionals dash handling and separator" { let force_cmd = @argparse.Command("demo", positionals=[ - Positional( - "tail", - num_args=ValueRange(lower=0), - last=true, - allow_hyphen_values=true, - ), + Positional("tail", num_args=ValueRange(lower=0), allow_hyphen_values=true), ]) let forced = force_cmd.parse(argv=["a", "--x", "-y"], env=empty_env()) catch { _ => panic() @@ -809,7 +801,7 @@ test "variadic positional keeps accepting hyphen values after first token" { test "bounded positional does not greedily consume later required values" { let cmd = @argparse.Command("demo", positionals=[ Positional("first", num_args=ValueRange(lower=1, upper=2)), - Positional("second", required=true), + Positional("second", num_args=@argparse.ValueRange::single()), ]) let two = cmd.parse(argv=["a", "b"], env=empty_env()) catch { _ => panic() } @@ -825,7 +817,7 @@ test "bounded positional does not greedily consume later required values" { test "indexed non-last positional allows explicit single num_args" { let cmd = @argparse.Command("demo", positionals=[ Positional("first", num_args=@argparse.ValueRange::single()), - Positional("second", required=true), + Positional("second", num_args=@argparse.ValueRange::single()), ]) let parsed = cmd.parse(argv=["a", "b"], env=empty_env()) catch { @@ -839,7 +831,7 @@ test "empty positional value range is rejected at build time" { try @argparse.Command("demo", positionals=[ Positional("skip", num_args=ValueRange(lower=0, upper=0)), - Positional("name", required=true), + Positional("name", num_args=@argparse.ValueRange::single()), ]).parse(argv=["alice"], env=empty_env()) catch { Message(msg) => @@ -2184,7 +2176,7 @@ test "empty groups without presence do not fail" { ///| test "help rendering edge paths stay stable" { let required_many = @argparse.Command("demo", positionals=[ - Positional("files", required=true, num_args=ValueRange(lower=1)), + Positional("files", num_args=ValueRange(lower=1)), ]) let required_help = required_many.render_help() assert_true(required_help.has_prefix("Usage: demo ")) @@ -2473,7 +2465,7 @@ test "subcommand help puts required options in usage" { "run", about="Run a file", options=[Option("mode", short='m', required=true)], - positionals=[Positional("file", required=true)], + positionals=[Positional("file", num_args=@argparse.ValueRange::single())], ), ]) @@ -2539,7 +2531,7 @@ test "required and env-fed ranged values validate after parsing" { } ///| -test "positionals honor explicit index sorting with last ranged positional" { +test "positionals keep declaration order with ranged positional" { let cmd = @argparse.Command("demo", positionals=[ Positional("late", num_args=ValueRange(lower=2, upper=2)), Positional("first"), diff --git a/argparse/argparse_test.mbt b/argparse/argparse_test.mbt index 1427da63b..25c5a6d07 100644 --- a/argparse/argparse_test.mbt +++ b/argparse/argparse_test.mbt @@ -119,7 +119,7 @@ test "declaration order controls positional parsing" { test "bounded non-last positional remains supported" { let cmd = @argparse.Command("demo", positionals=[ Positional("first", num_args=ValueRange(lower=1, upper=2)), - Positional("second", required=true), + Positional("second", num_args=@argparse.ValueRange::single()), ]) let two = cmd.parse(argv=["a", "b"], env=empty_env()) catch { _ => panic() } diff --git a/argparse/parser.mbt b/argparse/parser.mbt index b685d24f8..cd3d8b571 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -382,7 +382,6 @@ fn parse_command_impl( long_index.get("version") is None let positionals = positional_args(args) let positional_values = [] - let last_pos_idx = last_positional_index(positionals) let mut i = 0 let mut positional_arg_found = false while i < argv.length() { @@ -396,16 +395,6 @@ fn parse_command_impl( } break } - let force_positional = match last_pos_idx { - Some(idx) => positional_values.length() >= idx - None => false - } - if force_positional { - positional_values.push(arg) - positional_arg_found = true - i = i + 1 - continue - } if builtin_help_short && arg == "-h" { raise_context_help(cmd, inherited_globals, command_path) } diff --git a/argparse/parser_positionals.mbt b/argparse/parser_positionals.mbt index 9e5293067..cdcd5407c 100644 --- a/argparse/parser_positionals.mbt +++ b/argparse/parser_positionals.mbt @@ -23,17 +23,6 @@ fn positional_args(args : Array[Arg]) -> Array[Arg] { ordered } -///| -fn last_positional_index(positionals : Array[Arg]) -> Int? { - for idx in 0.. Arg? { let target = collected.length() diff --git a/argparse/parser_values.mbt b/argparse/parser_values.mbt index 4a9684eb3..276ef3be0 100644 --- a/argparse/parser_values.mbt +++ b/argparse/parser_values.mbt @@ -62,8 +62,6 @@ fn positional_min_required(arg : Arg) -> Int { let (min, _) = arg_min_max(arg) if min > 0 { min - } else if arg.required { - 1 } else { 0 } diff --git a/argparse/pkg.generated.mbti b/argparse/pkg.generated.mbti index a997d27f0..44f11ce7e 100644 --- a/argparse/pkg.generated.mbti +++ b/argparse/pkg.generated.mbti @@ -75,9 +75,9 @@ pub impl Show for OptionAction pub struct Positional { // private fields - fn new(StringView, about? : StringView, env? : StringView, default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : ArrayView[String], conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, hidden? : Bool) -> Positional + fn new(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) -> Positional } -pub fn Positional::new(StringView, about? : StringView, env? : StringView, default_values? : ArrayView[String], num_args? : ValueRange, allow_hyphen_values? : Bool, last? : Bool, requires? : ArrayView[String], conflicts_with? : ArrayView[String], required? : Bool, global? : Bool, hidden? : Bool) -> Self +pub fn Positional::new(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) -> Self pub struct ValueRange { // private fields From f910fabb8d08965168bc5b3cf01e3410e8e7fb26 Mon Sep 17 00:00:00 2001 From: zihang Date: Mon, 2 Mar 2026 14:03:16 +0800 Subject: [PATCH 28/40] docs(argparse): add positional passthrough separator examples --- argparse/README.mbt.md | 60 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index 7f4c8947e..1f6d08de7 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -467,4 +467,64 @@ test "bounded non-last positional failure snapshot" { _ => panic() } } + +///| +test "positional passthrough keeps child argv after double-dash snapshot" { + let cmd = @argparse.Command( + "wrap", + options=[Option("config"), Option("mode")], + positionals=[ + Positional("child_argv", num_args=ValueRange(lower=0), allow_hyphen_values=true), + ], + ) + + let parsed = cmd.parse(argv=[ + "--config", + "cfg.toml", + "--", + "child", + "--mode", + "fast", + "--", + "--flag", + ], env={}) catch { + _ => panic() + } + @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 cmd = @argparse.Command( + "wrap", + options=[Option("config"), Option("mode")], + positionals=[ + Positional("child_argv", num_args=ValueRange(lower=0), allow_hyphen_values=true), + ], + ) + + let parsed = cmd.parse(argv=[ + "--config", + "cfg.toml", + "child", + "--mode", + "fast", + ], env={}) catch { + _ => panic() + } + @debug.debug_inspect( + parsed.values, + content=( + #|{ "config": ["cfg.toml"], "mode": ["fast"], "child_argv": ["child"] } + ), + ) +} ``` From 380f3f53142f87936df61ea8d95bb7d021a2d727 Mon Sep 17 00:00:00 2001 From: zihang Date: Mon, 2 Mar 2026 14:13:12 +0800 Subject: [PATCH 29/40] test(argparse): remove unreachable panic branches and clean README examples --- argparse/README.mbt.md | 82 ++++++++++++----------------- argparse/argparse_blackbox_test.mbt | 77 --------------------------- argparse/argparse_test.mbt | 13 ----- 3 files changed, 35 insertions(+), 137 deletions(-) diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index 1f6d08de7..6e5ccfefb 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -16,6 +16,7 @@ test "basic option + positional success snapshot" { let matches = @argparse.parse( Command("demo", options=[Option("name")], positionals=[Positional("target")]), argv=["--name", "alice", "file.txt"], + env={}, ) @debug.debug_inspect( matches.values, @@ -65,7 +66,7 @@ states. test "negatable flag success snapshot" { let cmd = @argparse.Command("demo", flags=[Flag("cache", negatable=true)]) - let parsed = cmd.parse(argv=["--no-cache"], env={}) catch { _ => panic() } + let parsed = try! cmd.parse(argv=["--no-cache"], env={}) @debug.debug_inspect( parsed.flags, content=( @@ -109,16 +110,14 @@ test "global count flag success snapshot" { subcommands=[Command("run")], ) - let parsed = cmd.parse(argv=["-v", "run", "-v"], env={}) catch { - _ => panic() - } + let parsed = try! cmd.parse(argv=["-v", "run", "-v"], env={}) @debug.debug_inspect( parsed.flag_counts, content=( #|{ "verbose": 2 } ), ) - guard parsed.subcommand is Some(("run", child)) else { panic() } + guard parsed.subcommand is Some(("run", child)) @debug.debug_inspect( child.flag_counts, content=( @@ -166,7 +165,7 @@ test "value source precedence snapshots" { Option("level", env="LEVEL", default_values=["1"]), ]) - let from_default = cmd.parse(argv=[], env={}) catch { _ => panic() } + let from_default = try! cmd.parse(argv=[], env={}) @debug.debug_inspect( from_default.values, content=( @@ -180,7 +179,7 @@ test "value source precedence snapshots" { ), ) - let from_env = cmd.parse(argv=[], env={ "LEVEL": "2" }) catch { _ => panic() } + let from_env = try! cmd.parse(argv=[], env={ "LEVEL": "2" }) @debug.debug_inspect( from_env.values, content=( @@ -194,9 +193,7 @@ test "value source precedence snapshots" { ), ) - let from_argv = cmd.parse(argv=["--level", "3"], env={ "LEVEL": "2" }) catch { - _ => panic() - } + let from_argv = try! cmd.parse(argv=["--level", "3"], env={ "LEVEL": "2" }) @debug.debug_inspect( from_argv.values, content=( @@ -219,9 +216,7 @@ test "value source precedence snapshots" { test "option input forms snapshot" { let cmd = @argparse.Command("demo", options=[Option("count", short='c')]) - let long_split = cmd.parse(argv=["--count", "2"], env={}) catch { - _ => panic() - } + let long_split = try! cmd.parse(argv=["--count", "2"], env={}) @debug.debug_inspect( long_split.values, content=( @@ -229,7 +224,7 @@ test "option input forms snapshot" { ), ) - let long_inline = cmd.parse(argv=["--count=3"], env={}) catch { _ => panic() } + let long_inline = try! cmd.parse(argv=["--count=3"], env={}) @debug.debug_inspect( long_inline.values, content=( @@ -237,7 +232,7 @@ test "option input forms snapshot" { ), ) - let short_split = cmd.parse(argv=["-c", "4"], env={}) catch { _ => panic() } + let short_split = try! cmd.parse(argv=["-c", "4"], env={}) @debug.debug_inspect( short_split.values, content=( @@ -245,7 +240,7 @@ test "option input forms snapshot" { ), ) - let short_attached = cmd.parse(argv=["-c5"], env={}) catch { _ => panic() } + let short_attached = try! cmd.parse(argv=["-c5"], env={}) @debug.debug_inspect( short_attached.values, content=( @@ -259,9 +254,7 @@ test "double-dash separator snapshot" { let cmd = @argparse.Command("demo", positionals=[ Positional("tail", num_args=ValueRange(lower=0), allow_hyphen_values=true), ]) - let parsed = cmd.parse(argv=["--", "--x", "-y"], env={}) catch { - _ => panic() - } + let parsed = try! cmd.parse(argv=["--", "--x", "-y"], env={}) @debug.debug_inspect( parsed.values, content=( @@ -284,9 +277,7 @@ test "requires relationship success and failure snapshots" { Option("config"), ]) - let ok = cmd.parse(argv=["--mode", "fast", "--config", "cfg.toml"], env={}) catch { - _ => panic() - } + let ok = try! cmd.parse(argv=["--mode", "fast", "--config", "cfg.toml"], env={}) @debug.debug_inspect( ok.values, content=( @@ -388,7 +379,7 @@ test "conflicts_with success and failure snapshots" { Flag("quiet"), ]) - let ok = cmd.parse(argv=["--verbose"], env={}) catch { _ => panic() } + let ok = try! cmd.parse(argv=["--verbose"], env={}) @debug.debug_inspect( ok.flags, content=( @@ -430,7 +421,7 @@ test "bounded non-last positional success snapshot" { Positional("second", num_args=@argparse.ValueRange::single()), ]) - let parsed = cmd.parse(argv=["a", "b", "c"], env={}) catch { _ => panic() } + let parsed = try! cmd.parse(argv=["a", "b", "c"], env={}) @debug.debug_inspect( parsed.values, content=( @@ -474,22 +465,20 @@ test "positional passthrough keeps child argv after double-dash snapshot" { "wrap", options=[Option("config"), Option("mode")], positionals=[ - Positional("child_argv", num_args=ValueRange(lower=0), allow_hyphen_values=true), + Positional( + "child_argv", + num_args=ValueRange(lower=0), + allow_hyphen_values=true, + ), ], ) - let parsed = cmd.parse(argv=[ - "--config", - "cfg.toml", - "--", - "child", - "--mode", - "fast", - "--", - "--flag", - ], env={}) catch { - _ => panic() - } + let parsed = try! cmd.parse( + argv=[ + "--config", "cfg.toml", "--", "child", "--mode", "fast", "--", "--flag", + ], + env={}, + ) @debug.debug_inspect( parsed.values, content=( @@ -507,19 +496,18 @@ test "without separator outer parser still consumes its own option names snapsho "wrap", options=[Option("config"), Option("mode")], positionals=[ - Positional("child_argv", num_args=ValueRange(lower=0), allow_hyphen_values=true), + Positional( + "child_argv", + num_args=ValueRange(lower=0), + allow_hyphen_values=true, + ), ], ) - let parsed = cmd.parse(argv=[ - "--config", - "cfg.toml", - "child", - "--mode", - "fast", - ], env={}) catch { - _ => panic() - } + let parsed = try! cmd.parse( + argv=["--config", "cfg.toml", "child", "--mode", "fast"], + env={}, + ) @debug.debug_inspect( parsed.values, content=( diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 24e258cad..0bea84c5b 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -350,7 +350,6 @@ test "subcommand cannot follow positional arguments" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -437,7 +436,6 @@ test "help subcommand styles and errors" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -460,7 +458,6 @@ test "help subcommand styles and errors" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -500,7 +497,6 @@ test "subcommand help includes inherited global options" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -529,7 +525,6 @@ test "unknown argument suggestions are exposed" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -551,7 +546,6 @@ test "unknown argument suggestions are exposed" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -571,7 +565,6 @@ test "unknown argument suggestions are exposed" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -613,7 +606,6 @@ test "long and short value parsing branches" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -633,7 +625,6 @@ test "long and short value parsing branches" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -681,7 +672,6 @@ test "negation parsing and invalid negation forms" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -702,7 +692,6 @@ test "negation parsing and invalid negation forms" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -723,7 +712,6 @@ test "negation parsing and invalid negation forms" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -780,7 +768,6 @@ test "positionals dash handling and separator" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -851,7 +838,6 @@ test "empty positional value range is rejected at build time" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -889,7 +875,6 @@ test "env parsing for settrue setfalse count and invalid values" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -911,7 +896,6 @@ test "env parsing for settrue setfalse count and invalid values" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -933,7 +917,6 @@ test "env parsing for settrue setfalse count and invalid values" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -955,7 +938,6 @@ test "env parsing for settrue setfalse count and invalid values" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1005,7 +987,6 @@ test "defaults and value range helpers through public API" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1043,7 +1024,6 @@ test "options consume exactly one value per occurrence" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1067,7 +1047,6 @@ test "set options reject duplicate occurrences" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1094,7 +1073,6 @@ test "flag and option args require short or long names" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1118,7 +1096,6 @@ test "flag and option args require short or long names" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1164,7 +1141,6 @@ test "option parsing stops at the next option token" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1185,7 +1161,6 @@ test "option parsing stops at the next option token" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1212,7 +1187,6 @@ test "options always require a value" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1248,7 +1222,6 @@ test "option values reject hyphen tokens unless allow_hyphen_values is enabled" ) rejected = true } - _ => panic() } noraise { _ => rejected = true } @@ -1295,7 +1268,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1319,7 +1291,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1344,7 +1315,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1369,7 +1339,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1393,7 +1362,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1419,7 +1387,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1445,7 +1412,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1471,7 +1437,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1503,7 +1468,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1527,7 +1491,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1551,7 +1514,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1575,7 +1537,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1601,7 +1562,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1627,7 +1587,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1653,7 +1612,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1679,7 +1637,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1704,7 +1661,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1729,7 +1685,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1758,7 +1713,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1782,7 +1736,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1810,7 +1763,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1880,7 +1832,6 @@ test "validation branches exposed through parse" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1916,7 +1867,6 @@ test "builtin and custom help/version dispatch edge paths" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -1983,7 +1933,6 @@ test "group validation catches unknown requires target" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2010,7 +1959,6 @@ test "group validation catches unknown conflicts_with target" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2053,7 +2001,6 @@ test "group requires/conflicts can target argument names" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2089,7 +2036,6 @@ test "group requires/conflicts can target argument names" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2127,7 +2073,6 @@ test "arg validation catches unknown requires target" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2154,7 +2099,6 @@ test "arg validation catches unknown conflicts_with target" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2201,7 +2145,6 @@ test "help rendering edge paths stay stable" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2256,7 +2199,6 @@ test "unified error message formatting remains stable" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2276,7 +2218,6 @@ test "unified error message formatting remains stable" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2312,7 +2253,6 @@ test "options require one value per occurrence" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2345,7 +2285,6 @@ test "short options require one value before next option token" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2389,7 +2328,6 @@ test "version action dispatches on custom long and short flags" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2432,7 +2370,6 @@ test "global version action keeps parent version text in subcommand context" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2452,7 +2389,6 @@ test "global version action keeps parent version text in subcommand context" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2489,7 +2425,6 @@ test "subcommand help puts required options in usage" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2515,7 +2450,6 @@ test "required and env-fed ranged values validate after parsing" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2593,7 +2527,6 @@ test "positional num_args lower bound rejects missing argv values" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2707,7 +2640,6 @@ test "missing option values are reported when next token is another option" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2731,7 +2663,6 @@ test "short-only set options use short label in duplicate errors" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2757,7 +2688,6 @@ test "unknown short suggestion can be absent" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2864,7 +2794,6 @@ test "child local arg shadowing inherited global is rejected at build time" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -2965,7 +2894,6 @@ test "global set option rejects duplicate occurrences across subcommands" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -3000,7 +2928,6 @@ test "global override with incompatible inherited type is rejected" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -3033,7 +2960,6 @@ test "child local long alias collision with inherited global is rejected" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -3064,7 +2990,6 @@ test "child local short alias collision with inherited global is rejected" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -3120,7 +3045,6 @@ test "non-bmp hyphen token reports unknown argument without panic" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -3223,7 +3147,6 @@ test "global override with different negatable setting is rejected" { #| ), ) - _ => panic() } noraise { _ => panic() } diff --git a/argparse/argparse_test.mbt b/argparse/argparse_test.mbt index 25c5a6d07..160086dfe 100644 --- a/argparse/argparse_test.mbt +++ b/argparse/argparse_test.mbt @@ -300,7 +300,6 @@ test "display help and version" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -332,7 +331,6 @@ test "parse error show is readable" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -355,7 +353,6 @@ test "parse error show is readable" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -384,7 +381,6 @@ test "relationships and num args" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -427,7 +423,6 @@ test "arg groups required and multiple" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -451,7 +446,6 @@ test "arg groups required and multiple" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -489,7 +483,6 @@ test "arg groups requires and conflicts" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -523,7 +516,6 @@ test "arg groups requires and conflicts" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -682,7 +674,6 @@ test "negatable and conflicts" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -706,7 +697,6 @@ test "flag does not accept inline value" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -731,7 +721,6 @@ test "built-in long flags do not accept inline value" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -751,7 +740,6 @@ test "built-in long flags do not accept inline value" { #| ), ) - _ => panic() } noraise { _ => panic() } @@ -806,7 +794,6 @@ test "command policies" { #| ), ) - _ => panic() } noraise { _ => panic() } From 8a79e13752d85081a7a6db978d7bda682e850db4 Mon Sep 17 00:00:00 2001 From: zihang Date: Mon, 2 Mar 2026 14:25:56 +0800 Subject: [PATCH 30/40] chore: cleanup --- argparse/arg_spec.mbt | 42 ++++---------------------------- argparse/runtime_exit.mbt | 29 +--------------------- argparse/runtime_exit_js.mbt | 2 +- argparse/runtime_exit_native.mbt | 2 +- argparse/runtime_exit_wasm.mbt | 16 +++--------- 5 files changed, 11 insertions(+), 80 deletions(-) diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt index 2475d9e97..40fae8fb8 100644 --- a/argparse/arg_spec.mbt +++ b/argparse/arg_spec.mbt @@ -129,17 +129,7 @@ pub fn Flag::new( requires: requires.to_array(), conflicts_with: conflicts_with.to_array(), required, - // info: FlagInfo(short~, long~, action~, negatable~), - // - // option_action: OptionAction::Set, - // - // index: None, - // num_args: None, - // - // default_values: None, - // allow_hyphen_values: false, - // multiple: false, }, } @@ -209,7 +199,6 @@ pub fn Option::new( required, global, hidden, - // info: OptionInfo( short~, long~, @@ -217,15 +206,6 @@ pub fn Option::new( default_values=default_values.map(values => values.to_array()), allow_hyphen_values~, ), - // - // flag_action: FlagAction::SetTrue, - // negatable: false, - // - // option_action: action, - // - // index: None, - // num_args: None, - // multiple: action is Append, }, } @@ -283,20 +263,11 @@ pub fn Positional::new( required: false, global, hidden, - // info: PositionalInfo( num_args~, default_values=default_values.map(values => values.to_array()), allow_hyphen_values~, ), - // short: None, - // long: None, - // - // flag_action: FlagAction::SetTrue, - // negatable: false, - // - // option_action: OptionAction::Set, - // multiple: range_allows_multiple(num_args), }, } @@ -309,12 +280,9 @@ fn arg_name(arg : Arg) -> String { ///| fn range_allows_multiple(range : ValueRange?) -> Bool { - match range { - Some(r) => - match r.upper { - Some(upper) => r.lower != upper || r.lower > 1 - None => true - } - None => false - } + range is Some(r) && + (match r.upper { + Some(upper) => r.lower != upper || r.lower > 1 + None => true + }) } diff --git a/argparse/runtime_exit.mbt b/argparse/runtime_exit.mbt index 8f796fe59..0c83dc599 100644 --- a/argparse/runtime_exit.mbt +++ b/argparse/runtime_exit.mbt @@ -12,9 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -///| -fn println_runtime(s : String) -> Unit = "%println" - ///| fn trim_single_trailing_newline(text : String) -> String { match text.strip_suffix("\n") { @@ -25,31 +22,7 @@ fn trim_single_trailing_newline(text : String) -> String { ///| fn[T] print_and_exit_success(text : String) -> T { - println_runtime(trim_single_trailing_newline(text)) + println(trim_single_trailing_newline(text)) runtime_exit_success() panic() } - -///| -#cfg(target="native") -fn runtime_exit_success() -> Unit { - runtime_exit_success_native() -} - -///| -#cfg(target="js") -fn runtime_exit_success() -> Unit { - runtime_exit_success_js() -} - -///| -#cfg(target="wasm-gc") -fn runtime_exit_success() -> Unit { - runtime_exit_success_wasm() -} - -///| -#cfg(target="wasm") -fn runtime_exit_success() -> Unit { - runtime_exit_success_wasm() -} diff --git a/argparse/runtime_exit_js.mbt b/argparse/runtime_exit_js.mbt index 627077ba6..ee312a997 100644 --- a/argparse/runtime_exit_js.mbt +++ b/argparse/runtime_exit_js.mbt @@ -25,7 +25,7 @@ extern "js" fn runtime_js_try_exit(code : Int) -> Bool = ///| #cfg(target="js") -fn runtime_exit_success_js() -> Unit { +fn runtime_exit_success() -> Unit { if !runtime_js_try_exit(0) { panic() } diff --git a/argparse/runtime_exit_native.mbt b/argparse/runtime_exit_native.mbt index f05586b96..e98d0117b 100644 --- a/argparse/runtime_exit_native.mbt +++ b/argparse/runtime_exit_native.mbt @@ -18,6 +18,6 @@ extern "c" fn runtime_native_exit(code : Int) -> Unit = "exit" ///| #cfg(target="native") -fn runtime_exit_success_native() -> Unit { +fn runtime_exit_success() -> Unit { runtime_native_exit(0) } diff --git a/argparse/runtime_exit_wasm.mbt b/argparse/runtime_exit_wasm.mbt index d97fee243..2fe21b878 100644 --- a/argparse/runtime_exit_wasm.mbt +++ b/argparse/runtime_exit_wasm.mbt @@ -13,21 +13,11 @@ // limitations under the License. ///| -#cfg(target="wasm-gc") +#cfg(any(target="wasm", target="wasm-gc")) fn runtime_wasm_exit(code : Int) -> Unit = "__moonbit_sys_unstable" "exit" ///| -#cfg(target="wasm-gc") -fn runtime_exit_success_wasm() -> Unit { - runtime_wasm_exit(0) -} - -///| -#cfg(target="wasm") -fn runtime_wasm_exit(code : Int) -> Unit = "__moonbit_sys_unstable" "exit" - -///| -#cfg(target="wasm") -fn runtime_exit_success_wasm() -> Unit { +#cfg(any(target="wasm", target="wasm-gc")) +fn runtime_exit_success() -> Unit { runtime_wasm_exit(0) } From e65270c26450395e37e0bc6f2df061ed61fc6c56 Mon Sep 17 00:00:00 2001 From: zihang Date: Mon, 2 Mar 2026 15:14:51 +0800 Subject: [PATCH 31/40] doc(argparse): improve error message --- argparse/README.mbt.md | 2 +- argparse/argparse_blackbox_test.mbt | 10 +++++----- argparse/argparse_test.mbt | 2 +- argparse/error.mbt | 7 +++++-- argparse/parser.mbt | 4 ++-- argparse/parser_values.mbt | 12 +++++++++++- 6 files changed, 25 insertions(+), 12 deletions(-) diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index 6e5ccfefb..eb8cf033c 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -441,7 +441,7 @@ test "bounded non-last positional failure snapshot" { inspect( msg, content=( - #|error: too many positional arguments were provided + #|error: unexpected value 'd' for '' found; no more were expected #| #|Usage: demo #| diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 0bea84c5b..5d8d959d2 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -756,7 +756,7 @@ test "positionals dash handling and separator" { inspect( msg, content=( - #|error: too many positional arguments were provided + #|error: unexpected value 'y' for '' found; no more were expected #| #|Usage: demo [n] #| @@ -1014,7 +1014,7 @@ test "options consume exactly one value per occurrence" { inspect( msg, content=( - #|error: too many positional arguments were provided + #|error: unexpected value 'b' found; no more were expected #| #|Usage: demo [options] #| @@ -1130,7 +1130,7 @@ test "option parsing stops at the next option token" { inspect( msg, content=( - #|error: too many positional arguments were provided + #|error: unexpected value 'y' found; no more were expected #| #|Usage: demo [options] #| @@ -1150,7 +1150,7 @@ test "option parsing stops at the next option token" { inspect( msg, content=( - #|error: too many positional arguments were provided + #|error: unexpected value 'y' found; no more were expected #| #|Usage: demo [options] #| @@ -1329,7 +1329,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: too many positional arguments were provided + #|error: unexpected value 'b' found; no more were expected #| #|Usage: demo [options] #| diff --git a/argparse/argparse_test.mbt b/argparse/argparse_test.mbt index 160086dfe..5049b6a3f 100644 --- a/argparse/argparse_test.mbt +++ b/argparse/argparse_test.mbt @@ -340,7 +340,7 @@ test "parse error show is readable" { inspect( msg, content=( - #|error: too many positional arguments were provided + #|error: unexpected value 'bob' for '' found; no more were expected #| #|Usage: demo [options] [name] #| diff --git a/argparse/error.mbt b/argparse/error.mbt index 8db171886..24de8cc14 100644 --- a/argparse/error.mbt +++ b/argparse/error.mbt @@ -34,7 +34,7 @@ priv suberror ArgParseError { MissingRequired(String, String?) TooFewValues(String, Int, Int) TooManyValues(String, Int, Int) - TooManyPositionals + TooManyPositionals(String, String?) InvalidValue(String) MissingGroup(String) GroupConflict(String) @@ -68,7 +68,10 @@ fn ArgParseError::arg_parse_error_message(self : ArgParseError) -> String { "error: '\{name}' requires at least \{min} values but only \{got} were provided" TooManyValues(name, got, max) => "error: '\{name}' allows at most \{max} values but \{got} were provided" - TooManyPositionals => "error: too many positional arguments were provided" + TooManyPositionals(value, Some(arg)) => + "error: unexpected value '\{value}' for '\{arg}' found; no more were expected" + TooManyPositionals(value, None) => + "error: unexpected value '\{value}' found; no more were expected" InvalidValue(msg) => "error: \{msg}" MissingGroup(name) => "error: the following required argument group was not provided: '\{name}'" diff --git a/argparse/parser.mbt b/argparse/parser.mbt index cd3d8b571..d2c3daba8 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -296,9 +296,9 @@ fn parse_command( inherited_globals, command_path, ) - TooManyPositionals => + TooManyPositionals(value, arg) => raise arg_error_for_parse_failure( - TooManyPositionals, + TooManyPositionals(value, arg), cmd, inherited_globals, command_path, diff --git a/argparse/parser_values.mbt b/argparse/parser_values.mbt index 276ef3be0..1b21847de 100644 --- a/argparse/parser_values.mbt +++ b/argparse/parser_values.mbt @@ -53,7 +53,17 @@ fn assign_positionals( } } if cursor < values.length() { - raise TooManyPositionals + let overflow_label = if positionals.length() > 0 { + let last = positionals[positionals.length() - 1] + if last.multiple { + Some("<\{last.name}...>") + } else { + Some("<\{last.name}>") + } + } else { + None + } + raise TooManyPositionals(values[cursor], overflow_label) } } From fb9fe6bb3c03fb7d55cf7c68711a30419f2041bd Mon Sep 17 00:00:00 2001 From: zihang Date: Mon, 2 Mar 2026 15:41:25 +0800 Subject: [PATCH 32/40] feat(argparse): render help metadata and group flags as tags --- argparse/README.mbt.md | 60 +++++++++++++++++------------ argparse/argparse_blackbox_test.mbt | 34 ++++++++-------- argparse/argparse_test.mbt | 6 +-- argparse/help_render.mbt | 26 ++++++------- argparse/parser_lookup.mbt | 22 +++-------- argparse/parser_values.mbt | 17 ++++---- 6 files changed, 81 insertions(+), 84 deletions(-) diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index eb8cf033c..f94ab8cd7 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -65,6 +65,17 @@ states. ///| test "negatable flag success snapshot" { let cmd = @argparse.Command("demo", flags=[Flag("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={}) @debug.debug_inspect( @@ -74,29 +85,6 @@ test "negatable flag success snapshot" { ), ) } - -///| -test "negatable flag failure snapshot" { - let cmd = @argparse.Command("demo", flags=[Flag("cache", negatable=true)]) - try cmd.parse(argv=["--oops"], env={}) catch { - Message(msg) => - inspect( - msg, - content=( - #|error: unexpected argument '--oops' found - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --[no-]cache - #| - ), - ) - } noraise { - _ => panic() - } -} ``` ## Subcommands And Globals @@ -165,6 +153,18 @@ test "value source precedence snapshots" { Option("level", env="LEVEL", default_values=["1"]), ]) + inspect( + cmd.render_help(), + content=( + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| --level [env: LEVEL] [default: 1] + #| + ), + ) + let from_default = try! cmd.parse(argv=[], env={}) @debug.debug_inspect( from_default.values, @@ -216,6 +216,18 @@ test "value source precedence snapshots" { test "option input forms snapshot" { let cmd = @argparse.Command("demo", options=[Option("count", short='c')]) + inspect( + cmd.render_help(), + content=( + #|Usage: demo [options] + #| + #|Options: + #| -h, --help Show help information. + #| -c, --count + #| + ), + ) + let long_split = try! cmd.parse(argv=["--count", "2"], env={}) @debug.debug_inspect( long_split.values, @@ -332,7 +344,7 @@ test "arg group required and exclusive failure snapshot" { #| --slow #| #|Groups: - #| mode (required, exclusive) --fast, --slow + #| mode [required] [exclusive] --fast, --slow #| ), ) diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 5d8d959d2..020e89b8f 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -63,10 +63,10 @@ test "render help snapshot with groups and hidden entries" { #| -h, --help Show help information. #| -f, --fast #| --[no-]cache cache - #| -p, --path env: PATH_ENV, defaults: a, b + #| -p, --path [env: PATH_ENV] [default: a, b] #| #|Groups: - #| mode (required, exclusive) -f, --fast, -p, --path + #| mode [required] [exclusive] -f, --fast, -p, --path #| ), ) @@ -869,9 +869,9 @@ test "env parsing for settrue setfalse count and invalid values" { #| #|Options: #| -h, --help Show help information. - #| --on env: ON - #| --off env: OFF - #| --v env: V + #| --on [env: ON] + #| --off [env: OFF] + #| --v [env: V] #| ), ) @@ -890,9 +890,9 @@ test "env parsing for settrue setfalse count and invalid values" { #| #|Options: #| -h, --help Show help information. - #| --on env: ON - #| --off env: OFF - #| --v env: V + #| --on [env: ON] + #| --off [env: OFF] + #| --v [env: V] #| ), ) @@ -911,9 +911,9 @@ test "env parsing for settrue setfalse count and invalid values" { #| #|Options: #| -h, --help Show help information. - #| --on env: ON - #| --off env: OFF - #| --v env: V + #| --on [env: ON] + #| --off [env: OFF] + #| --v [env: V] #| ), ) @@ -932,9 +932,9 @@ test "env parsing for settrue setfalse count and invalid values" { #| #|Options: #| -h, --help Show help information. - #| --on env: ON - #| --off env: OFF - #| --v env: V + #| --on [env: ON] + #| --off [env: OFF] + #| --v [env: V] #| ), ) @@ -1311,7 +1311,7 @@ test "validation branches exposed through parse" { #| #|Options: #| -h, --help Show help information. - #| --f env: F + #| --f [env: F] #| ), ) @@ -1358,7 +1358,7 @@ test "validation branches exposed through parse" { #| #|Options: #| -h, --help Show help information. - #| --x defaults: a, b + #| --x [default: a, b] #| ), ) @@ -2790,7 +2790,7 @@ test "child local arg shadowing inherited global is rejected at build time" { #| #|Options: #| -h, --help Show help information. - #| --mode env: MODE, default: safe + #| --mode [env: MODE] [default: safe] #| ), ) diff --git a/argparse/argparse_test.mbt b/argparse/argparse_test.mbt index 5049b6a3f..eb26ce8b1 100644 --- a/argparse/argparse_test.mbt +++ b/argparse/argparse_test.mbt @@ -419,7 +419,7 @@ test "arg groups required and multiple" { #| --slow #| #|Groups: - #| mode (required, exclusive) --fast, --slow + #| mode [required] [exclusive] --fast, --slow #| ), ) @@ -442,7 +442,7 @@ test "arg groups required and multiple" { #| --slow #| #|Groups: - #| mode (required, exclusive) --fast, --slow + #| mode [required] [exclusive] --fast, --slow #| ), ) @@ -566,7 +566,7 @@ test "full help snapshot" { #|Options: #| -h, --help Show help information. #| -v, --verbose Enable verbose mode - #| --count Repeat count (default: 1) + #| --count Repeat count [default: 1] #| ), ) diff --git a/argparse/help_render.mbt b/argparse/help_render.mbt index 87f3d3dba..e1a2cea4d 100644 --- a/argparse/help_render.mbt +++ b/argparse/help_render.mbt @@ -368,25 +368,23 @@ fn positional_display(arg : Arg) -> String { fn arg_doc(arg : Arg) -> String { let notes = [] match arg.env { - Some(env_name) => notes.push("env: \{env_name}") + Some(env_name) => notes.push("[env: \{env_name}]") None => () } if arg.info is (OptionInfo(default_values~, ..) | PositionalInfo(default_values~, ..)) && default_values is Some(values) { - if values.length() > 1 { + if values.length() > 0 { let defaults = values.join(", ") - notes.push("defaults: \{defaults}") - } else if values.length() == 1 { - notes.push("default: \{values[0]}") + notes.push("[default: \{defaults}]") } } let help = arg.about.unwrap_or("") if help == "" { - notes.join(", ") + notes.join(" ") } else if notes.length() > 0 { - let notes_text = notes.join(", ") - "\{help} (\{notes_text})" + let notes_text = notes.join(" ") + "\{help} \{notes_text}" } else { help } @@ -417,18 +415,18 @@ fn is_required_arg(arg : Arg) -> Bool { ///| fn group_label(group : ArgGroup) -> String { - let flags = [] + let tags = [] if group.required { - flags.push("required") + tags.push("[required]") } if !group.multiple { - flags.push("exclusive") + tags.push("[exclusive]") } - if flags.length() == 0 { + if tags.length() == 0 { group.name } else { - let flags_text = flags.join(", ") - "\{group.name} (\{flags_text})" + let tags_text = tags.join(" ") + "\{group.name} \{tags_text}" } } diff --git a/argparse/parser_lookup.mbt b/argparse/parser_lookup.mbt index 87e883806..839920d3f 100644 --- a/argparse/parser_lookup.mbt +++ b/argparse/parser_lookup.mbt @@ -18,13 +18,7 @@ fn build_long_index( args : Array[Arg], ) -> Map[String, Arg] { let index : Map[String, Arg] = {} - for arg in globals { - if arg.info is (FlagInfo(long~, ..) | OptionInfo(long~, ..)) && - long is Some(name) { - index[name] = arg - } - } - for arg in args { + for arg in globals.iter() + args.iter() { if arg.info is (FlagInfo(long~, ..) | OptionInfo(long~, ..)) && long is Some(name) { index[name] = arg @@ -70,16 +64,10 @@ fn resolve_help_target( inherited_globals : Array[Arg], command_path : String, ) -> (Command, Array[Arg], String) raise ArgParseError { - let targets = if argv.length() == 0 { - argv - } else { - let last = argv[argv.length() - 1] - if (last == "-h" && builtin_help_short) || - (last == "--help" && builtin_help_long) { - argv[:argv.length() - 1].to_array() - } else { - argv - } + let targets = match argv { + [.. pre, "-h"] if builtin_help_short => pre + [.. pre, "--help"] if builtin_help_long => pre + _ => argv } let mut current = cmd let mut current_path = command_path diff --git a/argparse/parser_values.mbt b/argparse/parser_values.mbt index 1b21847de..ac7b80281 100644 --- a/argparse/parser_values.mbt +++ b/argparse/parser_values.mbt @@ -270,7 +270,7 @@ fn apply_defaults(matches : Matches, args : Array[Arg]) -> Unit { ///| fn matches_has_value_or_flag(matches : Matches, name : String) -> Bool { - matches.flags.get(name) is Some(_) || matches.values.get(name) is Some(_) + matches.flags.contains(name) || matches.values.contains(name) } ///| @@ -292,14 +292,13 @@ fn apply_flag(matches : Matches, arg : Arg, source : ValueSource) -> Unit { ///| fn parse_bool(value : String) -> Bool raise ArgParseError { - if value == "1" || value == "true" || value == "yes" || value == "on" { - true - } else if value == "0" || value == "false" || value == "no" || value == "off" { - false - } else { - raise InvalidValue( - "invalid value '\{value}' for boolean flag; expected one of: 1, 0, true, false, yes, no, on, off", - ) + match value { + "1" | "true" | "yes" | "on" => true + "0" | "false" | "no" | "off" => false + _ => + raise InvalidValue( + "invalid value '\{value}' for boolean flag; expected one of: 1, 0, true, false, yes, no, on, off", + ) } } From f22d01ceed48ee833a6d491e6868788689aa849d Mon Sep 17 00:00:00 2001 From: zihang Date: Mon, 2 Mar 2026 16:08:28 +0800 Subject: [PATCH 33/40] doc(argparse): improve doc comments --- argparse/README.mbt.md | 39 +++++++++++----------------- argparse/arg_group.mbt | 3 ++- argparse/arg_spec.mbt | 56 +++++++++++++++++++++++----------------- argparse/command.mbt | 11 +++++--- argparse/error.mbt | 3 +++ argparse/matches.mbt | 6 +++++ argparse/value_range.mbt | 4 ++- 7 files changed, 70 insertions(+), 52 deletions(-) diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index f94ab8cd7..d5a66b612 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -470,21 +470,24 @@ test "bounded non-last positional failure snapshot" { _ => panic() } } +``` +```mbt check ///| -test "positional passthrough keeps child argv after double-dash snapshot" { - let cmd = @argparse.Command( - "wrap", - options=[Option("config"), Option("mode")], - positionals=[ - Positional( - "child_argv", - num_args=ValueRange(lower=0), - allow_hyphen_values=true, - ), - ], - ) +let cmd : @argparse.Command = Command( + "wrap", + options=[Option("config"), Option("mode")], + positionals=[ + Positional( + "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", @@ -504,18 +507,6 @@ test "positional passthrough keeps child argv after double-dash snapshot" { ///| test "without separator outer parser still consumes its own option names snapshot" { - let cmd = @argparse.Command( - "wrap", - options=[Option("config"), Option("mode")], - positionals=[ - Positional( - "child_argv", - num_args=ValueRange(lower=0), - allow_hyphen_values=true, - ), - ], - ) - let parsed = try! cmd.parse( argv=["--config", "cfg.toml", "child", "--mode", "fast"], env={}, diff --git a/argparse/arg_group.mbt b/argparse/arg_group.mbt index 2a505a7ea..4821649d8 100644 --- a/argparse/arg_group.mbt +++ b/argparse/arg_group.mbt @@ -39,7 +39,8 @@ pub struct ArgGroup { /// 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. +/// - `requires` and `conflicts_with` may reference either group names or +/// argument names. pub fn ArgGroup::new( name : StringView, required? : Bool = false, diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt index 40fae8fb8..daa72feba 100644 --- a/argparse/arg_spec.mbt +++ b/argparse/arg_spec.mbt @@ -14,6 +14,10 @@ ///| /// Behavior for flag args. +/// +/// - `SetTrue` / `SetFalse` set a boolean value. +/// - `Count` increments `Matches.flag_counts`. +/// - `Help` / `Version` display output and exit successfully when triggered. pub(all) enum FlagAction { SetTrue SetFalse @@ -24,6 +28,9 @@ pub(all) enum FlagAction { ///| /// Behavior for option args. +/// +/// - `Set` keeps the last provided value. +/// - `Append` keeps all provided values in order. pub(all) enum OptionAction { Set Append @@ -92,15 +99,14 @@ pub struct Flag { ///| /// Create a flag argument. /// -/// `long` defaults to `name`. -/// -/// Pass `long=""` to disable the long form explicitly. -/// -/// At least one of `short` or `long` or `env` must be available. -/// -/// `global=true` makes the flag available in subcommands. -/// -/// If `negatable=true`, `--no-` is accepted for long flags. +/// 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-` for long flags. +/// - If `env` is set, accepted boolean values are: +/// `1`, `0`, `true`, `false`, `yes`, `no`, `on`, `off`. pub fn Flag::new( name : StringView, short? : Char, @@ -159,17 +165,16 @@ pub struct Option { } ///| -/// Create an option argument that consumes one value per occurrence. +/// Create an option argument. /// -/// `long` defaults to `name`. -/// -/// Pass `long=""` to disable the long form explicitly. -/// -/// At least one of `short` or `long` or `env` must be available. -/// -/// Use `action=Append` for repeated occurrences. -/// -/// `global=true` makes the option available in subcommands. +/// 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. pub fn Option::new( name : StringView, short? : Char, @@ -234,10 +239,15 @@ pub struct Positional { ///| /// Create a positional argument. /// -/// Positional ordering is declaration order. -/// -/// `num_args` controls the accepted value count. -/// Use `num_args=ValueRange::single()` for a required single positional. +/// Notes: +/// - Positional 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. pub fn Positional::new( name : StringView, about? : StringView, diff --git a/argparse/command.mbt b/argparse/command.mbt index 36e6756c7..ffc81f5c9 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -52,9 +52,14 @@ pub struct Command { /// Create a declarative command specification. /// /// Notes: -/// - `flags` / `options` / `positionals` declare command arguments by kind. -/// - `groups` explicitly declares all group memberships and policies. -/// - Built-in `--help`/`--version` behavior can be disabled with the flags below. +/// - `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 ` routing. +/// - `arg_required_else_help=true` prints help when no argv tokens are provided. +/// - `subcommand_required=true` requires selecting a subcommand. +/// - `hidden=true` omits this command from parent command listings. pub fn Command::new( name : StringView, flags? : ArrayView[Flag] = [], diff --git a/argparse/error.mbt b/argparse/error.mbt index 24de8cc14..40ebf7496 100644 --- a/argparse/error.mbt +++ b/argparse/error.mbt @@ -14,6 +14,9 @@ ///| /// Unified error surface exposed by argparse. +/// +/// `Message` is display-ready text intended for end users. +/// For parse/build failures it already includes the contextual help message. pub suberror ArgError { Message(String) } diff --git a/argparse/matches.mbt b/argparse/matches.mbt index 4ba56189a..efd40e576 100644 --- a/argparse/matches.mbt +++ b/argparse/matches.mbt @@ -25,6 +25,12 @@ pub enum ValueSource { ///| /// 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. pub struct Matches { flags : Map[String, Bool] values : Map[String, Array[String]] diff --git a/argparse/value_range.mbt b/argparse/value_range.mbt index 93965af45..2f62402d4 100644 --- a/argparse/value_range.mbt +++ b/argparse/value_range.mbt @@ -31,7 +31,9 @@ pub fn ValueRange::single() -> ValueRange { ///| /// Create a value-count range. /// -/// Examples: +/// Notes: +/// - `lower` defaults to `0`. +/// - `upper` omitted means no upper bound. /// - `ValueRange(lower=0)` means `0..`. /// - `ValueRange(lower=1, upper=3)` means `1..=3`. pub fn ValueRange::new(lower? : Int = 0, upper? : Int) -> ValueRange { From 74d284414685c04d9d606a31a46d0f3a0a343835 Mon Sep 17 00:00:00 2001 From: zihang Date: Mon, 2 Mar 2026 16:28:03 +0800 Subject: [PATCH 34/40] refactor(argparse): cleanup validation errors and parser internals --- argparse/arg_spec.mbt | 2 +- argparse/argparse_blackbox_test.mbt | 347 +++++----------------------- argparse/argparse_test.mbt | 11 +- argparse/command.mbt | 6 +- argparse/error.mbt | 10 +- argparse/parser.mbt | 42 +--- 6 files changed, 73 insertions(+), 345 deletions(-) diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt index daa72feba..752180913 100644 --- a/argparse/arg_spec.mbt +++ b/argparse/arg_spec.mbt @@ -292,7 +292,7 @@ fn arg_name(arg : Arg) -> String { fn range_allows_multiple(range : ValueRange?) -> Bool { range is Some(r) && (match r.upper { - Some(upper) => r.lower != upper || r.lower > 1 + Some(upper) => upper > 1 None => true }) } diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 020e89b8f..87e2e43e3 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -825,17 +825,7 @@ test "empty positional value range is rejected at build time" { inspect( msg, content=( - #|error: empty value range (0..0) is unsupported - #| - #|Usage: demo [skip] - #| - #|Arguments: - #| skip - #| name - #| - #|Options: - #| -h, --help Show help information. - #| + #|error: command definition validation failed: empty value range (0..0) is unsupported ), ) } noraise { @@ -1064,13 +1054,7 @@ test "flag and option args require short or long names" { inspect( msg, content=( - #|error: flag/option args require short/long/env - #| - #|Usage: demo - #| - #|Options: - #| -h, --help Show help information. - #| + #|error: command definition validation failed: flag/option args require short/long/env ), ) } noraise { @@ -1087,13 +1071,7 @@ test "flag and option args require short or long names" { inspect( msg, content=( - #|error: flag/option args require short/long/env - #| - #|Usage: demo - #| - #|Options: - #| -h, --help Show help information. - #| + #|error: command definition validation failed: flag/option args require short/long/env ), ) } noraise { @@ -1259,13 +1237,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: flag/option args require short/long/env - #| - #|Usage: demo - #| - #|Options: - #| -h, --help Show help information. - #| + #|error: command definition validation failed: flag/option args require short/long/env ), ) } noraise { @@ -1281,14 +1253,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: help/version actions do not support negatable - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --[no-]f - #| + #|error: command definition validation failed: help/version actions do not support negatable ), ) } noraise { @@ -1305,14 +1270,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: help/version actions do not support env/defaults - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --f [env: F] - #| + #|error: command definition validation failed: help/version actions do not support env/defaults ), ) } noraise { @@ -1352,14 +1310,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: default_values with multiple entries require action=Append - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --x [default: a, b] - #| + #|error: command definition validation failed: default_values with multiple entries require action=Append ), ) } noraise { @@ -1375,16 +1326,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: max values must be >= min values - #| - #|Usage: demo - #| - #|Arguments: - #| x... - #| - #|Options: - #| -h, --help Show help information. - #| + #|error: command definition validation failed: max values must be >= min values ), ) } noraise { @@ -1400,16 +1342,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: min values must be >= 0 - #| - #|Usage: demo [x...] - #| - #|Arguments: - #| x... - #| - #|Options: - #| -h, --help Show help information. - #| + #|error: command definition validation failed: min values must be >= 0 ), ) } noraise { @@ -1425,16 +1358,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: max values must be >= 0 - #| - #|Usage: demo [x...] - #| - #|Arguments: - #| x... - #| - #|Options: - #| -h, --help Show help information. - #| + #|error: command definition validation failed: max values must be >= 0 ), ) } noraise { @@ -1459,13 +1383,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: duplicate group: g - #| - #|Usage: demo - #| - #|Options: - #| -h, --help Show help information. - #| + #|error: command definition validation failed: duplicate group: g ), ) } noraise { @@ -1482,13 +1400,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: group cannot require itself: g - #| - #|Usage: demo - #| - #|Options: - #| -h, --help Show help information. - #| + #|error: command definition validation failed: group cannot require itself: g ), ) } noraise { @@ -1505,13 +1417,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: group cannot conflict with itself: g - #| - #|Usage: demo - #| - #|Options: - #| -h, --help Show help information. - #| + #|error: command definition validation failed: group cannot conflict with itself: g ), ) } noraise { @@ -1528,13 +1434,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: unknown group arg: g -> missing - #| - #|Usage: demo - #| - #|Options: - #| -h, --help Show help information. - #| + #|error: command definition validation failed: unknown group arg: g -> missing ), ) } noraise { @@ -1551,15 +1451,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: duplicate arg name: x - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --x - #| --y - #| + #|error: command definition validation failed: duplicate arg name: x ), ) } noraise { @@ -1576,15 +1468,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: duplicate long option: --same - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --same - #| --same - #| + #|error: command definition validation failed: duplicate long option: --same ), ) } noraise { @@ -1601,15 +1485,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: duplicate long option: --no-hello - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --[no-]hello - #| --no-hello - #| + #|error: command definition validation failed: duplicate long option: --no-hello ), ) } noraise { @@ -1626,15 +1502,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: duplicate short option: -s - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| -s, --x - #| -s, --y - #| + #|error: command definition validation failed: duplicate short option: -s ), ) } noraise { @@ -1651,14 +1519,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: arg cannot require itself: x - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --x - #| + #|error: command definition validation failed: arg cannot require itself: x ), ) } noraise { @@ -1675,14 +1536,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: arg cannot conflict with itself: x - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --x - #| + #|error: command definition validation failed: arg cannot conflict with itself: x ), ) } noraise { @@ -1699,18 +1553,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: duplicate subcommand: x - #| - #|Usage: demo [command] - #| - #|Commands: - #| x - #| x - #| help Print help for the subcommand(s). - #| - #|Options: - #| -h, --help Show help information. - #| + #|error: command definition validation failed: duplicate subcommand: x ), ) } noraise { @@ -1727,13 +1570,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: subcommand_required requires at least one subcommand - #| - #|Usage: demo - #| - #|Options: - #| -h, --help Show help information. - #| + #|error: command definition validation failed: subcommand_required requires at least one subcommand ), ) } noraise { @@ -1750,17 +1587,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: subcommand name reserved for built-in help: help (disable with disable_help_subcommand) - #| - #|Usage: demo [command] - #| - #|Commands: - #| help - #| help Print help for the subcommand(s). - #| - #|Options: - #| -h, --help Show help information. - #| + #|error: command definition validation failed: subcommand name reserved for built-in help: help (disable with disable_help_subcommand) ), ) } noraise { @@ -1822,14 +1649,7 @@ test "validation branches exposed through parse" { inspect( msg, content=( - #|error: version action requires command version text - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --v - #| + #|error: command definition validation failed: version action requires command version text ), ) } noraise { @@ -1924,13 +1744,7 @@ test "group validation catches unknown requires target" { inspect( msg, content=( - #|error: unknown group requires target: g -> missing - #| - #|Usage: demo - #| - #|Options: - #| -h, --help Show help information. - #| + #|error: command definition validation failed: unknown group requires target: g -> missing ), ) } noraise { @@ -1950,13 +1764,7 @@ test "group validation catches unknown conflicts_with target" { inspect( msg, content=( - #|error: unknown group conflicts_with target: g -> missing - #| - #|Usage: demo - #| - #|Options: - #| -h, --help Show help information. - #| + #|error: command definition validation failed: unknown group conflicts_with target: g -> missing ), ) } noraise { @@ -2063,14 +1871,7 @@ test "arg validation catches unknown requires target" { inspect( msg, content=( - #|error: unknown requires target: mode -> missing - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --mode - #| + #|error: command definition validation failed: unknown requires target: mode -> missing ), ) } noraise { @@ -2089,14 +1890,7 @@ test "arg validation catches unknown conflicts_with target" { inspect( msg, content=( - #|error: unknown conflicts_with target: mode -> missing - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --mode - #| + #|error: command definition validation failed: unknown conflicts_with target: mode -> missing ), ) } noraise { @@ -2780,18 +2574,7 @@ test "child local arg shadowing inherited global is rejected at build time" { inspect( msg, content=( - #|error: arg 'mode' shadows an inherited global; rename the arg or mark it global - #| - #|Usage: demo [options] [command] - #| - #|Commands: - #| run - #| help Print help for the subcommand(s). - #| - #|Options: - #| -h, --help Show help information. - #| --mode [env: MODE] [default: safe] - #| + #|error: command definition validation failed: arg 'mode' shadows an inherited global; rename the arg or mark it global ), ) } noraise { @@ -2914,18 +2697,7 @@ test "global override with incompatible inherited type is rejected" { inspect( msg, content=( - #|error: global arg 'mode' is incompatible with inherited global definition - #| - #|Usage: demo --mode [command] - #| - #|Commands: - #| run - #| help Print help for the subcommand(s). - #| - #|Options: - #| -h, --help Show help information. - #| --mode - #| + #|error: command definition validation failed: global arg 'mode' is incompatible with inherited global definition ), ) } noraise { @@ -2946,18 +2718,7 @@ test "child local long alias collision with inherited global is rejected" { inspect( msg, content=( - #|error: arg 'local' long option --verbose conflicts with inherited global 'verbose' - #| - #|Usage: demo [options] [command] - #| - #|Commands: - #| run - #| help Print help for the subcommand(s). - #| - #|Options: - #| -h, --help Show help information. - #| --verbose - #| + #|error: command definition validation failed: arg 'local' long option --verbose conflicts with inherited global 'verbose' ), ) } noraise { @@ -2976,18 +2737,7 @@ test "child local short alias collision with inherited global is rejected" { inspect( msg, content=( - #|error: arg 'local' short option -v conflicts with inherited global 'verbose' - #| - #|Usage: demo [options] [command] - #| - #|Commands: - #| run - #| help Print help for the subcommand(s). - #| - #|Options: - #| -h, --help Show help information. - #| -v, --verbose - #| + #|error: command definition validation failed: arg 'local' short option -v conflicts with inherited global 'verbose' ), ) } noraise { @@ -3133,21 +2883,30 @@ test "global override with different negatable setting is rejected" { inspect( msg, content=( - #|error: global arg 'verbose' is incompatible with inherited global definition - #| - #|Usage: demo [options] [command] - #| - #|Commands: - #| run - #| help Print help for the subcommand(s). - #| - #|Options: - #| -h, --help Show help information. - #| --[no-]verbose - #| + #|error: command definition validation failed: global arg 'verbose' is incompatible with inherited global definition ), ) } noraise { _ => panic() } } + +///| +test "positional range 0..1 renders as single optional value" { + let cmd = @argparse.Command("demo", positionals=[ + Positional("x", num_args=ValueRange(lower=0, upper=1)), + ]) + inspect( + cmd.render_help(), + content=( + #|Usage: demo [x] + #| + #|Arguments: + #| x + #| + #|Options: + #| -h, --help Show help information. + #| + ), + ) +} diff --git a/argparse/argparse_test.mbt b/argparse/argparse_test.mbt index eb26ce8b1..a92de05db 100644 --- a/argparse/argparse_test.mbt +++ b/argparse/argparse_test.mbt @@ -197,7 +197,7 @@ test "subcommand parse errors include subcommand help" { } ///| -test "build errors are surfaced as ArgError message with help" { +test "build errors are surfaced as validation failure message" { let cmd = @argparse.Command("demo", flags=[ Flag("fast", long="fast", requires=["missing"]), ]) @@ -207,14 +207,7 @@ test "build errors are surfaced as ArgError message with help" { inspect( msg, content=( - #|error: unknown requires target: fast -> missing - #| - #|Usage: demo [options] - #| - #|Options: - #| -h, --help Show help information. - #| --fast - #| + #|error: command definition validation failed: unknown requires target: fast -> missing ), ) } noraise { diff --git a/argparse/command.mbt b/argparse/command.mbt index ffc81f5c9..1970447db 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -113,8 +113,10 @@ pub fn Command::render_help(self : Command) -> String { /// Behavior: /// - Help/version requests print output immediately and terminate with exit code /// `0`. -/// - Parse/build failures raise `ArgError::Message` where the payload includes -/// the error text and full contextual help output. +/// - Parse failures raise `ArgError::Message` where the payload includes the +/// error text and full contextual help output. +/// - Command-definition validation failures raise `ArgError::Message` with a +/// validation-failure message (without appended help). /// /// Value precedence is `argv > env > default_values`. #as_free_fn diff --git a/argparse/error.mbt b/argparse/error.mbt index 40ebf7496..26b5f4c5d 100644 --- a/argparse/error.mbt +++ b/argparse/error.mbt @@ -16,7 +16,8 @@ /// Unified error surface exposed by argparse. /// /// `Message` is display-ready text intended for end users. -/// For parse/build failures it already includes the contextual help message. +/// Parse failures include contextual help; command-definition validation +/// failures are returned as plain validation messages. pub suberror ArgError { Message(String) } @@ -88,13 +89,6 @@ priv suberror ArgBuildError { Unsupported(String) } -///| -fn ArgBuildError::arg_build_error_message(self : ArgBuildError) -> String { - match self { - Unsupported(msg) => "error: \{msg}" - } -} - ///| /// Internal control-flow event for displaying help. priv suberror DisplayHelp { diff --git a/argparse/parser.mbt b/argparse/parser.mbt index d2c3daba8..279f95cd2 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -223,23 +223,6 @@ fn arg_error_for_parse_failure( } } -///| -fn arg_error_for_build_failure( - err : ArgBuildError, - cmd : Command, - inherited_globals : Array[Arg], - command_path : String, -) -> ArgError { - Message( - format_error_with_help( - err.arg_build_error_message(), - cmd, - inherited_globals, - command_path, - ), - ) -} - ///| fn parse_command( cmd : Command, @@ -325,11 +308,8 @@ fn parse_command( command_path, ) Unsupported(msg) => - raise arg_error_for_build_failure( - Unsupported(msg), - cmd, - inherited_globals, - command_path, + raise ArgError::Message( + "error: command definition validation failed: \{msg}", ) err => raise err } @@ -492,9 +472,10 @@ fn parse_command_impl( } if arg.has_prefix("-") && arg != "-" { // Parse short groups like `-abc` and short values like `-c3`. - let mut pos = 1 - while pos < arg.length() { - let short = arg.get_char(pos).unwrap() + let chars = arg.iter() + ignore(chars.next()) + let mut consumed_next = false + while chars.next() is Some(short) { if short == 'h' && builtin_help_short { raise_context_help(cmd, inherited_globals, command_path) } @@ -507,8 +488,8 @@ fn parse_command_impl( } if spec.info is (OptionInfo(_) | PositionalInfo(_)) { check_duplicate_set_occurrence(matches, spec) - if pos + 1 < arg.length() { - let rest = arg.unsafe_substring(start=pos + 1, end=arg.length()) + let rest = String::from_iter(chars) + if rest != "" { let inline = match rest.strip_prefix("=") { Some(view) => view.to_string() None => rest @@ -523,8 +504,8 @@ fn parse_command_impl( short_index, ) if can_take_next { - i = i + 1 - assign_value(matches, spec, argv[i], Argv) + consumed_next = true + assign_value(matches, spec, argv[i + 1], Argv) } else { raise ArgParseError::MissingValue("-\{short}") } @@ -543,9 +524,8 @@ fn parse_command_impl( _ => apply_flag(matches, spec, Argv) } } - pos = pos + short.utf16_len() } - i = i + 1 + i = i + 1 + (if consumed_next { 1 } else { 0 }) continue } if help_subcommand_enabled(cmd) && arg == "help" { From a1c5b1859587f655941635a0e1ad6d9907384375 Mon Sep 17 00:00:00 2001 From: zihang Date: Mon, 2 Mar 2026 17:04:47 +0800 Subject: [PATCH 35/40] fix(argparse): honor global overrides across subcommand parsing --- argparse/argparse_blackbox_test.mbt | 74 +++++++++++++++++++++++++++++ argparse/parser.mbt | 39 ++++++++++++--- argparse/parser_globals_merge.mbt | 35 ++++++++++++++ argparse/parser_lookup.mbt | 5 +- argparse/parser_lookup_wbtest.mbt | 42 ++++++++++++++++ argparse/parser_values.mbt | 2 +- 6 files changed, 189 insertions(+), 8 deletions(-) create mode 100644 argparse/parser_lookup_wbtest.mbt diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 87e2e43e3..6ec6222ee 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -2553,6 +2553,80 @@ test "child global arg with inherited global name updates parent global" { ) } +///| +test "child global override env/default win over inherited definition" { + let cmd = @argparse.Command( + "demo", + options=[ + Option( + "mode", + long="mode", + env="ROOT_MODE", + default_values=["safe"], + global=true, + ), + ], + subcommands=[ + Command("run", options=[ + Option( + "mode", + long="mode", + env="RUN_MODE", + default_values=["fast"], + global=true, + ), + ]), + ], + ) + + let from_env = cmd.parse(argv=["run"], env={ + "ROOT_MODE": "root-env", + "RUN_MODE": "run-env", + }) catch { + _ => panic() + } + assert_true(from_env.values is { "mode": ["run-env"], .. }) + assert_true(from_env.sources is { "mode": Env, .. }) + assert_true( + from_env.subcommand is Some(("run", sub)) && + sub.values is { "mode": ["run-env"], .. } && + sub.sources is { "mode": Env, .. }, + ) + + let from_default = cmd.parse(argv=["run"], env={}) catch { _ => panic() } + assert_true(from_default.values is { "mode": ["fast"], .. }) + assert_true(from_default.sources is { "mode": Default, .. }) + assert_true( + from_default.subcommand is Some(("run", sub)) && + sub.values is { "mode": ["fast"], .. } && + sub.sources is { "mode": Default, .. }, + ) +} + +///| +test "inherited argv global satisfies child required global override" { + let cmd = @argparse.Command( + "demo", + options=[Option("mode", long="mode", global=true)], + subcommands=[ + Command("run", options=[ + Option("mode", long="mode", required=true, global=true), + ]), + ], + ) + + let parsed = cmd.parse(argv=["--mode", "fast", "run"], env=empty_env()) catch { + _ => panic() + } + assert_true(parsed.values is { "mode": ["fast"], .. }) + assert_true(parsed.sources is { "mode": Argv, .. }) + assert_true( + parsed.subcommand is Some(("run", sub)) && + sub.values is { "mode": ["fast"], .. } && + sub.sources is { "mode": Argv, .. }, + ) +} + ///| test "child local arg shadowing inherited global is rejected at build time" { try diff --git a/argparse/parser.mbt b/argparse/parser.mbt index 279f95cd2..b752af54d 100644 --- a/argparse/parser.mbt +++ b/argparse/parser.mbt @@ -101,6 +101,21 @@ fn merge_global_defs( merged } +///| +fn env_resolution_args( + inherited_globals : Array[Arg], + args : Array[Arg], +) -> Array[Arg] { + let merged_globals = merge_global_defs( + inherited_globals, + collect_globals(args), + ) + let local_only = args.filter(arg => { + !(arg.global && arg.info is (FlagInfo(_) | OptionInfo(_))) + }) + merged_globals + local_only +} + ///| fn format_error_with_help( msg : String, @@ -232,10 +247,11 @@ fn parse_command( inherited_version_long : Map[String, String], inherited_version_short : Map[Char, String], command_path : String, + seed_matches? : Matches = new_matches_parse_state(), ) -> Matches raise { parse_command_impl( cmd, argv, env, inherited_globals, inherited_version_long, inherited_version_short, - command_path, + command_path, seed_matches, ) catch { UnknownArgument(arg, hint) => raise arg_error_for_parse_failure( @@ -324,6 +340,7 @@ fn parse_command_impl( inherited_version_long : Map[String, String], inherited_version_short : Map[Char, String], command_path : String, + seed_matches : Matches, ) -> Matches raise { match cmd.build_error { Some(err) => raise err @@ -335,7 +352,7 @@ fn parse_command_impl( if cmd.arg_required_else_help && argv.length() == 0 { raise_context_help(cmd, inherited_globals, command_path) } - let matches = new_matches_parse_state() + let matches = seed_matches let globals_here = collect_globals(args) let child_globals = merge_global_defs(inherited_globals, globals_here) let child_version_long = inherited_version_long.copy() @@ -549,17 +566,27 @@ fn parse_command_impl( } else { "\{command_path} \{sub.name}" } + let child_local_non_globals = collect_non_global_names(sub.args) + let child_seed = seed_child_globals_from_parent( + matches, child_globals, child_local_non_globals, + ) let sub_matches = parse_command( - sub, rest, env, child_globals, child_version_long, child_version_short, sub_path, + sub, + rest, + env, + child_globals, + child_version_long, + child_version_short, + sub_path, + seed_matches=child_seed, ) - let child_local_non_globals = collect_non_global_names(sub.args) matches.parsed_subcommand = Some((sub.name, sub_matches)) // Merge argv-provided globals from the subcommand parse into the parent // so globals work even when they appear after the subcommand name. merge_globals_from_child( matches, sub_matches, child_globals, child_local_non_globals, ) - let env_args = inherited_globals + args + let env_args = env_resolution_args(inherited_globals, args) let parent_matches = finalize_matches( cmd, args, groups, matches, positionals, positional_values, env_args, env, ) @@ -581,7 +608,7 @@ fn parse_command_impl( positional_arg_found = true i = i + 1 } - let env_args = inherited_globals + args + let env_args = env_resolution_args(inherited_globals, args) let final_matches = finalize_matches( cmd, args, groups, matches, positionals, positional_values, env_args, env, ) diff --git a/argparse/parser_globals_merge.mbt b/argparse/parser_globals_merge.mbt index e36978778..003527e4a 100644 --- a/argparse/parser_globals_merge.mbt +++ b/argparse/parser_globals_merge.mbt @@ -198,6 +198,41 @@ fn merge_global_flag_from_child( } } +///| +fn seed_child_globals_from_parent( + parent : Matches, + globals : Array[Arg], + child_local_non_globals : @set.Set[String], +) -> Matches { + let seed = new_matches_parse_state() + let seen : @set.Set[String] = @set.new() + for arg in globals { + let name = arg.name + if child_local_non_globals.contains(name) || !seen.add_and_check(name) { + continue + } + if arg.info is (OptionInfo(_) | PositionalInfo(_)) { + if parent.values.get(name) is Some(_) && + parent.value_sources.get(name) is Some(Argv) { + // Presence marker only: avoid duplicating parent argv values in child. + seed.values[name] = [] + seed.value_sources[name] = Argv + } + continue + } + if parent.flags.get(name) is Some(v) && + parent.flag_sources.get(name) is Some(Argv) { + seed.flags[name] = v + seed.flag_sources[name] = Argv + if arg.info is FlagInfo(action=Count, ..) { + // Presence marker only: count is rebuilt from child argv tokens. + seed.counts[name] = 0 + } + } + } + seed +} + ///| fn merge_globals_from_child( parent : Matches, diff --git a/argparse/parser_lookup.mbt b/argparse/parser_lookup.mbt index 839920d3f..f3b4f62bf 100644 --- a/argparse/parser_lookup.mbt +++ b/argparse/parser_lookup.mbt @@ -80,7 +80,10 @@ fn resolve_help_target( guard subs.iter().find_first(sub => sub.name == name) is Some(sub) else { raise InvalidArgument("unknown subcommand: \{name}") } - current_globals = current_globals + collect_globals(current.args) + current_globals = merge_global_defs( + current_globals, + collect_globals(current.args), + ) current = sub current_path = if current_path == "" { sub.name diff --git a/argparse/parser_lookup_wbtest.mbt b/argparse/parser_lookup_wbtest.mbt new file mode 100644 index 000000000..5b1963788 --- /dev/null +++ b/argparse/parser_lookup_wbtest.mbt @@ -0,0 +1,42 @@ +// 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. + +///| +test "resolve_help_target merges inherited globals by name" { + let leaf = Command("leaf") + let mid = Command( + "mid", + options=[Option("mode", long="mid-mode", global=true)], + subcommands=[leaf], + ) + let root = Command( + "demo", + options=[Option("mode", long="root-mode", global=true)], + subcommands=[mid], + ) + + let (target, globals, path) = resolve_help_target( + root, + ["mid", "leaf"], + true, + true, + [], + "demo", + ) catch { + _ => panic() + } + let help = render_help_for_context(target, globals, path) + assert_true(help.contains("--mid-mode ")) + assert_true(!help.contains("--root-mode ")) +} diff --git a/argparse/parser_values.mbt b/argparse/parser_values.mbt index ac7b80281..f45a831d9 100644 --- a/argparse/parser_values.mbt +++ b/argparse/parser_values.mbt @@ -166,7 +166,7 @@ fn check_duplicate_set_occurrence( guard arg.info is (OptionInfo(action=Set, ..) | PositionalInfo(_)) else { return } - if matches.values.get(arg.name) is Some(_) { + if matches.values.get(arg.name) is Some(values) && values.length() > 0 { raise InvalidArgument( "argument '\{option_conflict_label(arg)}' cannot be used multiple times", ) From 49b58a6a61dfdba04131ed89a2cfa1fdc5970309 Mon Sep 17 00:00:00 2001 From: zihang Date: Mon, 2 Mar 2026 17:19:49 +0800 Subject: [PATCH 36/40] fix(argparse): handle ffi and code review --- argparse/argparse_blackbox_test.mbt | 23 +++++++++++++++++++++++ argparse/command.mbt | 10 ++++------ argparse/runtime_exit_native.mbt | 2 +- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 6ec6222ee..30d29dd6c 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -2531,6 +2531,29 @@ test "global value from child default is merged back to parent" { ) } +///| +test "env-only global is propagated to nested subcommand matches" { + let cmd = @argparse.Command( + "demo", + options=[Option("level", long="", env="LEVEL", global=true)], + subcommands=[Command("run", subcommands=[Command("leaf")])], + ) + + let parsed = cmd.parse(argv=["run", "leaf"], env={ "LEVEL": "5" }) catch { + _ => panic() + } + assert_true(parsed.values is { "level": ["5"], .. }) + assert_true(parsed.sources is { "level": Env, .. }) + assert_true( + parsed.subcommand is Some(("run", sub_run)) && + sub_run.values is { "level": ["5"], .. } && + sub_run.sources is { "level": Env, .. } && + sub_run.subcommand is Some(("leaf", sub_leaf)) && + sub_leaf.values is { "level": ["5"], .. } && + sub_leaf.sources is { "level": Env, .. }, + ) +} + ///| test "child global arg with inherited global name updates parent global" { let cmd = @argparse.Command( diff --git a/argparse/command.mbt b/argparse/command.mbt index 1970447db..9d240356e 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -172,12 +172,10 @@ fn build_matches( } } } - let child_globals = inherited_globals + - cmd.args.filter(arg => { - arg.global && - arg.info is (OptionInfo(long~, short~, ..) | FlagInfo(long~, short~, ..)) && - (long is Some(_) || short is Some(_)) - }) + let child_globals = merge_global_defs( + inherited_globals, + collect_globals(cmd.args), + ) let subcommand = match raw.parsed_subcommand { Some((name, sub_raw)) => diff --git a/argparse/runtime_exit_native.mbt b/argparse/runtime_exit_native.mbt index e98d0117b..86a6648e8 100644 --- a/argparse/runtime_exit_native.mbt +++ b/argparse/runtime_exit_native.mbt @@ -14,7 +14,7 @@ ///| #cfg(target="native") -extern "c" fn runtime_native_exit(code : Int) -> Unit = "exit" +extern "c" fn runtime_native_exit(code : Int) = "exit" ///| #cfg(target="native") From 2c4a689da76ce32e231028bdc4ae1dcbfa546e03 Mon Sep 17 00:00:00 2001 From: Hongbo Zhang Date: Tue, 3 Mar 2026 08:56:00 +0800 Subject: [PATCH 37/40] change Option to Optional --- argparse/README.mbt.md | 16 +-- argparse/arg_spec.mbt | 9 +- argparse/argparse_blackbox_test.mbt | 145 ++++++++++++++-------------- argparse/argparse_test.mbt | 28 +++--- argparse/command.mbt | 6 +- argparse/parser_lookup_wbtest.mbt | 4 +- 6 files changed, 107 insertions(+), 101 deletions(-) diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index d5a66b612..1abcea23d 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -14,7 +14,9 @@ small, predictable feature set. ///| test "basic option + positional success snapshot" { let matches = @argparse.parse( - Command("demo", options=[Option("name")], positionals=[Positional("target")]), + Command("demo", options=[Optional("name")], positionals=[ + Positional("target"), + ]), argv=["--name", "alice", "file.txt"], env={}, ) @@ -28,7 +30,7 @@ test "basic option + positional success snapshot" { ///| test "basic option + positional failure snapshot" { - let cmd = @argparse.Command("demo", options=[Option("name")], positionals=[ + let cmd = @argparse.Command("demo", options=[Optional("name")], positionals=[ Positional("target"), ]) try cmd.parse(argv=["--bad"], env={}) catch { @@ -150,7 +152,7 @@ Value precedence is `argv > env > default_values`. ///| test "value source precedence snapshots" { let cmd = @argparse.Command("demo", options=[ - Option("level", env="LEVEL", default_values=["1"]), + Optional("level", env="LEVEL", default_values=["1"]), ]) inspect( @@ -214,7 +216,7 @@ test "value source precedence snapshots" { ```mbt check ///| test "option input forms snapshot" { - let cmd = @argparse.Command("demo", options=[Option("count", short='c')]) + let cmd = @argparse.Command("demo", options=[Optional("count", short='c')]) inspect( cmd.render_help(), @@ -285,8 +287,8 @@ error and full contextual help. ///| test "requires relationship success and failure snapshots" { let cmd = @argparse.Command("demo", options=[ - Option("mode", requires=["config"]), - Option("config"), + Optional("mode", requires=["config"]), + Optional("config"), ]) let ok = try! cmd.parse(argv=["--mode", "fast", "--config", "cfg.toml"], env={}) @@ -476,7 +478,7 @@ test "bounded non-last positional failure snapshot" { ///| let cmd : @argparse.Command = Command( "wrap", - options=[Option("config"), Option("mode")], + options=[Optional("config"), Optional("mode")], positionals=[ Positional( "child_argv", diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt index 752180913..c4255a42c 100644 --- a/argparse/arg_spec.mbt +++ b/argparse/arg_spec.mbt @@ -143,10 +143,11 @@ pub fn Flag::new( ///| /// Declarative option constructor wrapper. -pub struct Option { +pub struct Optional { priv arg : Arg /// Create an option argument. + // FIXME(upstram) rename does not work here fn new( name : StringView, short? : Char, @@ -161,7 +162,7 @@ pub struct Option { required? : Bool, global? : Bool, hidden? : Bool, - ) -> Option + ) -> Optional } ///| @@ -175,7 +176,7 @@ pub struct Option { /// - `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. -pub fn Option::new( +pub fn Optional::new( name : StringView, short? : Char, long? : StringView = name, @@ -189,7 +190,7 @@ pub fn Option::new( required? : Bool = false, global? : Bool = false, hidden? : Bool = false, -) -> Option { +) -> Optional { let name = name.to_string() let long = if long == "" { None } else { Some(long.to_string()) } let about = about.map(v => v.to_string()) diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 30d29dd6c..43dd9719d 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -31,7 +31,7 @@ test "render help snapshot with groups and hidden entries" { Flag("cache", long="cache", negatable=true, about="cache"), ], options=[ - Option( + Optional( "path", short='p', long="path", @@ -89,7 +89,7 @@ test "render help conversion coverage snapshot" { ), ], options=[ - Option( + Optional( "opt", short='o', about="opt", @@ -148,7 +148,7 @@ test "global option merges parent and child values" { let cmd = @argparse.Command( "demo", options=[ - Option("profile", short='p', long="profile", action=Append, global=true), + Optional("profile", short='p', long="profile", action=Append, global=true), ], subcommands=[child], ) @@ -172,8 +172,8 @@ test "global requires is validated after parent-child merge" { let cmd = @argparse.Command( "demo", options=[ - Option("mode", long="mode", requires=["config"], global=true), - Option("config", long="config", global=true), + Optional("mode", long="mode", requires=["config"], global=true), + Optional("config", long="config", global=true), ], subcommands=[Command("run")], ) @@ -197,7 +197,7 @@ test "global append keeps parent argv over child env/default" { let cmd = @argparse.Command( "demo", options=[ - Option( + Optional( "profile", long="profile", action=Append, @@ -229,7 +229,7 @@ test "global scalar keeps parent argv over child env/default" { let cmd = @argparse.Command( "demo", options=[ - Option( + Optional( "profile", long="profile", env="PROFILE", @@ -573,7 +573,7 @@ test "unknown argument suggestions are exposed" { ///| test "long and short value parsing branches" { let cmd = @argparse.Command("demo", options=[ - Option("count", short='c', long="count"), + Optional("count", short='c', long="count"), ]) let long_inline = cmd.parse(argv=["--count=2"], env=empty_env()) catch { @@ -633,7 +633,7 @@ test "long and short value parsing branches" { ///| test "append option action is publicly selectable" { let cmd = @argparse.Command("demo", options=[ - Option("tag", long="tag", action=Append), + Optional("tag", long="tag", action=Append), ]) let appended = cmd.parse(argv=["--tag", "a", "--tag", "b"], env=empty_env()) catch { _ => panic() @@ -647,7 +647,7 @@ test "negation parsing and invalid negation forms" { let cmd = @argparse.Command( "demo", flags=[Flag("cache", long="cache", negatable=true)], - options=[Option("path", long="path")], + options=[Optional("path", long="path")], ) let off = cmd.parse(argv=["--no-cache"], env=empty_env()) catch { @@ -936,8 +936,8 @@ test "env parsing for settrue setfalse count and invalid values" { ///| test "defaults and value range helpers through public API" { let defaults = @argparse.Command("demo", options=[ - Option("mode", long="mode", action=Append, default_values=["a", "b"]), - Option("one", long="one", default_values=["x"]), + Optional("mode", long="mode", action=Append, default_values=["a", "b"]), + Optional("one", long="one", default_values=["x"]), ]) let by_default = defaults.parse(argv=[], env=empty_env()) catch { _ => panic() @@ -946,7 +946,7 @@ test "defaults and value range helpers through public API" { assert_true(by_default.sources is { "mode": Default, "one": Default, .. }) let upper_only = @argparse.Command("demo", options=[ - Option("tag", long="tag", action=Append), + Optional("tag", long="tag", action=Append), ]) let upper_parsed = upper_only.parse( argv=["--tag", "a", "--tag", "b", "--tag", "c"], @@ -956,7 +956,9 @@ test "defaults and value range helpers through public API" { } assert_true(upper_parsed.values is { "tag": ["a", "b", "c"], .. }) - let lower_only = @argparse.Command("demo", options=[Option("tag", long="tag")]) + let lower_only = @argparse.Command("demo", options=[ + Optional("tag", long="tag"), + ]) let lower_absent = lower_only.parse(argv=[], env=empty_env()) catch { _ => panic() } @@ -992,7 +994,7 @@ test "defaults and value range helpers through public API" { ///| test "options consume exactly one value per occurrence" { - let cmd = @argparse.Command("demo", options=[Option("tag", long="tag")]) + let cmd = @argparse.Command("demo", options=[Optional("tag", long="tag")]) let parsed = cmd.parse(argv=["--tag", "a"], env=empty_env()) catch { _ => panic() } @@ -1021,7 +1023,7 @@ test "options consume exactly one value per occurrence" { ///| test "set options reject duplicate occurrences" { - let cmd = @argparse.Command("demo", options=[Option("mode", long="mode")]) + let cmd = @argparse.Command("demo", options=[Optional("mode", long="mode")]) try cmd.parse(argv=["--mode", "a", "--mode", "b"], env=empty_env()) catch { Message(msg) => inspect( @@ -1045,7 +1047,7 @@ test "set options reject duplicate occurrences" { ///| test "flag and option args require short or long names" { try - @argparse.Command("demo", options=[Option("input", long="")]).parse( + @argparse.Command("demo", options=[Optional("input", long="")]).parse( argv=[], env=empty_env(), ) @@ -1082,7 +1084,7 @@ test "flag and option args require short or long names" { ///| test "append options collect values across repeated occurrences" { let cmd = @argparse.Command("demo", options=[ - Option("arg", long="arg", action=Append), + Optional("arg", long="arg", action=Append), ]) let parsed = cmd.parse(argv=["--arg", "x", "--arg", "y"], env=empty_env()) catch { _ => panic() @@ -1094,7 +1096,7 @@ test "append options collect values across repeated occurrences" { ///| test "option parsing stops at the next option token" { let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")], options=[ - Option("arg", short='a', long="arg"), + Optional("arg", short='a', long="arg"), ]) let stopped = cmd.parse(argv=["--arg", "x", "--verbose"], env=empty_env()) catch { @@ -1147,7 +1149,7 @@ test "option parsing stops at the next option token" { ///| test "options always require a value" { let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")], options=[ - Option("opt", long="opt"), + Optional("opt", long="opt"), ]) try cmd.parse(argv=["--opt", "--verbose"], env=empty_env()) catch { Message(msg) => @@ -1170,7 +1172,7 @@ test "options always require a value" { } let zero_value_required = @argparse.Command("demo", options=[ - Option("opt", long="opt", required=true), + Optional("opt", long="opt", required=true), ]).parse(argv=["--opt", "x"], env=empty_env()) catch { _ => panic() } @@ -1180,7 +1182,7 @@ test "options always require a value" { ///| test "option values reject hyphen tokens unless allow_hyphen_values is enabled" { let strict = @argparse.Command("demo", options=[ - Option("pattern", long="pattern"), + Optional("pattern", long="pattern"), ]) let mut rejected = false try strict.parse(argv=["--pattern", "-file"], env=empty_env()) catch { @@ -1206,7 +1208,7 @@ test "option values reject hyphen tokens unless allow_hyphen_values is enabled" assert_true(rejected) let permissive = @argparse.Command("demo", options=[ - Option("pattern", long="pattern", allow_hyphen_values=true), + Optional("pattern", long="pattern", allow_hyphen_values=true), ]) let parsed = permissive.parse(argv=["--pattern", "-file"], env=empty_env()) catch { _ => panic() @@ -1278,7 +1280,7 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", options=[Option("x", long="x")]).parse( + @argparse.Command("demo", options=[Optional("x", long="x")]).parse( argv=["--x", "a", "b"], env=empty_env(), ) @@ -1303,7 +1305,7 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", options=[ - Option("x", long="x", default_values=["a", "b"]), + Optional("x", long="x", default_values=["a", "b"]), ]).parse(argv=[], env=empty_env()) catch { Message(msg) => @@ -1443,8 +1445,8 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", options=[ - Option("x", long="x"), - Option("x", long="y"), + Optional("x", long="x"), + Optional("x", long="y"), ]).parse(argv=[], env=empty_env()) catch { Message(msg) => @@ -1460,8 +1462,8 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", options=[ - Option("x", long="same"), - Option("y", long="same"), + Optional("x", long="same"), + Optional("y", long="same"), ]).parse(argv=[], env=empty_env()) catch { Message(msg) => @@ -1494,8 +1496,8 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", options=[ - Option("x", short='s'), - Option("y", short='s'), + Optional("x", short='s'), + Optional("y", short='s'), ]).parse(argv=[], env=empty_env()) catch { Message(msg) => @@ -1778,7 +1780,7 @@ test "group requires/conflicts can target argument names" { "demo", groups=[ArgGroup("mode", args=["fast"], requires=["config"])], flags=[Flag("fast", long="fast")], - options=[Option("config", long="config")], + options=[Optional("config", long="config")], ) let ok = requires_cmd.parse( @@ -1817,7 +1819,7 @@ test "group requires/conflicts can target argument names" { "demo", groups=[ArgGroup("mode", args=["fast"], conflicts_with=["config"])], flags=[Flag("fast", long="fast")], - options=[Option("config", long="config")], + options=[Optional("config", long="config")], ) try @@ -1864,7 +1866,7 @@ test "group without members has no parse effect" { test "arg validation catches unknown requires target" { try @argparse.Command("demo", options=[ - Option("mode", long="mode", requires=["missing"]), + Optional("mode", long="mode", requires=["missing"]), ]).parse(argv=["--mode", "fast"], env=empty_env()) catch { Message(msg) => @@ -1883,7 +1885,7 @@ test "arg validation catches unknown requires target" { test "arg validation catches unknown conflicts_with target" { try @argparse.Command("demo", options=[ - Option("mode", long="mode", conflicts_with=["missing"]), + Optional("mode", long="mode", conflicts_with=["missing"]), ]).parse(argv=["--mode", "fast"], env=empty_env()) catch { Message(msg) => @@ -1920,7 +1922,7 @@ test "help rendering edge paths stay stable" { assert_true(required_help.has_prefix("Usage: demo ")) let short_only_builtin = @argparse.Command("demo", options=[ - Option("helpopt", long="help"), + Optional("helpopt", long="help"), ]) let short_only_text = short_only_builtin.render_help() assert_true(short_only_text.has_prefix("Usage: demo")) @@ -1976,7 +1978,7 @@ test "help rendering edge paths stay stable" { ///| test "unified error message formatting remains stable" { - let cmd = @argparse.Command("demo", options=[Option("tag", long="tag")]) + let cmd = @argparse.Command("demo", options=[Optional("tag", long="tag")]) try cmd.parse(argv=["--oops"], env=empty_env()) catch { Message(msg) => @@ -2019,16 +2021,15 @@ test "unified error message formatting remains stable" { ///| test "options require one value per occurrence" { - let with_value = @argparse.Command("demo", options=[Option("tag", long="tag")]).parse( - argv=["--tag", "x"], - env=empty_env(), - ) catch { + let with_value = @argparse.Command("demo", options=[ + Optional("tag", long="tag"), + ]).parse(argv=["--tag", "x"], env=empty_env()) catch { _ => panic() } assert_true(with_value.values is { "tag": ["x"], .. }) try - @argparse.Command("demo", options=[Option("tag", long="tag")]).parse( + @argparse.Command("demo", options=[Optional("tag", long="tag")]).parse( argv=["--tag"], env=empty_env(), ) @@ -2055,7 +2056,7 @@ test "options require one value per occurrence" { ///| test "short options require one value before next option token" { let cmd = @argparse.Command("demo", flags=[Flag("verbose", short='v')], options=[ - Option("x", short='x'), + Optional("x", short='x'), ]) let ok = cmd.parse(argv=["-x", "a", "-v"], env=empty_env()) catch { _ => panic() @@ -2194,7 +2195,7 @@ test "subcommand help puts required options in usage" { Command( "run", about="Run a file", - options=[Option("mode", short='m', required=true)], + options=[Optional("mode", short='m', required=true)], positionals=[Positional("file", num_args=@argparse.ValueRange::single())], ), ]) @@ -2227,7 +2228,7 @@ test "subcommand help puts required options in usage" { ///| test "required and env-fed ranged values validate after parsing" { let required_cmd = @argparse.Command("demo", options=[ - Option("input", long="input", required=true), + Optional("input", long="input", required=true), ]) try required_cmd.parse(argv=[], env=empty_env()) catch { Message(msg) => @@ -2249,7 +2250,7 @@ test "required and env-fed ranged values validate after parsing" { } let env_min_cmd = @argparse.Command("demo", options=[ - Option("pair", long="pair", env="PAIR"), + Optional("pair", long="pair", env="PAIR"), ]) let env_value = env_min_cmd.parse(argv=[], env={ "PAIR": "one" }) catch { _ => panic() @@ -2348,7 +2349,7 @@ test "options with allow_hyphen_values accept option-like single values" { Flag("cache", long="cache", negatable=true), Flag("quiet", short='q'), ], - options=[Option("arg", long="arg", allow_hyphen_values=true)], + options=[Optional("arg", long="arg", allow_hyphen_values=true)], ) let known_long = cmd.parse(argv=["--arg", "--verbose"], env=empty_env()) catch { @@ -2379,7 +2380,7 @@ test "options with allow_hyphen_values accept option-like single values" { let cmd_with_rest = @argparse.Command( "demo", - options=[Option("arg", long="arg", allow_hyphen_values=true)], + options=[Optional("arg", long="arg", allow_hyphen_values=true)], positionals=[ Positional("rest", num_args=ValueRange(lower=0), allow_hyphen_values=true), ], @@ -2396,7 +2397,7 @@ test "options with allow_hyphen_values accept option-like single values" { ///| test "single-value options avoid consuming additional option values" { let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")], options=[ - Option("one", long="one"), + Optional("one", long="one"), ]) let parsed = cmd.parse(argv=["--one", "x", "--verbose"], env=empty_env()) catch { @@ -2409,7 +2410,7 @@ test "single-value options avoid consuming additional option values" { ///| test "missing option values are reported when next token is another option" { let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")], options=[ - Option("arg", long="arg"), + Optional("arg", long="arg"), ]) let ok = cmd.parse(argv=["--arg", "x", "--verbose"], env=empty_env()) catch { @@ -2441,7 +2442,7 @@ test "missing option values are reported when next token is another option" { ///| test "short-only set options use short label in duplicate errors" { - let cmd = @argparse.Command("demo", options=[Option("mode", short='m')]) + let cmd = @argparse.Command("demo", options=[Optional("mode", short='m')]) try cmd.parse(argv=["-m", "a", "-m", "b"], env=empty_env()) catch { Message(msg) => inspect( @@ -2465,7 +2466,7 @@ test "short-only set options use short label in duplicate errors" { ///| test "unknown short suggestion can be absent" { let cmd = @argparse.Command("demo", disable_help_flag=true, options=[ - Option("name", long="name"), + Optional("name", long="name"), ]) try cmd.parse(argv=["-x"], env=empty_env()) catch { @@ -2515,8 +2516,8 @@ test "global value from child default is merged back to parent" { let cmd = @argparse.Command( "demo", options=[ - Option("mode", long="mode", default_values=["safe"], global=true), - Option("unused", long="unused", global=true), + Optional("mode", long="mode", default_values=["safe"], global=true), + Optional("unused", long="unused", global=true), ], subcommands=[Command("run")], ) @@ -2535,7 +2536,7 @@ test "global value from child default is merged back to parent" { test "env-only global is propagated to nested subcommand matches" { let cmd = @argparse.Command( "demo", - options=[Option("level", long="", env="LEVEL", global=true)], + options=[Optional("level", long="", env="LEVEL", global=true)], subcommands=[Command("run", subcommands=[Command("leaf")])], ) @@ -2558,9 +2559,11 @@ test "env-only global is propagated to nested subcommand matches" { test "child global arg with inherited global name updates parent global" { let cmd = @argparse.Command( "demo", - options=[Option("mode", long="mode", default_values=["safe"], global=true)], + options=[ + Optional("mode", long="mode", default_values=["safe"], global=true), + ], subcommands=[ - Command("run", options=[Option("mode", long="mode", global=true)]), + Command("run", options=[Optional("mode", long="mode", global=true)]), ], ) @@ -2581,7 +2584,7 @@ test "child global override env/default win over inherited definition" { let cmd = @argparse.Command( "demo", options=[ - Option( + Optional( "mode", long="mode", env="ROOT_MODE", @@ -2591,7 +2594,7 @@ test "child global override env/default win over inherited definition" { ], subcommands=[ Command("run", options=[ - Option( + Optional( "mode", long="mode", env="RUN_MODE", @@ -2630,10 +2633,10 @@ test "child global override env/default win over inherited definition" { test "inherited argv global satisfies child required global override" { let cmd = @argparse.Command( "demo", - options=[Option("mode", long="mode", global=true)], + options=[Optional("mode", long="mode", global=true)], subcommands=[ Command("run", options=[ - Option("mode", long="mode", required=true, global=true), + Optional("mode", long="mode", required=true, global=true), ]), ], ) @@ -2656,7 +2659,7 @@ test "child local arg shadowing inherited global is rejected at build time" { @argparse.Command( "demo", options=[ - Option( + Optional( "mode", long="mode", env="MODE", @@ -2664,7 +2667,7 @@ test "child local arg shadowing inherited global is rejected at build time" { global=true, ), ], - subcommands=[Command("run", options=[Option("mode", long="mode")])], + subcommands=[Command("run", options=[Optional("mode", long="mode")])], ).parse(argv=["run"], env=empty_env()) catch { Message(msg) => @@ -2683,7 +2686,7 @@ test "child local arg shadowing inherited global is rejected at build time" { test "global append env value from child is merged back to parent" { let cmd = @argparse.Command( "demo", - options=[Option("tag", long="tag", action=Append, env="TAG", global=true)], + options=[Optional("tag", long="tag", action=Append, env="TAG", global=true)], subcommands=[Command("run")], ) @@ -2750,7 +2753,7 @@ test "global count negation after subcommand resets merged state" { test "global set option rejects duplicate occurrences across subcommands" { let cmd = @argparse.Command( "demo", - options=[Option("mode", long="mode", global=true)], + options=[Optional("mode", long="mode", global=true)], subcommands=[Command("run")], ) try @@ -2784,7 +2787,7 @@ test "global override with incompatible inherited type is rejected" { try @argparse.Command( "demo", - options=[Option("mode", long="mode", required=true, global=true)], + options=[Optional("mode", long="mode", required=true, global=true)], subcommands=[ Command("run", flags=[Flag("mode", long="mode", global=true)]), ], @@ -2808,7 +2811,7 @@ test "child local long alias collision with inherited global is rejected" { @argparse.Command( "demo", flags=[Flag("verbose", long="verbose", global=true)], - subcommands=[Command("run", options=[Option("local", long="verbose")])], + subcommands=[Command("run", options=[Optional("local", long="verbose")])], ).parse(argv=["run", "--verbose"], env=empty_env()) catch { Message(msg) => @@ -2827,7 +2830,7 @@ test "child local long alias collision with inherited global is rejected" { test "child local short alias collision with inherited global is rejected" { try @argparse.Command("demo", flags=[Flag("verbose", short='v', global=true)], subcommands=[ - Command("run", options=[Option("local", short='v')]), + Command("run", options=[Optional("local", short='v')]), ]).parse(argv=["run", "-v"], env=empty_env()) catch { Message(msg) => @@ -2900,7 +2903,7 @@ test "non-bmp hyphen token reports unknown argument without panic" { ///| test "option env values remain string values instead of flags" { let cmd = @argparse.Command("demo", options=[ - Option("mode", long="mode", env="MODE"), + Optional("mode", long="mode", env="MODE"), ]) let parsed = cmd.parse(argv=[], env={ "MODE": "fast" }) catch { _ => panic() } assert_true(parsed.values is { "mode": ["fast"], .. }) @@ -2939,12 +2942,12 @@ test "nested global override keeps single set value without false duplicate erro let leaf = @argparse.Command("leaf") let mid = @argparse.Command( "mid", - options=[Option("mode", long="mode", global=true)], + options=[Optional("mode", long="mode", global=true)], subcommands=[leaf], ) let root = @argparse.Command( "root", - options=[Option("mode", long="mode", global=true)], + options=[Optional("mode", long="mode", global=true)], subcommands=[mid], ) diff --git a/argparse/argparse_test.mbt b/argparse/argparse_test.mbt index a92de05db..0236576ec 100644 --- a/argparse/argparse_test.mbt +++ b/argparse/argparse_test.mbt @@ -22,7 +22,7 @@ test "declarative parse basics" { let cmd = @argparse.Command( "demo", flags=[Flag("verbose", short='v', long="verbose")], - options=[Option("count", long="count", env="COUNT")], + options=[Optional("count", long="count", env="COUNT")], positionals=[Positional("name")], ) let matches = cmd.parse(argv=["-v", "--count", "3", "alice"], env=empty_env()) catch { @@ -38,7 +38,7 @@ test "declarative parse basics" { ///| test "long defaults to name when omitted" { let cmd = @argparse.Command("demo", flags=[Flag("verbose")], options=[ - Option("count"), + Optional("count"), ]) let matches = cmd.parse(argv=["--verbose", "--count", "3"], env=empty_env()) catch { _ => panic() @@ -52,7 +52,7 @@ test "long empty string disables long alias" { let cmd = @argparse.Command( "demo", flags=[Flag("verbose", short='v', long="")], - options=[Option("count", short='c', long="")], + options=[Optional("count", short='c', long="")], ) let matches = cmd.parse(argv=["-v", "-c", "3"], env=empty_env()) catch { @@ -147,7 +147,7 @@ test "negatable flag preserves false state" { ///| test "parse failure message contains error and contextual help" { let cmd = @argparse.Command("demo", options=[ - Option("count", long="count", about="repeat count"), + Optional("count", long="count", about="repeat count"), ]) try cmd.parse(argv=["--bad"], env=empty_env()) catch { @@ -173,7 +173,7 @@ test "parse failure message contains error and contextual help" { ///| test "subcommand parse errors include subcommand help" { let cmd = @argparse.Command("demo", subcommands=[ - Command("echo", options=[Option("times", long="times")]), + Command("echo", options=[Optional("times", long="times")]), ]) try cmd.parse(argv=["echo", "--bad"], env=empty_env()) catch { @@ -247,7 +247,7 @@ test "render_help remains available for pure formatting" { "demo", about="Demo command", flags=[Flag("verbose", short='v', long="verbose")], - options=[Option("count", long="count")], + options=[Optional("count", long="count")], positionals=[Positional("name")], subcommands=[Command("echo")], ) @@ -354,8 +354,8 @@ test "parse error show is readable" { ///| test "relationships and num args" { let requires_cmd = @argparse.Command("demo", options=[ - Option("mode", long="mode", requires=["config"]), - Option("config", long="config"), + Optional("mode", long="mode", requires=["config"]), + Optional("config", long="config"), ]) try requires_cmd.parse(argv=["--mode", "fast"], env=empty_env()) catch { @@ -379,7 +379,7 @@ test "relationships and num args" { } let appended = @argparse.Command("demo", options=[ - Option("tag", long="tag", action=Append), + Optional("tag", long="tag", action=Append), ]).parse(argv=["--tag", "a", "--tag", "b", "--tag", "c"], env=empty_env()) catch { _ => panic() } @@ -537,7 +537,7 @@ test "full help snapshot" { Flag("verbose", short='v', long="verbose", about="Enable verbose mode"), ], options=[ - Option("count", long="count", about="Repeat count", default_values=["1"]), + Optional("count", long="count", about="Repeat count", default_values=["1"]), ], positionals=[Positional("name", about="Target name")], subcommands=[Command("echo", about="Echo a message")], @@ -568,7 +568,7 @@ test "full help snapshot" { ///| test "value source precedence argv env default" { let cmd = @argparse.Command("demo", options=[ - Option("level", long="level", env="LEVEL", default_values=["1"]), + Optional("level", long="level", env="LEVEL", default_values=["1"]), ]) let from_default = cmd.parse(argv=[], env=empty_env()) catch { _ => panic() } @@ -589,7 +589,7 @@ test "value source precedence argv env default" { ///| test "omitted env does not read process environment by default" { let cmd = @argparse.Command("demo", options=[ - Option("count", long="count", env="COUNT"), + Optional("count", long="count", env="COUNT"), ]) let matches = cmd.parse(argv=[]) catch { _ => panic() } assert_true(matches.values is { "count"? : None, .. }) @@ -602,8 +602,8 @@ test "options and multiple values" { let cmd = @argparse.Command( "demo", options=[ - Option("count", short='c', long="count"), - Option("tag", long="tag", action=Append), + Optional("count", short='c', long="count"), + Optional("tag", long="tag", action=Append), ], subcommands=[serve], ) diff --git a/argparse/command.mbt b/argparse/command.mbt index 9d240356e..07c4a9168 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -33,7 +33,7 @@ pub struct Command { fn new( name : StringView, flags? : ArrayView[Flag], - options? : ArrayView[Option], + options? : ArrayView[Optional], positionals? : ArrayView[Positional], subcommands? : ArrayView[Command], about? : StringView, @@ -63,7 +63,7 @@ pub struct Command { pub fn Command::new( name : StringView, flags? : ArrayView[Flag] = [], - options? : ArrayView[Option] = [], + options? : ArrayView[Optional] = [], positionals? : ArrayView[Positional] = [], subcommands? : ArrayView[Command] = [], about? : StringView, @@ -228,7 +228,7 @@ fn find_decl_subcommand(subs : Array[Command], name : String) -> Command? { ///| fn collect_args( flags : ArrayView[Flag], - options : ArrayView[Option], + options : ArrayView[Optional], positionals : ArrayView[Positional], ) -> (Array[Arg], ArgBuildError?) { let args : Array[Arg] = [] diff --git a/argparse/parser_lookup_wbtest.mbt b/argparse/parser_lookup_wbtest.mbt index 5b1963788..36642bc41 100644 --- a/argparse/parser_lookup_wbtest.mbt +++ b/argparse/parser_lookup_wbtest.mbt @@ -17,12 +17,12 @@ test "resolve_help_target merges inherited globals by name" { let leaf = Command("leaf") let mid = Command( "mid", - options=[Option("mode", long="mid-mode", global=true)], + options=[Optional("mode", long="mid-mode", global=true)], subcommands=[leaf], ) let root = Command( "demo", - options=[Option("mode", long="root-mode", global=true)], + options=[Optional("mode", long="root-mode", global=true)], subcommands=[mid], ) From 82a70fff807b2786c139519016b6b9c83387287e Mon Sep 17 00:00:00 2001 From: Hongbo Zhang Date: Tue, 3 Mar 2026 08:56:12 +0800 Subject: [PATCH 38/40] `moon info` --- argparse/pkg.generated.mbti | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/argparse/pkg.generated.mbti b/argparse/pkg.generated.mbti index 44f11ce7e..d5d4c51e3 100644 --- a/argparse/pkg.generated.mbti +++ b/argparse/pkg.generated.mbti @@ -24,9 +24,9 @@ pub fn ArgGroup::new(StringView, required? : Bool, multiple? : Bool, args? : Arr pub struct Command { // private fields - fn new(StringView, flags? : ArrayView[Flag], options? : ArrayView[Option], positionals? : ArrayView[Positional], 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]) -> Command + fn new(StringView, flags? : ArrayView[Flag], options? : ArrayView[Optional], positionals? : ArrayView[Positional], 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]) -> Command } -pub fn Command::new(StringView, flags? : ArrayView[Flag], options? : ArrayView[Option], positionals? : ArrayView[Positional], subcommands? : ArrayView[Self], 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]) -> Self +pub fn Command::new(StringView, flags? : ArrayView[Flag], options? : ArrayView[Optional], positionals? : ArrayView[Positional], subcommands? : ArrayView[Self], 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]) -> Self #as_free_fn pub fn Command::parse(Self, argv? : ArrayView[String], env? : Map[String, String]) -> Matches raise ArgError pub fn Command::render_help(Self) -> String @@ -58,13 +58,6 @@ pub struct Matches { } pub impl @debug.Debug for Matches -pub struct Option { - // private fields - - fn new(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) -> Option -} -pub fn Option::new(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) -> Self - pub(all) enum OptionAction { Set Append @@ -72,6 +65,13 @@ pub(all) enum OptionAction { pub impl Eq for OptionAction pub impl Show for OptionAction +pub struct Optional { + // private fields + + fn new(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) -> Optional +} +pub fn Optional::new(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) -> Self + pub struct Positional { // private fields From 955d20e5cc4cfa76fa9e2495eebcdde8785b7a9a Mon Sep 17 00:00:00 2001 From: zihang Date: Tue, 3 Mar 2026 11:45:26 +0800 Subject: [PATCH 39/40] refactor(argparse): hide ArgError and raise generic parse errors --- argparse/README.mbt.md | 51 +++-- argparse/argparse_blackbox_test.mbt | 308 ++++++++++++++-------------- argparse/argparse_test.mbt | 76 +++---- argparse/command.mbt | 16 +- argparse/error.mbt | 6 +- argparse/moon.pkg | 1 + argparse/pkg.generated.mbti | 6 +- 7 files changed, 241 insertions(+), 223 deletions(-) diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index 1abcea23d..71459241f 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -10,6 +10,9 @@ small, predictable feature set. ## Quick Start +For tests, snapshotting the failure message is the recommended way to cover +both the parse error and the full contextual help text. + ```mbt check ///| test "basic option + positional success snapshot" { @@ -34,9 +37,9 @@ test "basic option + positional failure snapshot" { Positional("target"), ]) try cmd.parse(argv=["--bad"], env={}) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--bad' found #| @@ -57,6 +60,22 @@ test "basic option + positional failure snapshot" { } ``` +### 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. + +```mbt nocheck +///| +async fn main { + let cmd = @argparse.Command("demo", options=[@argparse.Optional("name")], positionals=[ + @argparse.Positional("target"), + ]) + let _ = cmd.parse() +} +``` + ## Flags And Negation @@ -124,9 +143,9 @@ test "subcommand context failure snapshot" { subcommands=[Command("run")], ) try cmd.parse(argv=["run", "--oops"], env={}) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--oops' found #| @@ -280,8 +299,8 @@ test "double-dash separator snapshot" { ## Constraints And Policies -`parse` raises a single string error (`ArgError::Message`) that includes the -error and full contextual help. +`parse` raises a single display-ready error string that includes the error and +full contextual help. ```mbt check ///| @@ -300,9 +319,9 @@ test "requires relationship success and failure snapshots" { ) try cmd.parse(argv=["--mode", "fast"], env={}) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: the following required argument was not provided: 'config' (required by 'mode') #| @@ -331,9 +350,9 @@ test "arg group required and exclusive failure snapshot" { ) try cmd.parse(argv=[], env={}) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: the following required arguments were not provided: #| <--fast|--slow> @@ -362,9 +381,9 @@ test "subcommand required policy failure snapshot" { ]) try cmd.parse(argv=[], env={}) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: the following required argument was not provided: 'subcommand' #| @@ -402,9 +421,9 @@ test "conflicts_with success and failure snapshots" { ) try cmd.parse(argv=["--verbose", "--quiet"], env={}) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: conflicting arguments: verbose and quiet #| @@ -451,9 +470,9 @@ test "bounded non-last positional failure snapshot" { Positional("second", num_args=@argparse.ValueRange::single()), ]) try cmd.parse(argv=["a", "b", "c", "d"], env={}) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected value 'd' for '' found; no more were expected #| diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 43dd9719d..74150b462 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -330,9 +330,9 @@ test "subcommand cannot follow positional arguments" { Command("run"), ]) try cmd.parse(argv=["raw", "run"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: subcommand 'run' cannot be used with positional arguments #| @@ -419,9 +419,9 @@ test "help subcommand styles and errors" { ) try cmd.parse(argv=["help", "--bad"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected help argument: --bad #| @@ -441,9 +441,9 @@ test "help subcommand styles and errors" { } try cmd.parse(argv=["help", "missing"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unknown subcommand: missing #| @@ -481,9 +481,9 @@ test "subcommand help includes inherited global options" { ) try cmd.parse(argv=["echo", "--bad"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--bad' found #| @@ -509,9 +509,9 @@ test "unknown argument suggestions are exposed" { ]) try cmd.parse(argv=["--verbse"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--verbse' found #| @@ -530,9 +530,9 @@ test "unknown argument suggestions are exposed" { } try cmd.parse(argv=["-x"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '-x' found #| @@ -551,9 +551,9 @@ test "unknown argument suggestions are exposed" { } try cmd.parse(argv=["--zzzzzzzzzz"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--zzzzzzzzzz' found #| @@ -592,9 +592,9 @@ test "long and short value parsing branches" { assert_true(short_attached.values is { "count": ["4"], .. }) try cmd.parse(argv=["--count"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: a value is required for '--count' but none was supplied #| @@ -611,9 +611,9 @@ test "long and short value parsing branches" { } try cmd.parse(argv=["-c"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: a value is required for '-c' but none was supplied #| @@ -657,9 +657,9 @@ test "negation parsing and invalid negation forms" { assert_true(off.sources is { "cache": Argv, .. }) try cmd.parse(argv=["--no-path"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--no-path' found #| @@ -677,9 +677,9 @@ test "negation parsing and invalid negation forms" { } try cmd.parse(argv=["--no-missing"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--no-missing' found #| @@ -697,9 +697,9 @@ test "negation parsing and invalid negation forms" { } try cmd.parse(argv=["--no-cache=1"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--no-cache=1' found #| @@ -752,9 +752,9 @@ test "positionals dash handling and separator" { assert_true(negative.values is { "n": ["-9"], .. }) try negative_cmd.parse(argv=["x", "y"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected value 'y' for '' found; no more were expected #| @@ -821,9 +821,9 @@ test "empty positional value range is rejected at build time" { Positional("name", num_args=@argparse.ValueRange::single()), ]).parse(argv=["alice"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: empty value range (0..0) is unsupported ), @@ -849,9 +849,9 @@ test "env parsing for settrue setfalse count and invalid values" { assert_true(parsed.sources is { "on": Env, "off": Env, "v": Env, .. }) try cmd.parse(argv=[], env={ "ON": "bad" }) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: invalid value 'bad' for boolean flag; expected one of: 1, 0, true, false, yes, no, on, off #| @@ -870,9 +870,9 @@ test "env parsing for settrue setfalse count and invalid values" { } try cmd.parse(argv=[], env={ "OFF": "bad" }) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: invalid value 'bad' for boolean flag; expected one of: 1, 0, true, false, yes, no, on, off #| @@ -891,9 +891,9 @@ test "env parsing for settrue setfalse count and invalid values" { } try cmd.parse(argv=[], env={ "V": "bad" }) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: invalid value 'bad' for count; expected a non-negative integer #| @@ -912,9 +912,9 @@ test "env parsing for settrue setfalse count and invalid values" { } try cmd.parse(argv=[], env={ "V": "-1" }) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: invalid value '-1' for count; expected a non-negative integer #| @@ -965,9 +965,9 @@ test "defaults and value range helpers through public API" { assert_true(lower_absent.values is { "tag"? : None, .. }) try lower_only.parse(argv=["--tag"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: a value is required for '--tag' but none was supplied #| @@ -1002,9 +1002,9 @@ test "options consume exactly one value per occurrence" { assert_true(parsed.sources is { "tag": Argv, .. }) try cmd.parse(argv=["--tag", "a", "b"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected value 'b' found; no more were expected #| @@ -1025,9 +1025,9 @@ test "options consume exactly one value per occurrence" { test "set options reject duplicate occurrences" { let cmd = @argparse.Command("demo", options=[Optional("mode", long="mode")]) try cmd.parse(argv=["--mode", "a", "--mode", "b"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: argument '--mode' cannot be used multiple times #| @@ -1052,9 +1052,9 @@ test "flag and option args require short or long names" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: flag/option args require short/long/env ), @@ -1069,9 +1069,9 @@ test "flag and option args require short or long names" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: flag/option args require short/long/env ), @@ -1106,9 +1106,9 @@ test "option parsing stops at the next option token" { assert_true(stopped.flags is { "verbose": true, .. }) try cmd.parse(argv=["--arg=x", "y", "--verbose"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected value 'y' found; no more were expected #| @@ -1126,9 +1126,9 @@ test "option parsing stops at the next option token" { } try cmd.parse(argv=["-ax", "y", "--verbose"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected value 'y' found; no more were expected #| @@ -1152,9 +1152,9 @@ test "options always require a value" { Optional("opt", long="opt"), ]) try cmd.parse(argv=["--opt", "--verbose"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: a value is required for '--opt' but none was supplied #| @@ -1186,9 +1186,9 @@ test "option values reject hyphen tokens unless allow_hyphen_values is enabled" ]) let mut rejected = false try strict.parse(argv=["--pattern", "-file"], env=empty_env()) catch { - Message(msg) => { + err => { inspect( - msg, + err, content=( #|error: a value is required for '--pattern' but none was supplied #| @@ -1235,9 +1235,9 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: flag/option args require short/long/env ), @@ -1251,9 +1251,9 @@ test "validation branches exposed through parse" { Flag("f", long="f", action=Help, negatable=true), ]).parse(argv=[], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: help/version actions do not support negatable ), @@ -1268,9 +1268,9 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: help/version actions do not support env/defaults ), @@ -1285,9 +1285,9 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected value 'b' found; no more were expected #| @@ -1308,9 +1308,9 @@ test "validation branches exposed through parse" { Optional("x", long="x", default_values=["a", "b"]), ]).parse(argv=[], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: default_values with multiple entries require action=Append ), @@ -1324,9 +1324,9 @@ test "validation branches exposed through parse" { Positional("x", num_args=ValueRange(lower=3, upper=2)), ]).parse(argv=[], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: max values must be >= min values ), @@ -1340,9 +1340,9 @@ test "validation branches exposed through parse" { Positional("x", num_args=ValueRange(lower=-1, upper=2)), ]).parse(argv=[], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: min values must be >= 0 ), @@ -1356,9 +1356,9 @@ test "validation branches exposed through parse" { Positional("x", num_args=ValueRange(lower=0, upper=-1)), ]).parse(argv=[], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: max values must be >= 0 ), @@ -1381,9 +1381,9 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: duplicate group: g ), @@ -1398,9 +1398,9 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: group cannot require itself: g ), @@ -1415,9 +1415,9 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: group cannot conflict with itself: g ), @@ -1432,9 +1432,9 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: unknown group arg: g -> missing ), @@ -1449,9 +1449,9 @@ test "validation branches exposed through parse" { Optional("x", long="y"), ]).parse(argv=[], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: duplicate arg name: x ), @@ -1466,9 +1466,9 @@ test "validation branches exposed through parse" { Optional("y", long="same"), ]).parse(argv=[], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: duplicate long option: --same ), @@ -1483,9 +1483,9 @@ test "validation branches exposed through parse" { Flag("x", long="no-hello"), ]).parse(argv=[], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: duplicate long option: --no-hello ), @@ -1500,9 +1500,9 @@ test "validation branches exposed through parse" { Optional("y", short='s'), ]).parse(argv=[], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: duplicate short option: -s ), @@ -1517,9 +1517,9 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: arg cannot require itself: x ), @@ -1534,9 +1534,9 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: arg cannot conflict with itself: x ), @@ -1551,9 +1551,9 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: duplicate subcommand: x ), @@ -1568,9 +1568,9 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: subcommand_required requires at least one subcommand ), @@ -1585,9 +1585,9 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: subcommand name reserved for built-in help: help (disable with disable_help_subcommand) ), @@ -1647,9 +1647,9 @@ test "validation branches exposed through parse" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: version action requires command version text ), @@ -1675,9 +1675,9 @@ test "builtin and custom help/version dispatch edge paths" { ) try versioned.parse(argv=["--oops"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--oops' found #| @@ -1742,9 +1742,9 @@ test "group validation catches unknown requires target" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: unknown group requires target: g -> missing ), @@ -1762,9 +1762,9 @@ test "group validation catches unknown conflicts_with target" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: unknown group conflicts_with target: g -> missing ), @@ -1793,9 +1793,9 @@ test "group requires/conflicts can target argument names" { assert_true(ok.values is { "config": ["cfg.toml"], .. }) try requires_cmd.parse(argv=["--fast"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: the following required argument was not provided: 'config' #| @@ -1828,9 +1828,9 @@ test "group requires/conflicts can target argument names" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: group conflict mode conflicts with config #| @@ -1869,9 +1869,9 @@ test "arg validation catches unknown requires target" { Optional("mode", long="mode", requires=["missing"]), ]).parse(argv=["--mode", "fast"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: unknown requires target: mode -> missing ), @@ -1888,9 +1888,9 @@ test "arg validation catches unknown conflicts_with target" { Optional("mode", long="mode", conflicts_with=["missing"]), ]).parse(argv=["--mode", "fast"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: unknown conflicts_with target: mode -> missing ), @@ -1927,9 +1927,9 @@ test "help rendering edge paths stay stable" { let short_only_text = short_only_builtin.render_help() assert_true(short_only_text.has_prefix("Usage: demo")) try short_only_builtin.parse(argv=["--help"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: a value is required for '--help' but none was supplied #| @@ -1981,9 +1981,9 @@ test "unified error message formatting remains stable" { let cmd = @argparse.Command("demo", options=[Optional("tag", long="tag")]) try cmd.parse(argv=["--oops"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--oops' found #| @@ -2000,9 +2000,9 @@ test "unified error message formatting remains stable" { } try cmd.parse(argv=["--tag"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: a value is required for '--tag' but none was supplied #| @@ -2034,9 +2034,9 @@ test "options require one value per occurrence" { env=empty_env(), ) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: a value is required for '--tag' but none was supplied #| @@ -2065,9 +2065,9 @@ test "short options require one value before next option token" { assert_true(ok.flags is { "verbose": true, .. }) try cmd.parse(argv=["-x", "-v"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: a value is required for '-x' but none was supplied #| @@ -2107,9 +2107,9 @@ test "version action dispatches on custom long and short flags" { ) try cmd.parse(argv=["--oops"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--oops' found #| @@ -2146,9 +2146,9 @@ test "global version action keeps parent version text in subcommand context" { ) try cmd.parse(argv=["--oops"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--oops' found #| @@ -2170,9 +2170,9 @@ test "global version action keeps parent version text in subcommand context" { } try cmd.parse(argv=["run", "--oops"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--oops' found #| @@ -2201,9 +2201,9 @@ test "subcommand help puts required options in usage" { ]) try cmd.parse(argv=["run", "--oops"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--oops' found #| @@ -2231,9 +2231,9 @@ test "required and env-fed ranged values validate after parsing" { Optional("input", long="input", required=true), ]) try required_cmd.parse(argv=[], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: the following required argument was not provided: 'input' #| @@ -2306,9 +2306,9 @@ test "positional num_args lower bound rejects missing argv values" { ]) try cmd.parse(argv=[], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: 'first' requires at least 2 values but only 0 were provided #| @@ -2420,9 +2420,9 @@ test "missing option values are reported when next token is another option" { assert_true(ok.flags is { "verbose": true, .. }) try cmd.parse(argv=["--arg", "--verbose"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: a value is required for '--arg' but none was supplied #| @@ -2444,9 +2444,9 @@ test "missing option values are reported when next token is another option" { test "short-only set options use short label in duplicate errors" { let cmd = @argparse.Command("demo", options=[Optional("mode", short='m')]) try cmd.parse(argv=["-m", "a", "-m", "b"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: argument '--mode' cannot be used multiple times #| @@ -2470,9 +2470,9 @@ test "unknown short suggestion can be absent" { ]) try cmd.parse(argv=["-x"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '-x' found #| @@ -2670,9 +2670,9 @@ test "child local arg shadowing inherited global is rejected at build time" { subcommands=[Command("run", options=[Optional("mode", long="mode")])], ).parse(argv=["run"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: arg 'mode' shadows an inherited global; rename the arg or mark it global ), @@ -2759,9 +2759,9 @@ test "global set option rejects duplicate occurrences across subcommands" { try cmd.parse(argv=["--mode", "a", "run", "--mode", "b"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: argument '--mode' cannot be used multiple times #| @@ -2793,9 +2793,9 @@ test "global override with incompatible inherited type is rejected" { ], ).parse(argv=["run", "--mode"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: global arg 'mode' is incompatible with inherited global definition ), @@ -2814,9 +2814,9 @@ test "child local long alias collision with inherited global is rejected" { subcommands=[Command("run", options=[Optional("local", long="verbose")])], ).parse(argv=["run", "--verbose"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: arg 'local' long option --verbose conflicts with inherited global 'verbose' ), @@ -2833,9 +2833,9 @@ test "child local short alias collision with inherited global is rejected" { Command("run", options=[Optional("local", short='v')]), ]).parse(argv=["run", "-v"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: arg 'local' short option -v conflicts with inherited global 'verbose' ), @@ -2879,9 +2879,9 @@ test "non-bmp short option token does not panic" { test "non-bmp hyphen token reports unknown argument without panic" { let cmd = @argparse.Command("demo", positionals=[Positional("value")]) try cmd.parse(argv=["-🎉"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '-🎉' found #| @@ -2979,9 +2979,9 @@ test "global override with different negatable setting is rejected" { ], ).parse(argv=["run"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: global arg 'verbose' is incompatible with inherited global definition ), diff --git a/argparse/argparse_test.mbt b/argparse/argparse_test.mbt index 0236576ec..71754b0e6 100644 --- a/argparse/argparse_test.mbt +++ b/argparse/argparse_test.mbt @@ -62,9 +62,9 @@ test "long empty string disables long alias" { assert_true(matches.values is { "count": ["3"], .. }) try cmd.parse(argv=["--verbose"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--verbose' found #| @@ -82,9 +82,9 @@ test "long empty string disables long alias" { } try cmd.parse(argv=["--count", "3"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--count' found #| @@ -151,9 +151,9 @@ test "parse failure message contains error and contextual help" { ]) try cmd.parse(argv=["--bad"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--bad' found #| @@ -177,9 +177,9 @@ test "subcommand parse errors include subcommand help" { ]) try cmd.parse(argv=["echo", "--bad"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--bad' found #| @@ -203,9 +203,9 @@ test "build errors are surfaced as validation failure message" { ]) try cmd.parse(argv=[], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: command definition validation failed: unknown requires target: fast -> missing ), @@ -220,9 +220,9 @@ test "unknown argument keeps suggestion in final message" { let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")]) try cmd.parse(argv=["--verbse"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--verbse' found #| @@ -277,9 +277,9 @@ test "display help and version" { ) try cmd.parse(argv=["--oops"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--oops' found #| @@ -305,9 +305,9 @@ test "parse error show is readable" { ]) try cmd.parse(argv=["--verbse"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--verbse' found #| @@ -329,9 +329,9 @@ test "parse error show is readable" { } try cmd.parse(argv=["alice", "bob"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected value 'bob' for '' found; no more were expected #| @@ -359,9 +359,9 @@ test "relationships and num args" { ]) try requires_cmd.parse(argv=["--mode", "fast"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: the following required argument was not provided: 'config' (required by 'mode') #| @@ -397,9 +397,9 @@ test "arg groups required and multiple" { ) try cmd.parse(argv=[], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: the following required arguments were not provided: #| <--fast|--slow> @@ -421,9 +421,9 @@ test "arg groups required and multiple" { } try cmd.parse(argv=["--fast", "--slow"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: group conflict mode #| @@ -456,9 +456,9 @@ test "arg groups requires and conflicts" { ) try requires_cmd.parse(argv=["--fast"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: the following required arguments were not provided: #| <--json> @@ -490,9 +490,9 @@ test "arg groups requires and conflicts" { ) try conflict_cmd.parse(argv=["--fast", "--json"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: group conflict mode conflicts with output #| @@ -650,9 +650,9 @@ test "negatable and conflicts" { assert_true(no_failfast.flags is { "failfast": true, .. }) try cmd.parse(argv=["--verbose", "--quiet"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: conflicting arguments: verbose and quiet #| @@ -676,9 +676,9 @@ test "negatable and conflicts" { test "flag does not accept inline value" { let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")]) try cmd.parse(argv=["--verbose=true"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--verbose=true' found #| @@ -700,9 +700,9 @@ test "built-in long flags do not accept inline value" { let cmd = @argparse.Command("demo", version="1.2.3") try cmd.parse(argv=["--help=1"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--help=1' found #| @@ -719,9 +719,9 @@ test "built-in long flags do not accept inline value" { } try cmd.parse(argv=["--version=1"], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: unexpected argument '--version=1' found #| @@ -770,9 +770,9 @@ test "command policies" { ), ) try sub_cmd.parse(argv=[], env=empty_env()) catch { - Message(msg) => + err => inspect( - msg, + err, content=( #|error: the following required argument was not provided: 'subcommand' #| diff --git a/argparse/command.mbt b/argparse/command.mbt index 07c4a9168..78b994bf2 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -113,10 +113,9 @@ pub fn Command::render_help(self : Command) -> String { /// Behavior: /// - Help/version requests print output immediately and terminate with exit code /// `0`. -/// - Parse failures raise `ArgError::Message` where the payload includes the -/// error text and full contextual help output. -/// - Command-definition validation failures raise `ArgError::Message` with a -/// validation-failure message (without appended help). +/// - 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`. #as_free_fn @@ -124,15 +123,18 @@ pub fn Command::parse( self : Command, argv? : ArrayView[String] = default_argv(), env? : Map[String, String] = {}, -) -> Matches raise ArgError { +) -> Matches raise { try { let raw = parse_command(self, argv, env, [], {}, {}, self.name) build_matches(self, raw, []) } catch { DisplayHelp::Message(text) => print_and_exit_success(text) DisplayVersion::Message(text) => print_and_exit_success(text) - ArgError::Message(msg) => raise Message(msg) - _ => panic() + ArgError::Message(_) as err => raise err + err => { + println(err.to_string()) + panic() + } } } diff --git a/argparse/error.mbt b/argparse/error.mbt index 26b5f4c5d..386eaddc5 100644 --- a/argparse/error.mbt +++ b/argparse/error.mbt @@ -13,17 +13,17 @@ // limitations under the License. ///| -/// Unified error surface exposed by argparse. +/// Internal error surface used by argparse. /// /// `Message` is display-ready text intended for end users. /// Parse failures include contextual help; command-definition validation /// failures are returned as plain validation messages. -pub suberror ArgError { +priv suberror ArgError { Message(String) } ///| -pub impl Show for ArgError with output(self : ArgError, logger) { +impl Show for ArgError with output(self : ArgError, logger) { match self { Message(msg) => logger.write_string(msg) } diff --git a/argparse/moon.pkg b/argparse/moon.pkg index 160a04f4e..2b12cb1bf 100644 --- a/argparse/moon.pkg +++ b/argparse/moon.pkg @@ -1,5 +1,6 @@ import { "moonbitlang/core/builtin", + "moonbitlang/core/error", "moonbitlang/core/env", "moonbitlang/core/strconv", "moonbitlang/core/set", diff --git a/argparse/pkg.generated.mbti b/argparse/pkg.generated.mbti index d5d4c51e3..4cd5b27d9 100644 --- a/argparse/pkg.generated.mbti +++ b/argparse/pkg.generated.mbti @@ -8,10 +8,6 @@ import { // Values // Errors -pub suberror ArgError { - Message(String) -} -pub impl Show for ArgError // Types and methods pub struct ArgGroup { @@ -28,7 +24,7 @@ pub struct Command { } pub fn Command::new(StringView, flags? : ArrayView[Flag], options? : ArrayView[Optional], positionals? : ArrayView[Positional], subcommands? : ArrayView[Self], 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]) -> Self #as_free_fn -pub fn Command::parse(Self, argv? : ArrayView[String], env? : Map[String, String]) -> Matches raise ArgError +pub fn Command::parse(Self, argv? : ArrayView[String], env? : Map[String, String]) -> Matches raise pub fn Command::render_help(Self) -> String pub struct Flag { From 58eab6422c4299cf54bcc586a4d5455718b9830e Mon Sep 17 00:00:00 2001 From: zihang Date: Tue, 3 Mar 2026 13:29:17 +0800 Subject: [PATCH 40/40] refactor(argparse): rename constructors to *Arg forms --- argparse/README.mbt.md | 50 ++-- argparse/arg_spec.mbt | 27 +- argparse/argparse_blackbox_test.mbt | 374 +++++++++++++++------------- argparse/argparse_test.mbt | 86 ++++--- argparse/command.mbt | 18 +- argparse/parser_lookup_wbtest.mbt | 4 +- argparse/pkg.generated.mbti | 30 +-- 7 files changed, 311 insertions(+), 278 deletions(-) diff --git a/argparse/README.mbt.md b/argparse/README.mbt.md index 71459241f..ad12c46ed 100644 --- a/argparse/README.mbt.md +++ b/argparse/README.mbt.md @@ -6,6 +6,8 @@ This package is inspired by [`clap`](https://github.com/clap-rs/clap) and keeps 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 @@ -17,8 +19,8 @@ both the parse error and the full contextual help text. ///| test "basic option + positional success snapshot" { let matches = @argparse.parse( - Command("demo", options=[Optional("name")], positionals=[ - Positional("target"), + Command("demo", options=[OptionArg("name")], positionals=[ + PositionArg("target"), ]), argv=["--name", "alice", "file.txt"], env={}, @@ -33,8 +35,8 @@ test "basic option + positional success snapshot" { ///| test "basic option + positional failure snapshot" { - let cmd = @argparse.Command("demo", options=[Optional("name")], positionals=[ - Positional("target"), + let cmd = @argparse.Command("demo", options=[OptionArg("name")], positionals=[ + PositionArg("target"), ]) try cmd.parse(argv=["--bad"], env={}) catch { err => @@ -69,8 +71,8 @@ contextual help text. ```mbt nocheck ///| async fn main { - let cmd = @argparse.Command("demo", options=[@argparse.Optional("name")], positionals=[ - @argparse.Positional("target"), + let cmd = @argparse.Command("demo", options=[@argparse.OptionArg("name")], positionals=[ + @argparse.PositionArg("target"), ]) let _ = cmd.parse() } @@ -85,7 +87,7 @@ states. ```mbt check ///| test "negatable flag success snapshot" { - let cmd = @argparse.Command("demo", flags=[Flag("cache", negatable=true)]) + let cmd = @argparse.Command("demo", flags=[FlagArg("cache", negatable=true)]) inspect( cmd.render_help(), content=( @@ -115,7 +117,7 @@ test "negatable flag success snapshot" { test "global count flag success snapshot" { let cmd = @argparse.Command( "demo", - flags=[Flag("verbose", short='v', action=Count, global=true)], + flags=[FlagArg("verbose", short='v', action=Count, global=true)], subcommands=[Command("run")], ) @@ -139,7 +141,7 @@ test "global count flag success snapshot" { test "subcommand context failure snapshot" { let cmd = @argparse.Command( "demo", - flags=[Flag("verbose", short='v', action=Count, global=true)], + flags=[FlagArg("verbose", short='v', action=Count, global=true)], subcommands=[Command("run")], ) try cmd.parse(argv=["run", "--oops"], env={}) catch { @@ -171,7 +173,7 @@ Value precedence is `argv > env > default_values`. ///| test "value source precedence snapshots" { let cmd = @argparse.Command("demo", options=[ - Optional("level", env="LEVEL", default_values=["1"]), + OptionArg("level", env="LEVEL", default_values=["1"]), ]) inspect( @@ -235,7 +237,7 @@ test "value source precedence snapshots" { ```mbt check ///| test "option input forms snapshot" { - let cmd = @argparse.Command("demo", options=[Optional("count", short='c')]) + let cmd = @argparse.Command("demo", options=[OptionArg("count", short='c')]) inspect( cmd.render_help(), @@ -285,7 +287,7 @@ test "option input forms snapshot" { ///| test "double-dash separator snapshot" { let cmd = @argparse.Command("demo", positionals=[ - Positional("tail", num_args=ValueRange(lower=0), allow_hyphen_values=true), + PositionArg("tail", num_args=ValueRange(lower=0), allow_hyphen_values=true), ]) let parsed = try! cmd.parse(argv=["--", "--x", "-y"], env={}) @debug.debug_inspect( @@ -306,8 +308,8 @@ full contextual help. ///| test "requires relationship success and failure snapshots" { let cmd = @argparse.Command("demo", options=[ - Optional("mode", requires=["config"]), - Optional("config"), + OptionArg("mode", requires=["config"]), + OptionArg("config"), ]) let ok = try! cmd.parse(argv=["--mode", "fast", "--config", "cfg.toml"], env={}) @@ -346,7 +348,7 @@ test "arg group required and exclusive failure snapshot" { groups=[ ArgGroup("mode", required=true, multiple=false, args=["fast", "slow"]), ], - flags=[Flag("fast"), Flag("slow")], + flags=[FlagArg("fast"), FlagArg("slow")], ) try cmd.parse(argv=[], env={}) catch { @@ -408,8 +410,8 @@ test "subcommand required policy failure snapshot" { ///| test "conflicts_with success and failure snapshots" { let cmd = @argparse.Command("demo", flags=[ - Flag("verbose", conflicts_with=["quiet"]), - Flag("quiet"), + FlagArg("verbose", conflicts_with=["quiet"]), + FlagArg("quiet"), ]) let ok = try! cmd.parse(argv=["--verbose"], env={}) @@ -442,7 +444,7 @@ test "conflicts_with success and failure snapshots" { } ``` -## Positional Value Ranges +## PositionArg Value Ranges Positionals are parsed in declaration order (no explicit index). @@ -450,8 +452,8 @@ Positionals are parsed in declaration order (no explicit index). ///| test "bounded non-last positional success snapshot" { let cmd = @argparse.Command("demo", positionals=[ - Positional("first", num_args=ValueRange(lower=1, upper=2)), - Positional("second", num_args=@argparse.ValueRange::single()), + 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={}) @@ -466,8 +468,8 @@ test "bounded non-last positional success snapshot" { ///| test "bounded non-last positional failure snapshot" { let cmd = @argparse.Command("demo", positionals=[ - Positional("first", num_args=ValueRange(lower=1, upper=2)), - Positional("second", num_args=@argparse.ValueRange::single()), + 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={}) catch { err => @@ -497,9 +499,9 @@ test "bounded non-last positional failure snapshot" { ///| let cmd : @argparse.Command = Command( "wrap", - options=[Optional("config"), Optional("mode")], + options=[OptionArg("config"), OptionArg("mode")], positionals=[ - Positional( + PositionArg( "child_argv", num_args=ValueRange(lower=0), allow_hyphen_values=true, diff --git a/argparse/arg_spec.mbt b/argparse/arg_spec.mbt index c4255a42c..2615486d5 100644 --- a/argparse/arg_spec.mbt +++ b/argparse/arg_spec.mbt @@ -76,7 +76,7 @@ priv enum ArgInfo { ///| /// Declarative flag constructor wrapper. -pub struct Flag { +pub struct FlagArg { priv arg : Arg /// Create a flag argument. @@ -93,7 +93,7 @@ pub struct Flag { global? : Bool, negatable? : Bool, hidden? : Bool, - ) -> Flag + ) -> FlagArg } ///| @@ -107,7 +107,7 @@ pub struct Flag { /// - `negatable=true` accepts `--no-` for long flags. /// - If `env` is set, accepted boolean values are: /// `1`, `0`, `true`, `false`, `yes`, `no`, `on`, `off`. -pub fn Flag::new( +pub fn FlagArg::new( name : StringView, short? : Char, long? : StringView = name, @@ -120,7 +120,7 @@ pub fn Flag::new( global? : Bool = false, negatable? : Bool = false, hidden? : Bool = false, -) -> Flag { +) -> FlagArg { let name = name.to_string() let long = if long == "" { None } else { Some(long.to_string()) } let about = about.map(v => v.to_string()) @@ -143,7 +143,8 @@ pub fn Flag::new( ///| /// Declarative option constructor wrapper. -pub struct Optional { +/// Named `OptionArg` to avoid shadowing the built-in `Option` type. +pub struct OptionArg { priv arg : Arg /// Create an option argument. @@ -162,7 +163,7 @@ pub struct Optional { required? : Bool, global? : Bool, hidden? : Bool, - ) -> Optional + ) -> OptionArg } ///| @@ -176,7 +177,7 @@ pub struct Optional { /// - `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. -pub fn Optional::new( +pub fn OptionArg::new( name : StringView, short? : Char, long? : StringView = name, @@ -190,7 +191,7 @@ pub fn Optional::new( required? : Bool = false, global? : Bool = false, hidden? : Bool = false, -) -> Optional { +) -> OptionArg { let name = name.to_string() let long = if long == "" { None } else { Some(long.to_string()) } let about = about.map(v => v.to_string()) @@ -219,7 +220,7 @@ pub fn Optional::new( ///| /// Declarative positional constructor wrapper. -pub struct Positional { +pub struct PositionArg { priv arg : Arg /// Create a positional argument. @@ -234,14 +235,14 @@ pub struct Positional { conflicts_with? : ArrayView[String], global? : Bool, hidden? : Bool, - ) -> Positional + ) -> PositionArg } ///| /// Create a positional argument. /// /// Notes: -/// - Positional order follows declaration order. +/// - 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`). @@ -249,7 +250,7 @@ pub struct 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. -pub fn Positional::new( +pub fn PositionArg::new( name : StringView, about? : StringView, env? : StringView, @@ -260,7 +261,7 @@ pub fn Positional::new( conflicts_with? : ArrayView[String] = [], global? : Bool = false, hidden? : Bool = false, -) -> Positional { +) -> PositionArg { let name = name.to_string() let about = about.map(v => v.to_string()) let env = env.map(v => v.to_string()) diff --git a/argparse/argparse_blackbox_test.mbt b/argparse/argparse_blackbox_test.mbt index 74150b462..27c69bf7c 100644 --- a/argparse/argparse_blackbox_test.mbt +++ b/argparse/argparse_blackbox_test.mbt @@ -26,12 +26,12 @@ test "render help snapshot with groups and hidden entries" { Command("hidden", about="hidden", hidden=true), ], flags=[ - Flag("fast", short='f', long="fast"), - Flag("slow", long="slow", hidden=true), - Flag("cache", long="cache", negatable=true, about="cache"), + FlagArg("fast", short='f', long="fast"), + FlagArg("slow", long="slow", hidden=true), + FlagArg("cache", long="cache", negatable=true, about="cache"), ], options=[ - Optional( + OptionArg( "path", short='p', long="path", @@ -41,9 +41,9 @@ test "render help snapshot with groups and hidden entries" { ), ], positionals=[ - Positional("target", num_args=@argparse.ValueRange::single()), - Positional("rest", num_args=ValueRange(lower=0)), - Positional("secret", hidden=true), + PositionArg("target", num_args=@argparse.ValueRange::single()), + PositionArg("rest", num_args=ValueRange(lower=0)), + PositionArg("secret", hidden=true), ], ) inspect( @@ -78,7 +78,7 @@ test "render help conversion coverage snapshot" { "shape", groups=[ArgGroup("grp", args=["f", "opt", "pos"])], flags=[ - Flag( + FlagArg( "f", short='f', about="f", @@ -89,7 +89,7 @@ test "render help conversion coverage snapshot" { ), ], options=[ - Optional( + OptionArg( "opt", short='o', about="opt", @@ -103,7 +103,7 @@ test "render help conversion coverage snapshot" { ), ], positionals=[ - Positional( + PositionArg( "pos", about="pos", env="POS_ENV", @@ -132,7 +132,7 @@ test "render help conversion coverage snapshot" { ///| test "count flags and sources with pattern matching" { let cmd = @argparse.Command("demo", flags=[ - Flag("verbose", short='v', long="verbose", action=Count), + FlagArg("verbose", short='v', long="verbose", action=Count), ]) let matches = cmd.parse(argv=["-v", "-v", "-v"], env=empty_env()) catch { _ => panic() @@ -148,7 +148,13 @@ test "global option merges parent and child values" { let cmd = @argparse.Command( "demo", options=[ - Optional("profile", short='p', long="profile", action=Append, global=true), + OptionArg( + "profile", + short='p', + long="profile", + action=Append, + global=true, + ), ], subcommands=[child], ) @@ -172,8 +178,8 @@ test "global requires is validated after parent-child merge" { let cmd = @argparse.Command( "demo", options=[ - Optional("mode", long="mode", requires=["config"], global=true), - Optional("config", long="config", global=true), + OptionArg("mode", long="mode", requires=["config"], global=true), + OptionArg("config", long="config", global=true), ], subcommands=[Command("run")], ) @@ -197,7 +203,7 @@ test "global append keeps parent argv over child env/default" { let cmd = @argparse.Command( "demo", options=[ - Optional( + OptionArg( "profile", long="profile", action=Append, @@ -229,7 +235,7 @@ test "global scalar keeps parent argv over child env/default" { let cmd = @argparse.Command( "demo", options=[ - Optional( + OptionArg( "profile", long="profile", env="PROFILE", @@ -259,7 +265,7 @@ test "global count merges parent and child occurrences" { let child = @argparse.Command("run") let cmd = @argparse.Command( "demo", - flags=[Flag("verbose", short='v', action=Count, global=true)], + flags=[FlagArg("verbose", short='v', action=Count, global=true)], subcommands=[child], ) @@ -279,7 +285,7 @@ test "global count keeps parent argv over child env fallback" { let cmd = @argparse.Command( "demo", flags=[ - Flag( + FlagArg( "verbose", short='v', long="verbose", @@ -308,7 +314,7 @@ test "global flag keeps parent argv over child env fallback" { let child = @argparse.Command("run") let cmd = @argparse.Command( "demo", - flags=[Flag("verbose", long="verbose", env="VERBOSE", global=true)], + flags=[FlagArg("verbose", long="verbose", env="VERBOSE", global=true)], subcommands=[child], ) @@ -326,7 +332,7 @@ test "global flag keeps parent argv over child env fallback" { ///| test "subcommand cannot follow positional arguments" { - let cmd = @argparse.Command("demo", positionals=[Positional("input")], subcommands=[ + let cmd = @argparse.Command("demo", positionals=[PositionArg("input")], subcommands=[ Command("run"), ]) try cmd.parse(argv=["raw", "run"], env=empty_env()) catch { @@ -361,7 +367,7 @@ test "global count source keeps env across subcommand merge" { let cmd = @argparse.Command( "demo", flags=[ - Flag( + FlagArg( "verbose", short='v', long="verbose", @@ -469,7 +475,7 @@ test "subcommand help includes inherited global options" { let cmd = @argparse.Command( "demo", flags=[ - Flag( + FlagArg( "verbose", short='v', long="verbose", @@ -505,7 +511,7 @@ test "subcommand help includes inherited global options" { ///| test "unknown argument suggestions are exposed" { let cmd = @argparse.Command("demo", flags=[ - Flag("verbose", short='v', long="verbose"), + FlagArg("verbose", short='v', long="verbose"), ]) try cmd.parse(argv=["--verbse"], env=empty_env()) catch { @@ -573,7 +579,7 @@ test "unknown argument suggestions are exposed" { ///| test "long and short value parsing branches" { let cmd = @argparse.Command("demo", options=[ - Optional("count", short='c', long="count"), + OptionArg("count", short='c', long="count"), ]) let long_inline = cmd.parse(argv=["--count=2"], env=empty_env()) catch { @@ -633,7 +639,7 @@ test "long and short value parsing branches" { ///| test "append option action is publicly selectable" { let cmd = @argparse.Command("demo", options=[ - Optional("tag", long="tag", action=Append), + OptionArg("tag", long="tag", action=Append), ]) let appended = cmd.parse(argv=["--tag", "a", "--tag", "b"], env=empty_env()) catch { _ => panic() @@ -646,8 +652,8 @@ test "append option action is publicly selectable" { test "negation parsing and invalid negation forms" { let cmd = @argparse.Command( "demo", - flags=[Flag("cache", long="cache", negatable=true)], - options=[Optional("path", long="path")], + flags=[FlagArg("cache", long="cache", negatable=true)], + options=[OptionArg("path", long="path")], ) let off = cmd.parse(argv=["--no-cache"], env=empty_env()) catch { @@ -717,7 +723,7 @@ test "negation parsing and invalid negation forms" { } let count_cmd = @argparse.Command("demo", flags=[ - Flag("verbose", long="verbose", action=Count, negatable=true), + FlagArg("verbose", long="verbose", action=Count, negatable=true), ]) let reset = count_cmd.parse( argv=["--verbose", "--no-verbose"], @@ -733,7 +739,7 @@ test "negation parsing and invalid negation forms" { ///| test "positionals dash handling and separator" { let force_cmd = @argparse.Command("demo", positionals=[ - Positional("tail", num_args=ValueRange(lower=0), allow_hyphen_values=true), + PositionArg("tail", num_args=ValueRange(lower=0), allow_hyphen_values=true), ]) let forced = force_cmd.parse(argv=["a", "--x", "-y"], env=empty_env()) catch { _ => panic() @@ -745,7 +751,7 @@ test "positionals dash handling and separator" { } assert_true(dashed.values is { "tail": ["p", "q"], .. }) - let negative_cmd = @argparse.Command("demo", positionals=[Positional("n")]) + let negative_cmd = @argparse.Command("demo", positionals=[PositionArg("n")]) let negative = negative_cmd.parse(argv=["-9"], env=empty_env()) catch { _ => panic() } @@ -776,7 +782,7 @@ test "positionals dash handling and separator" { ///| test "variadic positional keeps accepting hyphen values after first token" { let cmd = @argparse.Command("demo", positionals=[ - Positional("tail", num_args=ValueRange(lower=0), allow_hyphen_values=true), + PositionArg("tail", num_args=ValueRange(lower=0), allow_hyphen_values=true), ]) let parsed = cmd.parse(argv=["a", "-b", "--mystery"], env=empty_env()) catch { _ => panic() @@ -787,8 +793,8 @@ test "variadic positional keeps accepting hyphen values after first token" { ///| test "bounded positional does not greedily consume later required values" { let cmd = @argparse.Command("demo", positionals=[ - Positional("first", num_args=ValueRange(lower=1, upper=2)), - Positional("second", num_args=@argparse.ValueRange::single()), + PositionArg("first", num_args=ValueRange(lower=1, upper=2)), + PositionArg("second", num_args=@argparse.ValueRange::single()), ]) let two = cmd.parse(argv=["a", "b"], env=empty_env()) catch { _ => panic() } @@ -803,8 +809,8 @@ test "bounded positional does not greedily consume later required values" { ///| test "indexed non-last positional allows explicit single num_args" { let cmd = @argparse.Command("demo", positionals=[ - Positional("first", num_args=@argparse.ValueRange::single()), - Positional("second", num_args=@argparse.ValueRange::single()), + PositionArg("first", num_args=@argparse.ValueRange::single()), + PositionArg("second", num_args=@argparse.ValueRange::single()), ]) let parsed = cmd.parse(argv=["a", "b"], env=empty_env()) catch { @@ -817,8 +823,8 @@ test "indexed non-last positional allows explicit single num_args" { test "empty positional value range is rejected at build time" { try @argparse.Command("demo", positionals=[ - Positional("skip", num_args=ValueRange(lower=0, upper=0)), - Positional("name", num_args=@argparse.ValueRange::single()), + PositionArg("skip", num_args=ValueRange(lower=0, upper=0)), + PositionArg("name", num_args=@argparse.ValueRange::single()), ]).parse(argv=["alice"], env=empty_env()) catch { err => @@ -836,9 +842,9 @@ test "empty positional value range is rejected at build time" { ///| test "env parsing for settrue setfalse count and invalid values" { let cmd = @argparse.Command("demo", flags=[ - Flag("on", long="on", action=SetTrue, env="ON"), - Flag("off", long="off", action=SetFalse, env="OFF"), - Flag("v", long="v", action=Count, env="V"), + FlagArg("on", long="on", action=SetTrue, env="ON"), + FlagArg("off", long="off", action=SetFalse, env="OFF"), + FlagArg("v", long="v", action=Count, env="V"), ]) let parsed = cmd.parse(argv=[], env={ "ON": "true", "OFF": "true", "V": "3" }) catch { @@ -936,8 +942,8 @@ test "env parsing for settrue setfalse count and invalid values" { ///| test "defaults and value range helpers through public API" { let defaults = @argparse.Command("demo", options=[ - Optional("mode", long="mode", action=Append, default_values=["a", "b"]), - Optional("one", long="one", default_values=["x"]), + OptionArg("mode", long="mode", action=Append, default_values=["a", "b"]), + OptionArg("one", long="one", default_values=["x"]), ]) let by_default = defaults.parse(argv=[], env=empty_env()) catch { _ => panic() @@ -946,7 +952,7 @@ test "defaults and value range helpers through public API" { assert_true(by_default.sources is { "mode": Default, "one": Default, .. }) let upper_only = @argparse.Command("demo", options=[ - Optional("tag", long="tag", action=Append), + OptionArg("tag", long="tag", action=Append), ]) let upper_parsed = upper_only.parse( argv=["--tag", "a", "--tag", "b", "--tag", "c"], @@ -957,7 +963,7 @@ test "defaults and value range helpers through public API" { assert_true(upper_parsed.values is { "tag": ["a", "b", "c"], .. }) let lower_only = @argparse.Command("demo", options=[ - Optional("tag", long="tag"), + OptionArg("tag", long="tag"), ]) let lower_absent = lower_only.parse(argv=[], env=empty_env()) catch { _ => panic() @@ -994,7 +1000,7 @@ test "defaults and value range helpers through public API" { ///| test "options consume exactly one value per occurrence" { - let cmd = @argparse.Command("demo", options=[Optional("tag", long="tag")]) + let cmd = @argparse.Command("demo", options=[OptionArg("tag", long="tag")]) let parsed = cmd.parse(argv=["--tag", "a"], env=empty_env()) catch { _ => panic() } @@ -1023,7 +1029,7 @@ test "options consume exactly one value per occurrence" { ///| test "set options reject duplicate occurrences" { - let cmd = @argparse.Command("demo", options=[Optional("mode", long="mode")]) + let cmd = @argparse.Command("demo", options=[OptionArg("mode", long="mode")]) try cmd.parse(argv=["--mode", "a", "--mode", "b"], env=empty_env()) catch { err => inspect( @@ -1047,7 +1053,7 @@ test "set options reject duplicate occurrences" { ///| test "flag and option args require short or long names" { try - @argparse.Command("demo", options=[Optional("input", long="")]).parse( + @argparse.Command("demo", options=[OptionArg("input", long="")]).parse( argv=[], env=empty_env(), ) @@ -1064,7 +1070,7 @@ test "flag and option args require short or long names" { } try - @argparse.Command("demo", flags=[Flag("verbose", long="")]).parse( + @argparse.Command("demo", flags=[FlagArg("verbose", long="")]).parse( argv=[], env=empty_env(), ) @@ -1084,7 +1090,7 @@ test "flag and option args require short or long names" { ///| test "append options collect values across repeated occurrences" { let cmd = @argparse.Command("demo", options=[ - Optional("arg", long="arg", action=Append), + OptionArg("arg", long="arg", action=Append), ]) let parsed = cmd.parse(argv=["--arg", "x", "--arg", "y"], env=empty_env()) catch { _ => panic() @@ -1095,9 +1101,11 @@ test "append options collect values across repeated occurrences" { ///| test "option parsing stops at the next option token" { - let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")], options=[ - Optional("arg", short='a', long="arg"), - ]) + let cmd = @argparse.Command( + "demo", + flags=[FlagArg("verbose", long="verbose")], + options=[OptionArg("arg", short='a', long="arg")], + ) let stopped = cmd.parse(argv=["--arg", "x", "--verbose"], env=empty_env()) catch { _ => panic() @@ -1148,9 +1156,11 @@ test "option parsing stops at the next option token" { ///| test "options always require a value" { - let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")], options=[ - Optional("opt", long="opt"), - ]) + let cmd = @argparse.Command( + "demo", + flags=[FlagArg("verbose", long="verbose")], + options=[OptionArg("opt", long="opt")], + ) try cmd.parse(argv=["--opt", "--verbose"], env=empty_env()) catch { err => inspect( @@ -1172,7 +1182,7 @@ test "options always require a value" { } let zero_value_required = @argparse.Command("demo", options=[ - Optional("opt", long="opt", required=true), + OptionArg("opt", long="opt", required=true), ]).parse(argv=["--opt", "x"], env=empty_env()) catch { _ => panic() } @@ -1182,7 +1192,7 @@ test "options always require a value" { ///| test "option values reject hyphen tokens unless allow_hyphen_values is enabled" { let strict = @argparse.Command("demo", options=[ - Optional("pattern", long="pattern"), + OptionArg("pattern", long="pattern"), ]) let mut rejected = false try strict.parse(argv=["--pattern", "-file"], env=empty_env()) catch { @@ -1208,7 +1218,7 @@ test "option values reject hyphen tokens unless allow_hyphen_values is enabled" assert_true(rejected) let permissive = @argparse.Command("demo", options=[ - Optional("pattern", long="pattern", allow_hyphen_values=true), + OptionArg("pattern", long="pattern", allow_hyphen_values=true), ]) let parsed = permissive.parse(argv=["--pattern", "-file"], env=empty_env()) catch { _ => panic() @@ -1222,7 +1232,7 @@ test "option values reject hyphen tokens unless allow_hyphen_values is enabled" ///| test "default argv path is reachable" { let cmd = @argparse.Command("demo", positionals=[ - Positional("rest", num_args=ValueRange(lower=0), allow_hyphen_values=true), + PositionArg("rest", num_args=ValueRange(lower=0), allow_hyphen_values=true), ]) let _ = cmd.parse(env=empty_env()) catch { _ => panic() } } @@ -1230,7 +1240,7 @@ test "default argv path is reachable" { ///| test "validation branches exposed through parse" { try - @argparse.Command("demo", flags=[Flag("f", long="", action=Help)]).parse( + @argparse.Command("demo", flags=[FlagArg("f", long="", action=Help)]).parse( argv=[], env=empty_env(), ) @@ -1248,7 +1258,7 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", flags=[ - Flag("f", long="f", action=Help, negatable=true), + FlagArg("f", long="f", action=Help, negatable=true), ]).parse(argv=[], env=empty_env()) catch { err => @@ -1263,10 +1273,9 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", flags=[Flag("f", long="f", action=Help, env="F")]).parse( - argv=[], - env=empty_env(), - ) + @argparse.Command("demo", flags=[ + FlagArg("f", long="f", action=Help, env="F"), + ]).parse(argv=[], env=empty_env()) catch { err => inspect( @@ -1280,7 +1289,7 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", options=[Optional("x", long="x")]).parse( + @argparse.Command("demo", options=[OptionArg("x", long="x")]).parse( argv=["--x", "a", "b"], env=empty_env(), ) @@ -1305,7 +1314,7 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", options=[ - Optional("x", long="x", default_values=["a", "b"]), + OptionArg("x", long="x", default_values=["a", "b"]), ]).parse(argv=[], env=empty_env()) catch { err => @@ -1321,7 +1330,7 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", positionals=[ - Positional("x", num_args=ValueRange(lower=3, upper=2)), + PositionArg("x", num_args=ValueRange(lower=3, upper=2)), ]).parse(argv=[], env=empty_env()) catch { err => @@ -1337,7 +1346,7 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", positionals=[ - Positional("x", num_args=ValueRange(lower=-1, upper=2)), + PositionArg("x", num_args=ValueRange(lower=-1, upper=2)), ]).parse(argv=[], env=empty_env()) catch { err => @@ -1353,7 +1362,7 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", positionals=[ - Positional("x", num_args=ValueRange(lower=0, upper=-1)), + PositionArg("x", num_args=ValueRange(lower=0, upper=-1)), ]).parse(argv=[], env=empty_env()) catch { err => @@ -1368,8 +1377,8 @@ test "validation branches exposed through parse" { } let positional_ok = @argparse.Command("demo", positionals=[ - Positional("x", num_args=ValueRange(lower=0, upper=2)), - Positional("y"), + PositionArg("x", num_args=ValueRange(lower=0, upper=2)), + PositionArg("y"), ]).parse(argv=["a"], env=empty_env()) catch { _ => panic() } @@ -1445,8 +1454,8 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", options=[ - Optional("x", long="x"), - Optional("x", long="y"), + OptionArg("x", long="x"), + OptionArg("x", long="y"), ]).parse(argv=[], env=empty_env()) catch { err => @@ -1462,8 +1471,8 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", options=[ - Optional("x", long="same"), - Optional("y", long="same"), + OptionArg("x", long="same"), + OptionArg("y", long="same"), ]).parse(argv=[], env=empty_env()) catch { err => @@ -1479,8 +1488,8 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", flags=[ - Flag("hello", long="hello", negatable=true), - Flag("x", long="no-hello"), + FlagArg("hello", long="hello", negatable=true), + FlagArg("x", long="no-hello"), ]).parse(argv=[], env=empty_env()) catch { err => @@ -1496,8 +1505,8 @@ test "validation branches exposed through parse" { try @argparse.Command("demo", options=[ - Optional("x", short='s'), - Optional("y", short='s'), + OptionArg("x", short='s'), + OptionArg("y", short='s'), ]).parse(argv=[], env=empty_env()) catch { err => @@ -1512,7 +1521,7 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", flags=[Flag("x", long="x", requires=["x"])]).parse( + @argparse.Command("demo", flags=[FlagArg("x", long="x", requires=["x"])]).parse( argv=[], env=empty_env(), ) @@ -1529,10 +1538,9 @@ test "validation branches exposed through parse" { } try - @argparse.Command("demo", flags=[Flag("x", long="x", conflicts_with=["x"])]).parse( - argv=[], - env=empty_env(), - ) + @argparse.Command("demo", flags=[ + FlagArg("x", long="x", conflicts_with=["x"]), + ]).parse(argv=[], env=empty_env()) catch { err => inspect( @@ -1597,7 +1605,7 @@ test "validation branches exposed through parse" { } let custom_help = @argparse.Command("demo", flags=[ - Flag("custom_help", short='h', long="help", about="custom help"), + FlagArg("custom_help", short='h', long="help", about="custom help"), ]) let help_short = custom_help.parse(argv=["-h"], env=empty_env()) catch { _ => panic() @@ -1619,7 +1627,7 @@ test "validation branches exposed through parse" { ) let custom_version = @argparse.Command("demo", version="1.0", flags=[ - Flag("custom_version", short='V', long="version", about="custom version"), + FlagArg("custom_version", short='V', long="version", about="custom version"), ]) let version_short = custom_version.parse(argv=["-V"], env=empty_env()) catch { _ => panic() @@ -1642,7 +1650,7 @@ test "validation branches exposed through parse" { ) try - @argparse.Command("demo", flags=[Flag("v", long="v", action=Version)]).parse( + @argparse.Command("demo", flags=[FlagArg("v", long="v", action=Version)]).parse( argv=[], env=empty_env(), ) @@ -1694,7 +1702,7 @@ test "builtin and custom help/version dispatch edge paths" { } let long_help = @argparse.Command("demo", flags=[ - Flag("assist", long="assist", action=Help), + FlagArg("assist", long="assist", action=Help), ]) inspect( long_help.render_help(), @@ -1709,7 +1717,7 @@ test "builtin and custom help/version dispatch edge paths" { ) let short_help = @argparse.Command("demo", flags=[ - Flag("assist", short='?', action=Help), + FlagArg("assist", short='?', action=Help), ]) inspect( short_help.render_help(), @@ -1726,7 +1734,7 @@ test "builtin and custom help/version dispatch edge paths" { ///| test "subcommand lookup falls back to positional value" { - let cmd = @argparse.Command("demo", positionals=[Positional("input")], subcommands=[ + let cmd = @argparse.Command("demo", positionals=[PositionArg("input")], subcommands=[ Command("run"), ]) let parsed = cmd.parse(argv=["raw"], env=empty_env()) catch { _ => panic() } @@ -1779,8 +1787,8 @@ test "group requires/conflicts can target argument names" { let requires_cmd = @argparse.Command( "demo", groups=[ArgGroup("mode", args=["fast"], requires=["config"])], - flags=[Flag("fast", long="fast")], - options=[Optional("config", long="config")], + flags=[FlagArg("fast", long="fast")], + options=[OptionArg("config", long="config")], ) let ok = requires_cmd.parse( @@ -1818,8 +1826,8 @@ test "group requires/conflicts can target argument names" { let conflicts_cmd = @argparse.Command( "demo", groups=[ArgGroup("mode", args=["fast"], conflicts_with=["config"])], - flags=[Flag("fast", long="fast")], - options=[Optional("config", long="config")], + flags=[FlagArg("fast", long="fast")], + options=[OptionArg("config", long="config")], ) try @@ -1854,7 +1862,7 @@ test "group requires/conflicts can target argument names" { ///| test "group without members has no parse effect" { let cmd = @argparse.Command("demo", groups=[ArgGroup("known")], flags=[ - Flag("x", long="x"), + FlagArg("x", long="x"), ]) let parsed = cmd.parse(argv=["--x"], env=empty_env()) catch { _ => panic() } assert_true(parsed.flags is { "x": true, .. }) @@ -1866,7 +1874,7 @@ test "group without members has no parse effect" { test "arg validation catches unknown requires target" { try @argparse.Command("demo", options=[ - Optional("mode", long="mode", requires=["missing"]), + OptionArg("mode", long="mode", requires=["missing"]), ]).parse(argv=["--mode", "fast"], env=empty_env()) catch { err => @@ -1885,7 +1893,7 @@ test "arg validation catches unknown requires target" { test "arg validation catches unknown conflicts_with target" { try @argparse.Command("demo", options=[ - Optional("mode", long="mode", conflicts_with=["missing"]), + OptionArg("mode", long="mode", conflicts_with=["missing"]), ]).parse(argv=["--mode", "fast"], env=empty_env()) catch { err => @@ -1905,7 +1913,7 @@ test "empty groups without presence do not fail" { let grouped_ok = @argparse.Command( "demo", groups=[ArgGroup("left", args=["l"]), ArgGroup("right", args=["r"])], - flags=[Flag("l", long="left"), Flag("r", long="right")], + flags=[FlagArg("l", long="left"), FlagArg("r", long="right")], ) let parsed = grouped_ok.parse(argv=["--left"], env=empty_env()) catch { _ => panic() @@ -1916,13 +1924,13 @@ test "empty groups without presence do not fail" { ///| test "help rendering edge paths stay stable" { let required_many = @argparse.Command("demo", positionals=[ - Positional("files", num_args=ValueRange(lower=1)), + PositionArg("files", num_args=ValueRange(lower=1)), ]) let required_help = required_many.render_help() assert_true(required_help.has_prefix("Usage: demo ")) let short_only_builtin = @argparse.Command("demo", options=[ - Optional("helpopt", long="help"), + OptionArg("helpopt", long="help"), ]) let short_only_text = short_only_builtin.render_help() assert_true(short_only_text.has_prefix("Usage: demo")) @@ -1946,7 +1954,7 @@ test "help rendering edge paths stay stable" { } let long_only_builtin = @argparse.Command("demo", flags=[ - Flag("custom_h", short='h'), + FlagArg("custom_h", short='h'), ]) let long_only_text = long_only_builtin.render_help() assert_true(long_only_text.has_prefix("Usage: demo")) @@ -1964,7 +1972,7 @@ test "help rendering edge paths stay stable" { assert_true(empty_options_help.has_prefix("Usage: demo")) let implicit_group = @argparse.Command("demo", positionals=[ - Positional("item"), + PositionArg("item"), ]) let implicit_group_help = implicit_group.render_help() assert_true(implicit_group_help.has_prefix("Usage: demo [item]")) @@ -1978,7 +1986,7 @@ test "help rendering edge paths stay stable" { ///| test "unified error message formatting remains stable" { - let cmd = @argparse.Command("demo", options=[Optional("tag", long="tag")]) + let cmd = @argparse.Command("demo", options=[OptionArg("tag", long="tag")]) try cmd.parse(argv=["--oops"], env=empty_env()) catch { err => @@ -2022,14 +2030,14 @@ test "unified error message formatting remains stable" { ///| test "options require one value per occurrence" { let with_value = @argparse.Command("demo", options=[ - Optional("tag", long="tag"), + OptionArg("tag", long="tag"), ]).parse(argv=["--tag", "x"], env=empty_env()) catch { _ => panic() } assert_true(with_value.values is { "tag": ["x"], .. }) try - @argparse.Command("demo", options=[Optional("tag", long="tag")]).parse( + @argparse.Command("demo", options=[OptionArg("tag", long="tag")]).parse( argv=["--tag"], env=empty_env(), ) @@ -2055,8 +2063,8 @@ test "options require one value per occurrence" { ///| test "short options require one value before next option token" { - let cmd = @argparse.Command("demo", flags=[Flag("verbose", short='v')], options=[ - Optional("x", short='x'), + let cmd = @argparse.Command("demo", flags=[FlagArg("verbose", short='v')], options=[ + OptionArg("x", short='x'), ]) let ok = cmd.parse(argv=["-x", "a", "-v"], env=empty_env()) catch { _ => panic() @@ -2088,8 +2096,8 @@ test "short options require one value before next option token" { ///| test "version action dispatches on custom long and short flags" { let cmd = @argparse.Command("demo", version="2.0.0", flags=[ - Flag("show_long", long="show-version", action=Version), - Flag("show_short", short='S', action=Version), + FlagArg("show_long", long="show-version", action=Version), + FlagArg("show_short", short='S', action=Version), ]) inspect( @@ -2134,7 +2142,7 @@ test "global version action keeps parent version text in subcommand context" { "demo", version="1.0.0", flags=[ - Flag( + FlagArg( "show_version", short='S', long="show-version", @@ -2195,8 +2203,8 @@ test "subcommand help puts required options in usage" { Command( "run", about="Run a file", - options=[Optional("mode", short='m', required=true)], - positionals=[Positional("file", num_args=@argparse.ValueRange::single())], + options=[OptionArg("mode", short='m', required=true)], + positionals=[PositionArg("file", num_args=@argparse.ValueRange::single())], ), ]) @@ -2228,7 +2236,7 @@ test "subcommand help puts required options in usage" { ///| test "required and env-fed ranged values validate after parsing" { let required_cmd = @argparse.Command("demo", options=[ - Optional("input", long="input", required=true), + OptionArg("input", long="input", required=true), ]) try required_cmd.parse(argv=[], env=empty_env()) catch { err => @@ -2250,7 +2258,7 @@ test "required and env-fed ranged values validate after parsing" { } let env_min_cmd = @argparse.Command("demo", options=[ - Optional("pair", long="pair", env="PAIR"), + OptionArg("pair", long="pair", env="PAIR"), ]) let env_value = env_min_cmd.parse(argv=[], env={ "PAIR": "one" }) catch { _ => panic() @@ -2262,9 +2270,9 @@ test "required and env-fed ranged values validate after parsing" { ///| test "positionals keep declaration order with ranged positional" { let cmd = @argparse.Command("demo", positionals=[ - Positional("late", num_args=ValueRange(lower=2, upper=2)), - Positional("first"), - Positional("mid"), + PositionArg("late", num_args=ValueRange(lower=2, upper=2)), + PositionArg("first"), + PositionArg("mid"), ]) let parsed = cmd.parse(argv=["a", "b", "c", "d"], env=empty_env()) catch { @@ -2278,8 +2286,8 @@ test "positionals keep declaration order with ranged positional" { ///| test "mixed indexed and unindexed positionals keep inferred order" { let cmd = @argparse.Command("demo", positionals=[ - Positional("first"), - Positional("second"), + PositionArg("first"), + PositionArg("second"), ]) let parsed = cmd.parse(argv=["a", "b"], env=empty_env()) catch { @@ -2290,7 +2298,7 @@ test "mixed indexed and unindexed positionals keep inferred order" { ///| test "single positional parses without explicit index metadata" { - let parsed = @argparse.Command("demo", positionals=[Positional("late")]).parse( + let parsed = @argparse.Command("demo", positionals=[PositionArg("late")]).parse( argv=["x"], env=empty_env(), ) catch { @@ -2302,7 +2310,7 @@ test "single positional parses without explicit index metadata" { ///| test "positional num_args lower bound rejects missing argv values" { let cmd = @argparse.Command("demo", positionals=[ - Positional("first", num_args=ValueRange(lower=2, upper=3)), + PositionArg("first", num_args=ValueRange(lower=2, upper=3)), ]) try cmd.parse(argv=[], env=empty_env()) catch { @@ -2330,8 +2338,8 @@ test "positional num_args lower bound rejects missing argv values" { ///| test "positional max clamp leaves trailing value for next positional" { let cmd = @argparse.Command("demo", positionals=[ - Positional("items", num_args=ValueRange(lower=0, upper=2)), - Positional("tail"), + PositionArg("items", num_args=ValueRange(lower=0, upper=2)), + PositionArg("tail"), ]) let parsed = cmd.parse(argv=["a", "b", "c"], env=empty_env()) catch { @@ -2345,11 +2353,11 @@ test "options with allow_hyphen_values accept option-like single values" { let cmd = @argparse.Command( "demo", flags=[ - Flag("verbose", long="verbose"), - Flag("cache", long="cache", negatable=true), - Flag("quiet", short='q'), + FlagArg("verbose", long="verbose"), + FlagArg("cache", long="cache", negatable=true), + FlagArg("quiet", short='q'), ], - options=[Optional("arg", long="arg", allow_hyphen_values=true)], + options=[OptionArg("arg", long="arg", allow_hyphen_values=true)], ) let known_long = cmd.parse(argv=["--arg", "--verbose"], env=empty_env()) catch { @@ -2380,9 +2388,13 @@ test "options with allow_hyphen_values accept option-like single values" { let cmd_with_rest = @argparse.Command( "demo", - options=[Optional("arg", long="arg", allow_hyphen_values=true)], + options=[OptionArg("arg", long="arg", allow_hyphen_values=true)], positionals=[ - Positional("rest", num_args=ValueRange(lower=0), allow_hyphen_values=true), + PositionArg( + "rest", + num_args=ValueRange(lower=0), + allow_hyphen_values=true, + ), ], ) let sentinel_stop = cmd_with_rest.parse( @@ -2396,9 +2408,11 @@ test "options with allow_hyphen_values accept option-like single values" { ///| test "single-value options avoid consuming additional option values" { - let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")], options=[ - Optional("one", long="one"), - ]) + let cmd = @argparse.Command( + "demo", + flags=[FlagArg("verbose", long="verbose")], + options=[OptionArg("one", long="one")], + ) let parsed = cmd.parse(argv=["--one", "x", "--verbose"], env=empty_env()) catch { _ => panic() @@ -2409,9 +2423,11 @@ test "single-value options avoid consuming additional option values" { ///| test "missing option values are reported when next token is another option" { - let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")], options=[ - Optional("arg", long="arg"), - ]) + let cmd = @argparse.Command( + "demo", + flags=[FlagArg("verbose", long="verbose")], + options=[OptionArg("arg", long="arg")], + ) let ok = cmd.parse(argv=["--arg", "x", "--verbose"], env=empty_env()) catch { _ => panic() @@ -2442,7 +2458,7 @@ test "missing option values are reported when next token is another option" { ///| test "short-only set options use short label in duplicate errors" { - let cmd = @argparse.Command("demo", options=[Optional("mode", short='m')]) + let cmd = @argparse.Command("demo", options=[OptionArg("mode", short='m')]) try cmd.parse(argv=["-m", "a", "-m", "b"], env=empty_env()) catch { err => inspect( @@ -2466,7 +2482,7 @@ test "short-only set options use short label in duplicate errors" { ///| test "unknown short suggestion can be absent" { let cmd = @argparse.Command("demo", disable_help_flag=true, options=[ - Optional("name", long="name"), + OptionArg("name", long="name"), ]) try cmd.parse(argv=["-x"], env=empty_env()) catch { @@ -2491,7 +2507,7 @@ test "unknown short suggestion can be absent" { ///| test "setfalse flags apply false when present" { let cmd = @argparse.Command("demo", flags=[ - Flag("failfast", long="failfast", action=SetFalse), + FlagArg("failfast", long="failfast", action=SetFalse), ]) let parsed = cmd.parse(argv=["--failfast"], env=empty_env()) catch { _ => panic() @@ -2502,8 +2518,8 @@ test "setfalse flags apply false when present" { ///| test "allow_hyphen positional treats unknown long token as value" { - let cmd = @argparse.Command("demo", flags=[Flag("known", long="known")], positionals=[ - Positional("input", allow_hyphen_values=true), + let cmd = @argparse.Command("demo", flags=[FlagArg("known", long="known")], positionals=[ + PositionArg("input", allow_hyphen_values=true), ]) let parsed = cmd.parse(argv=["--mystery"], env=empty_env()) catch { _ => panic() @@ -2516,8 +2532,8 @@ test "global value from child default is merged back to parent" { let cmd = @argparse.Command( "demo", options=[ - Optional("mode", long="mode", default_values=["safe"], global=true), - Optional("unused", long="unused", global=true), + OptionArg("mode", long="mode", default_values=["safe"], global=true), + OptionArg("unused", long="unused", global=true), ], subcommands=[Command("run")], ) @@ -2536,7 +2552,7 @@ test "global value from child default is merged back to parent" { test "env-only global is propagated to nested subcommand matches" { let cmd = @argparse.Command( "demo", - options=[Optional("level", long="", env="LEVEL", global=true)], + options=[OptionArg("level", long="", env="LEVEL", global=true)], subcommands=[Command("run", subcommands=[Command("leaf")])], ) @@ -2560,10 +2576,10 @@ test "child global arg with inherited global name updates parent global" { let cmd = @argparse.Command( "demo", options=[ - Optional("mode", long="mode", default_values=["safe"], global=true), + OptionArg("mode", long="mode", default_values=["safe"], global=true), ], subcommands=[ - Command("run", options=[Optional("mode", long="mode", global=true)]), + Command("run", options=[OptionArg("mode", long="mode", global=true)]), ], ) @@ -2584,7 +2600,7 @@ test "child global override env/default win over inherited definition" { let cmd = @argparse.Command( "demo", options=[ - Optional( + OptionArg( "mode", long="mode", env="ROOT_MODE", @@ -2594,7 +2610,7 @@ test "child global override env/default win over inherited definition" { ], subcommands=[ Command("run", options=[ - Optional( + OptionArg( "mode", long="mode", env="RUN_MODE", @@ -2633,10 +2649,10 @@ test "child global override env/default win over inherited definition" { test "inherited argv global satisfies child required global override" { let cmd = @argparse.Command( "demo", - options=[Optional("mode", long="mode", global=true)], + options=[OptionArg("mode", long="mode", global=true)], subcommands=[ Command("run", options=[ - Optional("mode", long="mode", required=true, global=true), + OptionArg("mode", long="mode", required=true, global=true), ]), ], ) @@ -2659,7 +2675,7 @@ test "child local arg shadowing inherited global is rejected at build time" { @argparse.Command( "demo", options=[ - Optional( + OptionArg( "mode", long="mode", env="MODE", @@ -2667,7 +2683,7 @@ test "child local arg shadowing inherited global is rejected at build time" { global=true, ), ], - subcommands=[Command("run", options=[Optional("mode", long="mode")])], + subcommands=[Command("run", options=[OptionArg("mode", long="mode")])], ).parse(argv=["run"], env=empty_env()) catch { err => @@ -2686,7 +2702,9 @@ test "child local arg shadowing inherited global is rejected at build time" { test "global append env value from child is merged back to parent" { let cmd = @argparse.Command( "demo", - options=[Optional("tag", long="tag", action=Append, env="TAG", global=true)], + options=[ + OptionArg("tag", long="tag", action=Append, env="TAG", global=true), + ], subcommands=[Command("run")], ) @@ -2706,7 +2724,7 @@ test "global append env value from child is merged back to parent" { test "global flag set in child argv is merged back to parent" { let cmd = @argparse.Command( "demo", - flags=[Flag("verbose", long="verbose", global=true)], + flags=[FlagArg("verbose", long="verbose", global=true)], subcommands=[Command("run")], ) @@ -2727,7 +2745,13 @@ test "global count negation after subcommand resets merged state" { let cmd = @argparse.Command( "demo", flags=[ - Flag("verbose", long="verbose", action=Count, negatable=true, global=true), + FlagArg( + "verbose", + long="verbose", + action=Count, + negatable=true, + global=true, + ), ], subcommands=[Command("run")], ) @@ -2753,7 +2777,7 @@ test "global count negation after subcommand resets merged state" { test "global set option rejects duplicate occurrences across subcommands" { let cmd = @argparse.Command( "demo", - options=[Optional("mode", long="mode", global=true)], + options=[OptionArg("mode", long="mode", global=true)], subcommands=[Command("run")], ) try @@ -2787,9 +2811,9 @@ test "global override with incompatible inherited type is rejected" { try @argparse.Command( "demo", - options=[Optional("mode", long="mode", required=true, global=true)], + options=[OptionArg("mode", long="mode", required=true, global=true)], subcommands=[ - Command("run", flags=[Flag("mode", long="mode", global=true)]), + Command("run", flags=[FlagArg("mode", long="mode", global=true)]), ], ).parse(argv=["run", "--mode"], env=empty_env()) catch { @@ -2810,8 +2834,8 @@ test "child local long alias collision with inherited global is rejected" { try @argparse.Command( "demo", - flags=[Flag("verbose", long="verbose", global=true)], - subcommands=[Command("run", options=[Optional("local", long="verbose")])], + flags=[FlagArg("verbose", long="verbose", global=true)], + subcommands=[Command("run", options=[OptionArg("local", long="verbose")])], ).parse(argv=["run", "--verbose"], env=empty_env()) catch { err => @@ -2829,9 +2853,11 @@ test "child local long alias collision with inherited global is rejected" { ///| test "child local short alias collision with inherited global is rejected" { try - @argparse.Command("demo", flags=[Flag("verbose", short='v', global=true)], subcommands=[ - Command("run", options=[Optional("local", short='v')]), - ]).parse(argv=["run", "-v"], env=empty_env()) + @argparse.Command( + "demo", + flags=[FlagArg("verbose", short='v', global=true)], + subcommands=[Command("run", options=[OptionArg("local", short='v')])], + ).parse(argv=["run", "-v"], env=empty_env()) catch { err => inspect( @@ -2851,7 +2877,7 @@ test "nested subcommands inherit finalized globals from ancestors" { let mid = @argparse.Command("mid", subcommands=[leaf]) let cmd = @argparse.Command( "demo", - flags=[Flag("verbose", long="verbose", global=true)], + flags=[FlagArg("verbose", long="verbose", global=true)], subcommands=[mid], ) @@ -2870,14 +2896,14 @@ test "nested subcommands inherit finalized globals from ancestors" { ///| test "non-bmp short option token does not panic" { - let cmd = @argparse.Command("demo", flags=[Flag("party", short='🎉')]) + let cmd = @argparse.Command("demo", flags=[FlagArg("party", short='🎉')]) let parsed = cmd.parse(argv=["-🎉"], env=empty_env()) catch { _ => panic() } assert_true(parsed.flags is { "party": true, .. }) } ///| test "non-bmp hyphen token reports unknown argument without panic" { - let cmd = @argparse.Command("demo", positionals=[Positional("value")]) + let cmd = @argparse.Command("demo", positionals=[PositionArg("value")]) try cmd.parse(argv=["-🎉"], env=empty_env()) catch { err => inspect( @@ -2903,7 +2929,7 @@ test "non-bmp hyphen token reports unknown argument without panic" { ///| test "option env values remain string values instead of flags" { let cmd = @argparse.Command("demo", options=[ - Optional("mode", long="mode", env="MODE"), + OptionArg("mode", long="mode", env="MODE"), ]) let parsed = cmd.parse(argv=[], env={ "MODE": "fast" }) catch { _ => panic() } assert_true(parsed.values is { "mode": ["fast"], .. }) @@ -2916,12 +2942,12 @@ test "nested global override deduplicates count merge by name" { let leaf = @argparse.Command("leaf") let mid = @argparse.Command( "mid", - flags=[Flag("verbose", long="verbose", action=Count, global=true)], + flags=[FlagArg("verbose", long="verbose", action=Count, global=true)], subcommands=[leaf], ) let root = @argparse.Command( "root", - flags=[Flag("verbose", long="verbose", action=Count, global=true)], + flags=[FlagArg("verbose", long="verbose", action=Count, global=true)], subcommands=[mid], ) @@ -2942,12 +2968,12 @@ test "nested global override keeps single set value without false duplicate erro let leaf = @argparse.Command("leaf") let mid = @argparse.Command( "mid", - options=[Optional("mode", long="mode", global=true)], + options=[OptionArg("mode", long="mode", global=true)], subcommands=[leaf], ) let root = @argparse.Command( "root", - options=[Optional("mode", long="mode", global=true)], + options=[OptionArg("mode", long="mode", global=true)], subcommands=[mid], ) @@ -2971,10 +2997,10 @@ test "global override with different negatable setting is rejected" { try @argparse.Command( "demo", - flags=[Flag("verbose", long="verbose", negatable=true, global=true)], + flags=[FlagArg("verbose", long="verbose", negatable=true, global=true)], subcommands=[ Command("run", flags=[ - Flag("verbose", long="verbose", negatable=false, global=true), + FlagArg("verbose", long="verbose", negatable=false, global=true), ]), ], ).parse(argv=["run"], env=empty_env()) @@ -2994,7 +3020,7 @@ test "global override with different negatable setting is rejected" { ///| test "positional range 0..1 renders as single optional value" { let cmd = @argparse.Command("demo", positionals=[ - Positional("x", num_args=ValueRange(lower=0, upper=1)), + PositionArg("x", num_args=ValueRange(lower=0, upper=1)), ]) inspect( cmd.render_help(), diff --git a/argparse/argparse_test.mbt b/argparse/argparse_test.mbt index 71754b0e6..add337644 100644 --- a/argparse/argparse_test.mbt +++ b/argparse/argparse_test.mbt @@ -21,9 +21,9 @@ fn empty_env() -> Map[String, String] { test "declarative parse basics" { let cmd = @argparse.Command( "demo", - flags=[Flag("verbose", short='v', long="verbose")], - options=[Optional("count", long="count", env="COUNT")], - positionals=[Positional("name")], + flags=[FlagArg("verbose", short='v', long="verbose")], + options=[OptionArg("count", long="count", env="COUNT")], + positionals=[PositionArg("name")], ) let matches = cmd.parse(argv=["-v", "--count", "3", "alice"], env=empty_env()) catch { _ => panic() @@ -37,8 +37,8 @@ test "declarative parse basics" { ///| test "long defaults to name when omitted" { - let cmd = @argparse.Command("demo", flags=[Flag("verbose")], options=[ - Optional("count"), + let cmd = @argparse.Command("demo", flags=[FlagArg("verbose")], options=[ + OptionArg("count"), ]) let matches = cmd.parse(argv=["--verbose", "--count", "3"], env=empty_env()) catch { _ => panic() @@ -51,8 +51,8 @@ test "long defaults to name when omitted" { test "long empty string disables long alias" { let cmd = @argparse.Command( "demo", - flags=[Flag("verbose", short='v', long="")], - options=[Optional("count", short='c', long="")], + flags=[FlagArg("verbose", short='v', long="")], + options=[OptionArg("count", short='c', long="")], ) let matches = cmd.parse(argv=["-v", "-c", "3"], env=empty_env()) catch { @@ -105,8 +105,8 @@ test "long empty string disables long alias" { ///| test "declaration order controls positional parsing" { let cmd = @argparse.Command("demo", positionals=[ - Positional("first"), - Positional("second"), + PositionArg("first"), + PositionArg("second"), ]) let parsed = cmd.parse(argv=["a", "b"], env=empty_env()) catch { @@ -118,8 +118,8 @@ test "declaration order controls positional parsing" { ///| test "bounded non-last positional remains supported" { let cmd = @argparse.Command("demo", positionals=[ - Positional("first", num_args=ValueRange(lower=1, upper=2)), - Positional("second", num_args=@argparse.ValueRange::single()), + PositionArg("first", num_args=ValueRange(lower=1, upper=2)), + PositionArg("second", num_args=@argparse.ValueRange::single()), ]) let two = cmd.parse(argv=["a", "b"], env=empty_env()) catch { _ => panic() } @@ -134,7 +134,7 @@ test "bounded non-last positional remains supported" { ///| test "negatable flag preserves false state" { let cmd = @argparse.Command("demo", flags=[ - Flag("cache", long="cache", negatable=true), + FlagArg("cache", long="cache", negatable=true), ]) let no_cache = cmd.parse(argv=["--no-cache"], env=empty_env()) catch { @@ -147,7 +147,7 @@ test "negatable flag preserves false state" { ///| test "parse failure message contains error and contextual help" { let cmd = @argparse.Command("demo", options=[ - Optional("count", long="count", about="repeat count"), + OptionArg("count", long="count", about="repeat count"), ]) try cmd.parse(argv=["--bad"], env=empty_env()) catch { @@ -173,7 +173,7 @@ test "parse failure message contains error and contextual help" { ///| test "subcommand parse errors include subcommand help" { let cmd = @argparse.Command("demo", subcommands=[ - Command("echo", options=[Optional("times", long="times")]), + Command("echo", options=[OptionArg("times", long="times")]), ]) try cmd.parse(argv=["echo", "--bad"], env=empty_env()) catch { @@ -199,7 +199,7 @@ test "subcommand parse errors include subcommand help" { ///| test "build errors are surfaced as validation failure message" { let cmd = @argparse.Command("demo", flags=[ - Flag("fast", long="fast", requires=["missing"]), + FlagArg("fast", long="fast", requires=["missing"]), ]) try cmd.parse(argv=[], env=empty_env()) catch { @@ -217,7 +217,7 @@ test "build errors are surfaced as validation failure message" { ///| test "unknown argument keeps suggestion in final message" { - let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")]) + let cmd = @argparse.Command("demo", flags=[FlagArg("verbose", long="verbose")]) try cmd.parse(argv=["--verbse"], env=empty_env()) catch { err => @@ -246,9 +246,9 @@ test "render_help remains available for pure formatting" { let cmd = @argparse.Command( "demo", about="Demo command", - flags=[Flag("verbose", short='v', long="verbose")], - options=[Optional("count", long="count")], - positionals=[Positional("name")], + flags=[FlagArg("verbose", short='v', long="verbose")], + options=[OptionArg("count", long="count")], + positionals=[PositionArg("name")], subcommands=[Command("echo")], ) @@ -300,9 +300,11 @@ test "display help and version" { ///| test "parse error show is readable" { - let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")], positionals=[ - Positional("name"), - ]) + let cmd = @argparse.Command( + "demo", + flags=[FlagArg("verbose", long="verbose")], + positionals=[PositionArg("name")], + ) try cmd.parse(argv=["--verbse"], env=empty_env()) catch { err => @@ -354,8 +356,8 @@ test "parse error show is readable" { ///| test "relationships and num args" { let requires_cmd = @argparse.Command("demo", options=[ - Optional("mode", long="mode", requires=["config"]), - Optional("config", long="config"), + OptionArg("mode", long="mode", requires=["config"]), + OptionArg("config", long="config"), ]) try requires_cmd.parse(argv=["--mode", "fast"], env=empty_env()) catch { @@ -379,7 +381,7 @@ test "relationships and num args" { } let appended = @argparse.Command("demo", options=[ - Optional("tag", long="tag", action=Append), + OptionArg("tag", long="tag", action=Append), ]).parse(argv=["--tag", "a", "--tag", "b", "--tag", "c"], env=empty_env()) catch { _ => panic() } @@ -393,7 +395,7 @@ test "arg groups required and multiple" { groups=[ ArgGroup("mode", required=true, multiple=false, args=["fast", "slow"]), ], - flags=[Flag("fast", long="fast"), Flag("slow", long="slow")], + flags=[FlagArg("fast", long="fast"), FlagArg("slow", long="slow")], ) try cmd.parse(argv=[], env=empty_env()) catch { @@ -452,7 +454,7 @@ test "arg groups requires and conflicts" { ArgGroup("mode", args=["fast"], requires=["output"]), ArgGroup("output", args=["json"]), ], - flags=[Flag("fast", long="fast"), Flag("json", long="json")], + flags=[FlagArg("fast", long="fast"), FlagArg("json", long="json")], ) try requires_cmd.parse(argv=["--fast"], env=empty_env()) catch { @@ -486,7 +488,7 @@ test "arg groups requires and conflicts" { ArgGroup("mode", args=["fast"], conflicts_with=["output"]), ArgGroup("output", args=["json"]), ], - flags=[Flag("fast", long="fast"), Flag("json", long="json")], + flags=[FlagArg("fast", long="fast"), FlagArg("json", long="json")], ) try conflict_cmd.parse(argv=["--fast", "--json"], env=empty_env()) catch { @@ -516,7 +518,7 @@ test "arg groups requires and conflicts" { ///| test "subcommand parsing" { - let echo = @argparse.Command("echo", positionals=[Positional("msg")]) + let echo = @argparse.Command("echo", positionals=[PositionArg("msg")]) let root = @argparse.Command("root", subcommands=[echo]) let matches = root.parse(argv=["echo", "hi"], env=empty_env()) catch { @@ -534,12 +536,14 @@ test "full help snapshot" { "demo", about="Demo command", flags=[ - Flag("verbose", short='v', long="verbose", about="Enable verbose mode"), + FlagArg("verbose", short='v', long="verbose", about="Enable verbose mode"), ], options=[ - Optional("count", long="count", about="Repeat count", default_values=["1"]), + OptionArg("count", long="count", about="Repeat count", default_values=[ + "1", + ]), ], - positionals=[Positional("name", about="Target name")], + positionals=[PositionArg("name", about="Target name")], subcommands=[Command("echo", about="Echo a message")], ) inspect( @@ -568,7 +572,7 @@ test "full help snapshot" { ///| test "value source precedence argv env default" { let cmd = @argparse.Command("demo", options=[ - Optional("level", long="level", env="LEVEL", default_values=["1"]), + OptionArg("level", long="level", env="LEVEL", default_values=["1"]), ]) let from_default = cmd.parse(argv=[], env=empty_env()) catch { _ => panic() } @@ -589,7 +593,7 @@ test "value source precedence argv env default" { ///| test "omitted env does not read process environment by default" { let cmd = @argparse.Command("demo", options=[ - Optional("count", long="count", env="COUNT"), + OptionArg("count", long="count", env="COUNT"), ]) let matches = cmd.parse(argv=[]) catch { _ => panic() } assert_true(matches.values is { "count"? : None, .. }) @@ -602,8 +606,8 @@ test "options and multiple values" { let cmd = @argparse.Command( "demo", options=[ - Optional("count", short='c', long="count"), - Optional("tag", long="tag", action=Append), + OptionArg("count", short='c', long="count"), + OptionArg("tag", long="tag", action=Append), ], subcommands=[serve], ) @@ -632,10 +636,10 @@ test "options and multiple values" { ///| test "negatable and conflicts" { let cmd = @argparse.Command("demo", flags=[ - Flag("cache", long="cache", negatable=true), - Flag("failfast", long="failfast", action=SetFalse, negatable=true), - Flag("verbose", long="verbose", conflicts_with=["quiet"]), - Flag("quiet", long="quiet"), + FlagArg("cache", long="cache", negatable=true), + FlagArg("failfast", long="failfast", action=SetFalse, negatable=true), + FlagArg("verbose", long="verbose", conflicts_with=["quiet"]), + FlagArg("quiet", long="quiet"), ]) let no_cache = cmd.parse(argv=["--no-cache"], env=empty_env()) catch { @@ -674,7 +678,7 @@ test "negatable and conflicts" { ///| test "flag does not accept inline value" { - let cmd = @argparse.Command("demo", flags=[Flag("verbose", long="verbose")]) + let cmd = @argparse.Command("demo", flags=[FlagArg("verbose", long="verbose")]) try cmd.parse(argv=["--verbose=true"], env=empty_env()) catch { err => inspect( diff --git a/argparse/command.mbt b/argparse/command.mbt index 78b994bf2..93a4ebdc1 100644 --- a/argparse/command.mbt +++ b/argparse/command.mbt @@ -32,9 +32,9 @@ pub struct Command { /// Create a declarative command specification. fn new( name : StringView, - flags? : ArrayView[Flag], - options? : ArrayView[Optional], - positionals? : ArrayView[Positional], + flags? : ArrayView[FlagArg], + options? : ArrayView[OptionArg], + positionals? : ArrayView[PositionArg], subcommands? : ArrayView[Command], about? : StringView, version? : StringView, @@ -62,9 +62,9 @@ pub struct Command { /// - `hidden=true` omits this command from parent command listings. pub fn Command::new( name : StringView, - flags? : ArrayView[Flag] = [], - options? : ArrayView[Optional] = [], - positionals? : ArrayView[Positional] = [], + flags? : ArrayView[FlagArg] = [], + options? : ArrayView[OptionArg] = [], + positionals? : ArrayView[PositionArg] = [], subcommands? : ArrayView[Command] = [], about? : StringView, version? : StringView, @@ -229,9 +229,9 @@ fn find_decl_subcommand(subs : Array[Command], name : String) -> Command? { ///| fn collect_args( - flags : ArrayView[Flag], - options : ArrayView[Optional], - positionals : ArrayView[Positional], + flags : ArrayView[FlagArg], + options : ArrayView[OptionArg], + positionals : ArrayView[PositionArg], ) -> (Array[Arg], ArgBuildError?) { let args : Array[Arg] = [] for flag in flags { diff --git a/argparse/parser_lookup_wbtest.mbt b/argparse/parser_lookup_wbtest.mbt index 36642bc41..7d5a80dd8 100644 --- a/argparse/parser_lookup_wbtest.mbt +++ b/argparse/parser_lookup_wbtest.mbt @@ -17,12 +17,12 @@ test "resolve_help_target merges inherited globals by name" { let leaf = Command("leaf") let mid = Command( "mid", - options=[Optional("mode", long="mid-mode", global=true)], + options=[OptionArg("mode", long="mid-mode", global=true)], subcommands=[leaf], ) let root = Command( "demo", - options=[Optional("mode", long="root-mode", global=true)], + options=[OptionArg("mode", long="root-mode", global=true)], subcommands=[mid], ) diff --git a/argparse/pkg.generated.mbti b/argparse/pkg.generated.mbti index 4cd5b27d9..431e79037 100644 --- a/argparse/pkg.generated.mbti +++ b/argparse/pkg.generated.mbti @@ -20,20 +20,13 @@ pub fn ArgGroup::new(StringView, required? : Bool, multiple? : Bool, args? : Arr pub struct Command { // private fields - fn new(StringView, flags? : ArrayView[Flag], options? : ArrayView[Optional], positionals? : ArrayView[Positional], 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]) -> Command + fn new(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]) -> Command } -pub fn Command::new(StringView, flags? : ArrayView[Flag], options? : ArrayView[Optional], positionals? : ArrayView[Positional], subcommands? : ArrayView[Self], 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]) -> Self +pub fn Command::new(StringView, flags? : ArrayView[FlagArg], options? : ArrayView[OptionArg], positionals? : ArrayView[PositionArg], subcommands? : ArrayView[Self], 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]) -> Self #as_free_fn pub fn Command::parse(Self, argv? : ArrayView[String], env? : Map[String, String]) -> Matches raise pub fn Command::render_help(Self) -> String -pub struct Flag { - // private fields - - fn new(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) -> Flag -} -pub fn Flag::new(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) -> Self - pub(all) enum FlagAction { SetTrue SetFalse @@ -44,6 +37,13 @@ pub(all) enum FlagAction { pub impl Eq for FlagAction pub impl Show for FlagAction +pub struct FlagArg { + // private fields + + fn new(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 +} +pub fn FlagArg::new(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) -> Self + pub struct Matches { flags : Map[String, Bool] values : Map[String, Array[String]] @@ -61,19 +61,19 @@ pub(all) enum OptionAction { pub impl Eq for OptionAction pub impl Show for OptionAction -pub struct Optional { +pub struct OptionArg { // private fields - fn new(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) -> Optional + fn new(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 } -pub fn Optional::new(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) -> Self +pub fn OptionArg::new(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) -> Self -pub struct Positional { +pub struct PositionArg { // private fields - fn new(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) -> Positional + fn new(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 } -pub fn Positional::new(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) -> Self +pub fn PositionArg::new(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) -> Self pub struct ValueRange { // private fields