///|
fn validate_option_types(
options : ArrayView[OptionDef],
matches : @argparse.Matches,
) -> Unit raise CliError {
for option in options {
if option.kind is IntOpt(_) {
for value in matches.values.get(option.name).unwrap_or([]) {
ignore(parse_integer_value(option.name, value))
}
}
}
}
///|
fn invocation_from_matches(
app : CliApp,
matches : @argparse.Matches,
) -> Invocation raise CliError {
let path : Array[String] = []
let (selected, current, globals) = for command = app.root, current = matches, globals = [] {
guard current.subcommand is Some((name, child)) else {
break (command, current, globals)
}
// Reject explicitly supplied locals instead of silently discarding them.
for option in command.options {
if !option.global &&
current.sources.get(option.name) is Some(@argparse.ValueSource::Argv) {
raise CliError::UnexpectedLocalArgument(
command=([app.root.name] + path).join(" "),
name=option.name,
)
}
}
path.push(name)
guard find_command_def(command.subcommands, name) is Some(child_command) else {
raise CliError::UnknownCommand(path)
}
continue child_command, child, inherited_options(globals, command)
}
let visible = globals + selected.options[:].to_owned()
for option in visible {
if option.required && !current.values.contains(option.name) {
raise CliError::MissingOption(option.name)
}
}
for pos in selected.positionals {
if pos.required && current.values.get(pos.name).unwrap_or([]).is_empty() {
raise CliError::MissingPositional(pos.name)
}
}
validate_option_types(visible, current)
let specs : Map[String, ArgumentSpec] = Map([])
for option in visible {
specs[option.name] = Option(option)
}
for index, pos in selected.positionals {
specs[pos.name] = Positional(
pos,
multiple=positional_is_multiple(pos, index, selected.positionals.length()),
)
}
let flags : Map[String, Bool] = Map([])
let values : Map[String, Array[String]] = Map([])
let sources : Map[String, ValueSource] = Map([])
for name, _ in specs {
if current.flags.get(name) is Some(value) {
flags[name] = value
}
if current.values.get(name) is Some(value) {
values[name] = value.copy()
}
if current.sources.get(name) is Some(source) {
sources[name] = match source {
@argparse.ValueSource::Argv => Argv
@argparse.ValueSource::Env => Env
@argparse.ValueSource::Default => Default
}
}
}
{
command_path: ReadOnlyArray::from_array(path),
context: { flags, values, sources, specs, },
selected,
}
}
///|
/// Parses explicit argv without process I/O or callbacks. Structural failures
/// and help/version precede cmder validation. Explicit ancestor-local options
/// are rejected; missing required options use MissingOption in every scope.
/// Missing required positionals use MissingPositional. Integer syntax and
/// Int32 range are checked before returning an invocation.
///
/// ```mbt check
/// test {
/// let app = @cmder.cli(name="app", options=[@cmder.int("count")])
/// assert_true(
/// app.parse(argv=["--count", "bad"])
/// is Failed(@cmder.CliError::InvalidInteger("count", "bad")),
/// )
/// }
/// ```
pub fn CliApp::parse(self : CliApp, argv~ : ArrayView[String]) -> ParseOutcome {
let parser = build_parser(
self.root,
version=self.version,
defer_required=true,
)
match parser.parse_outcome(argv=normalize_integer_argv(self.root, argv)) {
Parsed(matches) =>
Parsed(invocation_from_matches(self, matches)) catch {
error => Failed(error)
}
Help(text) => Help(restore_parser_help(self, text, help_only=true))
Version(text) => Version(text)
Failed(error) => {
let text = error.to_string()
Failed(
CliError::ParseError(restore_parser_help(self, text, help_only=false)),
)
}
}
}