///|
/// Command-line parsing and dispatch for `mb`.
///
/// Argument handling mirrors `uv`: every command and subcommand responds to
/// `-h`/`--help` (which takes precedence over everything else), required
/// arguments and unknown options produce a clap-style usage error on stderr
/// with exit code 2, and `tool upgrade`/`tool uninstall` accept several names
/// or `--all`.
///|
pub const MB_VERSION : String = "0.1.4"
///|
fn tail(a : Array[String], from : Int) -> Array[String] {
a[from:].to_owned()
}
///|
/// Entry point used by `cmd/main`. Returns the process exit code.
pub fn run_cli(argv : Array[String]) -> Int raise MbError {
if argv.is_empty() {
print_help()
return 0
}
let rest = tail(argv, 1)
match argv[0] {
"run" => cli_run(rest)
"moon" => cli_moon(rest)
"tool" => cli_tool(rest)
"help" => cli_help(rest)
"version" | "--version" | "-V" => {
println("mb " + MB_VERSION)
0
}
"--help" | "-h" => {
print_help()
0
}
other => {
eprintln("error: unrecognized command '" + other + "'")
eprintln("")
eprintln("Usage: mb ")
eprintln("")
eprintln("For more information, try '--help'.")
2
}
}
}
///|
fn cli_help(rest : Array[String]) -> Int {
match rest.get(0) {
None => {
print_help()
0
}
Some(cmd) =>
match cmd {
"run" => {
print_run_help()
0
}
"moon" =>
match rest.get(1) {
None => {
print_moon_help()
0
}
Some(sub) => {
print_moon_sub_help(sub)
0
}
}
"tool" =>
match rest.get(1) {
None => {
print_tool_help()
0
}
Some(sub) => {
print_tool_sub_help(sub)
0
}
}
other => {
eprintln("error: unrecognized command '" + other + "'")
eprintln("")
eprintln("For more information, try '--help'.")
2
}
}
}
}
///|
fn has_help_flag(args : Array[String]) -> Bool {
for a in args {
if a == "-h" || a == "--help" {
return true
}
}
false
}
///|
/// clap-style usage error: printed on stderr, exit code 2.
fn usage_error(msg : String, usage : String) -> Int {
eprintln("error: " + msg)
eprintln("")
eprintln("Usage: " + usage)
eprintln("")
eprintln("For more information, try '--help'.")
2
}
///|
fn unknown_subcommand(usage : String, sub : String) -> Int {
eprintln("error: unrecognized subcommand '" + sub + "'")
eprintln("")
eprintln("Usage: " + usage)
eprintln("")
eprintln("For more information, try '--help'.")
2
}
///|
fn print_help() -> Unit {
let text =
$|mb — a MoonBit toolchain and package manager
$|
$|Usage: mb [OPTIONS]
$|
$|Commands:
$| run Run a command inside a managed MoonBit environment
$| moon Manage MoonBit toolchains
$| tool Install, list, upgrade and remove tools from mooncakes.io
$|
$|Options:
$| -h, --help Print help
$| -V, --version Print version
$|
$|Environment:
$| MB_HOME Data directory (default: ~/.local/share/mb)
$| MB_BIN_DIR Where executables are installed (default: ~/.local/bin)
println(text)
}
///|
/// `mb run` — mirrors `uv run`: everything after the subcommand's own options
/// is passed straight through to the command.
fn cli_run(args : Array[String]) -> Int raise MbError {
let mut moon = ""
let mut i = 0
while i < args.length() {
let a = args[i]
if a == "--moon" && i + 1 < args.length() {
moon = args[i + 1]
i = i + 2
} else if a.has_prefix("--moon=") {
moon = a[7:].to_owned()
i = i + 1
} else if a == "-h" || a == "--help" {
print_run_help()
return 0
} else {
break
}
}
cmd_run(moon, tail(args, i))
}
///|
fn print_run_help() -> Unit {
let text =
$|Run a command inside a managed MoonBit environment
$|
$|Usage: mb run [OPTIONS] [ARGS]...
$|
$|Arguments:
$| The command to run
$| [ARGS]... Arguments passed to the command
$|
$|Options:
$| --moon Use the given managed MoonBit toolchain
$| -h, --help Print help
println(text)
}
///|
fn cli_moon(args : Array[String]) -> Int raise MbError {
if args.is_empty() {
print_moon_help()
return 0
}
let sub = args[0]
if sub == "-h" || sub == "--help" || sub == "help" {
print_moon_help()
return 0
}
let rest = tail(args, 1)
if has_help_flag(rest) {
print_moon_sub_help(sub)
return 0
}
match sub {
"list" | "ls" =>
if !rest.is_empty() {
usage_error("unexpected argument '" + rest[0] + "'", "mb moon list")
} else {
moon_list()
0
}
"install" | "add" =>
match parse_version_flags(rest, "latest") {
Err(e) => usage_error(e, "mb moon install [OPTIONS] [VERSION]")
Ok((version, force)) => {
moon_install(version, force)
0
}
}
"uninstall" | "remove" | "rm" =>
match parse_required_arg(rest, "") {
Err(e) => usage_error(e, "mb moon uninstall ")
Ok(version) => {
moon_uninstall(version)
0
}
}
"default" | "use" =>
match parse_required_arg(rest, "") {
Err(e) => usage_error(e, "mb moon default ")
Ok(version) => {
moon_set_default(version)
0
}
}
"which" => {
let version = if rest.is_empty() {
""
} else {
match parse_required_arg(rest, "") {
Err(e) => return usage_error(e, "mb moon which [VERSION]")
Ok(v) => v
}
}
moon_which(version)
0
}
"latest" =>
if !rest.is_empty() {
usage_error("unexpected argument '" + rest[0] + "'", "mb moon latest")
} else {
match latest_moon_cli_version() {
Some(v) => {
println(v)
0
}
None =>
raise MbError("could not determine the latest MoonBit version")
}
}
other => unknown_subcommand("mb moon ", other)
}
}
///|
fn print_moon_help() -> Unit {
let text =
$|Manage MoonBit toolchains
$|
$|Usage: mb moon [OPTIONS]
$|
$|Commands:
$| list List installed and available toolchains
$| install Install a MoonBit toolchain
$| uninstall Remove an installed toolchain
$| default Set the default toolchain
$| which Print the path to a toolchain's `moon`
$| latest Print the latest published toolchain version
$|
$|Options:
$| -h, --help Print help
println(text)
}
///|
fn print_moon_sub_help(sub : String) -> Unit {
match sub {
"list" | "ls" => print_moon_list_help()
"install" | "add" => print_moon_install_help()
"uninstall" | "remove" | "rm" => print_moon_uninstall_help()
"default" | "use" => print_moon_default_help()
"which" => print_moon_which_help()
"latest" => print_moon_latest_help()
_ => print_moon_help()
}
}
///|
fn print_moon_list_help() -> Unit {
let text =
$|List installed MoonBit toolchains and the release channel
$|
$|Usage: mb moon list
$|
$|Options:
$| -h, --help Print help
println(text)
}
///|
fn print_moon_install_help() -> Unit {
let text =
$|Install a MoonBit toolchain
$|
$|Usage: mb moon install [OPTIONS] [VERSION]
$|
$|Arguments:
$| [VERSION] Version to install (default: latest)
$|
$|Options:
$| -f, --force Reinstall even if already installed
$| -h, --help Print help
println(text)
}
///|
fn print_moon_uninstall_help() -> Unit {
let text =
$|Remove an installed MoonBit toolchain
$|
$|Usage: mb moon uninstall
$|
$|Arguments:
$| The toolchain version to remove, e.g. 0.10.14
$|
$|Options:
$| -h, --help Print help
println(text)
}
///|
fn print_moon_default_help() -> Unit {
let text =
$|Set the default MoonBit toolchain
$|
$|Usage: mb moon default
$|
$|Arguments:
$| Toolchain version to select, e.g. 0.10.14
$|
$|Options:
$| -h, --help Print help
println(text)
}
///|
fn print_moon_which_help() -> Unit {
let text =
$|Print the path to a toolchain's `moon` executable
$|
$|Usage: mb moon which [VERSION]
$|
$|Arguments:
$| [VERSION] Toolchain version to inspect (default: active)
$|
$|Options:
$| -h, --help Print help
println(text)
}
///|
fn print_moon_latest_help() -> Unit {
let text =
$|Print the latest published MoonBit toolchain version
$|
$|Usage: mb moon latest
$|
$|Options:
$| -h, --help Print help
println(text)
}
///|
fn cli_tool(args : Array[String]) -> Int raise MbError {
if args.is_empty() {
print_tool_help()
return 0
}
let sub = args[0]
if sub == "-h" || sub == "--help" || sub == "help" {
print_tool_help()
return 0
}
let rest = tail(args, 1)
if has_help_flag(rest) {
print_tool_sub_help(sub)
return 0
}
match sub {
"install" | "add" =>
match parse_tool_install_flags(rest) {
Err(e) => usage_error(e, "mb tool install [OPTIONS] [@]")
Ok(opt) => {
tool_install(opt.spec, opt.moon, opt.bin, opt.force)
0
}
}
"upgrade" | "update" | "up" =>
match parse_names_flags(rest) {
Err(e) => usage_error(e, "mb tool upgrade [OPTIONS] ...")
Ok(o) => {
tool_upgrade(o.names, o.all, o.moon)
0
}
}
"list" | "ls" =>
match parse_list_flags(rest) {
Err(e) => usage_error(e, "mb tool list [OPTIONS]")
Ok(outdated) => {
tool_list(outdated)
0
}
}
"uninstall" | "remove" | "rm" =>
match parse_names_flags(rest) {
Err(e) => usage_error(e, "mb tool uninstall [OPTIONS] ...")
Ok(o) => {
tool_uninstall_many(o.names, o.all)
0
}
}
other => unknown_subcommand("mb tool ", other)
}
}
///|
fn print_tool_help() -> Unit {
let text =
$|Install, list, upgrade and remove tools from mooncakes.io
$|
$|Usage: mb tool [OPTIONS]
$|
$|Commands:
$| install Install a tool from a mooncakes.io module
$| upgrade Upgrade installed tools to their latest version
$| list List installed tools
$| uninstall Remove an installed tool
$|
$|Options:
$| -h, --help Print help
println(text)
}
///|
fn print_tool_sub_help(sub : String) -> Unit {
match sub {
"install" | "add" => print_tool_install_help()
"upgrade" | "update" | "up" => print_tool_upgrade_help()
"list" | "ls" => print_tool_list_help()
"uninstall" | "remove" | "rm" => print_tool_uninstall_help()
_ => print_tool_help()
}
}
///|
fn print_tool_install_help() -> Unit {
let text =
$|Install a tool from a mooncakes.io module
$|
$|Usage: mb tool install [OPTIONS] [@]
$|
$|Arguments:
$| [@] The package to install, e.g. author/module or
$| author/module@0.1.1
$|
$|Options:
$| -f, --force Force (re)installation even if already installed
$| --bin Install executables into DIR (default: ~/.local/bin)
$| --bin-dir Alias for --bin
$| --moon Build with the given managed MoonBit toolchain
$| -h, --help Print help
println(text)
}
///|
fn print_tool_upgrade_help() -> Unit {
let text =
$|Upgrade installed tools to their latest version
$|
$|Usage: mb tool upgrade [OPTIONS] ...
$|
$|Arguments:
$| ... Name of a tool to upgrade, with an optional version
$| specifier; pass --all to upgrade every installed tool
$|
$|Options:
$| --all Upgrade all installed tools
$| --moon Build with the given managed MoonBit toolchain
$| -h, --help Print help
println(text)
}
///|
fn print_tool_list_help() -> Unit {
let text =
$|List installed tools
$|
$|Usage: mb tool list [OPTIONS]
$|
$|Options:
$| --outdated Check mooncakes.io and mark tools that have a newer version
$| -h, --help Print help
println(text)
}
///|
fn print_tool_uninstall_help() -> Unit {
let text =
$|Remove an installed tool
$|
$|Usage: mb tool uninstall [OPTIONS] ...
$|
$|Arguments:
$| ... Name of the tool to remove
$|
$|Options:
$| --all Uninstall all installed tools
$| -h, --help Print help
println(text)
}
///|
/// Parse the shared `--all` / `--moon` / `...` option set used by
/// `tool upgrade` and `tool uninstall`.
fn parse_names_flags(args : Array[String]) -> Result[NamesFlags, String] {
let names : Array[String] = []
let mut all = false
let mut moon = ""
let mut i = 0
while i < args.length() {
let a = args[i]
if a == "--all" {
all = true
i = i + 1
} else if a == "--moon" && i + 1 < args.length() {
moon = args[i + 1]
i = i + 2
} else if a.has_prefix("--moon=") {
moon = a[7:].to_owned()
i = i + 1
} else if a.has_prefix("-") {
return Err("unexpected option '" + a + "'")
} else {
names.push(a)
i = i + 1
}
}
if names.is_empty() && !all {
return Err(
"the following required arguments were not provided:\n ...",
)
}
Ok({ names, all, moon, })
}
///|
priv struct NamesFlags {
names : Array[String]
all : Bool
moon : String
}
///|
fn parse_list_flags(args : Array[String]) -> Result[Bool, String] {
let mut outdated = false
for a in args {
if a == "--outdated" {
outdated = true
} else if a.has_prefix("-") {
return Err("unexpected option '" + a + "'")
} else {
return Err("unexpected argument '" + a + "'")
}
}
Ok(outdated)
}
///|
/// A single required value, with no extra flags (which are filtered out by the
/// `--help` check before this runs).
fn parse_required_arg(
args : Array[String],
placeholder : String,
) -> Result[String, String] {
if args.is_empty() {
Err("the following required argument was not provided:\n " + placeholder)
} else if args.length() > 1 {
Err("unexpected argument '" + args[1] + "'")
} else {
Ok(args[0])
}
}
///|
/// Parsed flags for `mb tool install`.
pub struct ToolInstallOpts {
spec : String
force : Bool
bin : String
moon : String
}
///|
fn parse_tool_install_flags(
args : Array[String],
) -> Result[ToolInstallOpts, String] {
let mut spec = ""
let mut force = false
let mut bin = ""
let mut moon = ""
let mut i = 0
while i < args.length() {
let a = args[i]
if a == "--force" || a == "-f" {
force = true
i = i + 1
} else if a == "--bin" || a == "--bin-dir" {
if i + 1 >= args.length() {
return Err(
"a value is required for '" + a + " ' but none was supplied",
)
}
bin = args[i + 1]
i = i + 2
} else if a.has_prefix("--bin-dir=") {
bin = a[10:].to_owned()
i = i + 1
} else if a.has_prefix("--bin=") {
bin = a[6:].to_owned()
i = i + 1
} else if a == "--moon" {
if i + 1 >= args.length() {
return Err(
"a value is required for '--moon ' but none was supplied",
)
}
moon = args[i + 1]
i = i + 2
} else if a.has_prefix("--moon=") {
moon = a[7:].to_owned()
i = i + 1
} else if a.has_prefix("-") {
return Err("unexpected option '" + a + "'")
} else if spec == "" {
spec = a
i = i + 1
} else {
return Err("unexpected argument '" + a + "'")
}
}
if spec == "" {
return Err("the following required argument was not provided:\n ")
}
Ok({ spec, force, bin, moon, })
}
///|
fn parse_version_flags(
args : Array[String],
default_version : String,
) -> Result[(String, Bool), String] {
let mut version = default_version
let mut force = false
let mut seen = false
for a in args {
if a == "--force" || a == "-f" || a == "--force-reinstall" {
force = true
} else if a.has_prefix("-") {
return Err("unexpected option '" + a + "'")
} else if !seen {
version = a
seen = true
} else {
return Err("unexpected argument '" + a + "'")
}
}
Ok((version, force))
}