///|
/// Defines an immutable command. Input collections are copied. Callback errors
/// propagate unchanged to the application host.
/// required_scope_notice replaces the default group scope note; "" disables it.
///
/// ```mbt check
/// test {
/// let cmd = @cmder.command(name="go", run=_ => ())
/// inspect(cmd.name, content="go")
/// }
/// ```
pub fn command(
name~ : String,
description~ : String = "",
options~ : ArrayView[OptionDef] = [],
positionals~ : ArrayView[PositionalDef] = [],
examples~ : ArrayView[String] = [],
subcommands~ : ArrayView[CommandDef] = [],
run? : (Context) -> Unit raise,
required_scope_notice? : String,
) -> CommandDef {
{
name,
description,
options: ReadOnlyArray::from_array(options),
positionals: ReadOnlyArray::from_array(positionals),
examples: ReadOnlyArray::from_array(examples),
subcommands: ReadOnlyArray::from_array(subcommands),
run,
required_scope_notice,
}
}
///|
/// Builds and validates an immutable application. Root commands support the
/// same options, positionals, examples and raising callbacks as subcommands.
/// Only explicitly global options are shared with descendants.
/// required_scope_notice replaces the default group scope note; "" disables it.
///
/// ```mbt check
/// test {
/// let count = Ref(0)
/// let app = @cmder.cli(name="demo", options=[@cmder.int("count", default=2)], run=ctx => {
/// count.val = ctx.get_int_required("count")
/// })
/// app.run(argv=[])
/// assert_eq(count.val, 2)
/// }
/// ```
pub fn cli(
name~ : String,
version~ : String = "0.0.0",
description~ : String = "",
options~ : ArrayView[OptionDef] = [],
positionals~ : ArrayView[PositionalDef] = [],
examples~ : ArrayView[String] = [],
commands~ : ArrayView[CommandDef] = [],
run? : (Context) -> Unit raise,
required_scope_notice? : String,
) -> CliApp raise CliError {
let root = command(
name~,
description~,
options~,
positionals~,
examples~,
subcommands=commands,
run?,
required_scope_notice?,
)
validate_definition(root, [], [])
{ root, version, }
}
///|
fn find_command_def(
commands : ArrayView[CommandDef],
name : String,
) -> CommandDef? {
commands.iter().find_first(cmd => cmd.name == name)
}
///|
/// Executes the command bound during parsing with this invocation's context.
/// It performs no parsing, output or process exit. Callback errors propagate
/// unchanged. Each call deliberately executes again. Host overrides must be
/// applied with with_overrides, which returns another validated invocation.
///
/// ```mbt check
/// test {
/// let called = Ref(false)
/// let app = @cmder.cli(name="demo", run=_ => called.val = true)
/// guard app.parse(argv=[]) is Parsed(call) else { fail("parse") }
/// assert_false(called.val)
/// call.dispatch()
/// assert_true(called.val)
/// }
/// ```
pub fn Invocation::dispatch(self : Invocation) -> Unit raise {
match self.selected.run {
Some(callback) => callback(self.context)
None => {
if self.command_path.is_empty() {
raise CliError::MissingCommand
}
if self.selected.subcommands.is_empty() {
raise CliError::MissingHandler(self.selected.name)
}
raise CliError::MissingSubcommand(
self.selected.name,
[
for cmd in self.selected.subcommands => cmd.name
],
)
}
}
}
///|
#cfg(target="js")
fn process_argv() -> ArrayView[String] {
match @env.args() {
[_, _, .. rest] => rest
_ => []
}
}
///|
#cfg(not(target="js"))
fn process_argv() -> ArrayView[String] {
match @env.args() {
[_, .. rest] => rest
[] => []
}
}
///|
/// Parses and dispatches, propagating parser and original callback errors.
/// Omit argv to use process arguments. Help and version print and exit with
/// status zero. Embedded hosts can use parse and Invocation::dispatch instead.
///
/// ```mbt check
/// test {
/// let called = Ref(false)
/// let app = @cmder.cli(name="demo", run=_ => called.val = true)
/// app.run(argv=[])
/// assert_true(called.val)
/// }
/// ```
pub fn CliApp::run(self : CliApp, argv? : ArrayView[String]) -> Unit raise {
let args = argv.unwrap_or_else(process_argv)
match self.parse(argv=args) {
Parsed(call) => call.dispatch()
Help(text) | Version(text) => {
println(text)
@env.exit(0)
}
Failed(error) => raise error
}
}
///|
/// Runs a standalone CLI, reporting parser or callback errors and exiting one.
/// Help/version still exit zero. Embedded hosts should use parse or run.
///
/// ```mbt check
/// test {
/// let app = @cmder.cli(name="demo", run=_ => ())
/// app.run_or_exit(argv=[])
/// }
/// ```
pub fn CliApp::run_or_exit(self : CliApp, argv? : ArrayView[String]) -> Unit {
self.run(argv?) catch {
error => {
eprintln(error)
@env.exit(1)
}
}
}