// cli.mbt — argument model and parsing for the weblink-tool CLI.
///|
struct CliOptions {
mut input : String
mut input_set : Bool
mut limits_preset : String
mut json : Bool
mut rel : String
mut media_type : String
mut hreflang : String
mut list : Bool
mut count : Bool
mut help : Bool
positionals : Array[String]
mut unknown : String
}
///|
fn empty_options() -> CliOptions {
{
input: "",
input_set: false,
limits_preset: "default",
json: false,
rel: "",
media_type: "",
hreflang: "",
list: false,
count: false,
help: false,
positionals: Array::new(),
unknown: "",
}
}
///|
fn known_command(c : String) -> Bool {
c == "parse" ||
c == "validate" ||
c == "canonicalize" ||
c == "query" ||
c == "to-linkset-json" ||
c == "from-linkset-json" ||
c == "to-linkset-text" ||
c == "relation" ||
c == "audit" ||
c == "stats" ||
c == "help" ||
c == "version"
}
///|
/// True when the token is a bare JavaScript/WASM engine name, with or
/// without the Windows `.exe` suffix.
fn is_engine_name(a : String) -> Bool {
a == "node" ||
a == "node.exe" ||
a == "deno" ||
a == "deno.exe" ||
a == "bun" ||
a == "bun.exe"
}
///|
/// True when the token is a program path: it contains a directory separator
/// and ends in an executable extension.
fn is_program_path(a : String) -> Bool {
(a.contains("\\") || a.contains("/")) &&
(
a.has_suffix(".exe") ||
a.has_suffix(".js") ||
a.has_suffix(".cjs") ||
a.has_suffix(".mjs") ||
a.has_suffix(".wasm")
)
}
///|
/// The number of leading argv tokens that are runtime/program prefixes:
/// engine names (`node`, `deno`, `bun`) and program paths. Scanning stops at
/// the first known command or the first token that is neither, so a real CLI
/// command or user input is never consumed.
fn strip_program_name(args : Array[String]) -> Int {
let mut start = 0
while start < args.length() {
if known_command(args[start]) {
break
}
if !is_engine_name(args[start]) && !is_program_path(args[start]) {
break
}
start = start + 1
}
start
}
///|
/// Splits `@env.args()` into the command and its options.
///
/// Each MoonBit target prefixes `@env.args()` differently: native and
/// wasm-gc pass `[program, args...]`, js passes `[node, program.js,
/// args...]`. `strip_program_name` skips those prefixes uniformly. As a
/// fallback for platforms that report a bare executable name without a
/// directory separator, a single leading token that is not a known command
/// is dropped as well.
fn split_command(args : Array[String]) -> (String, Array[String]) {
if args.is_empty() {
return ("help", Array::new())
}
let mut start = strip_program_name(args)
if start == 0 && !known_command(args[0]) {
start = 1
}
if start >= args.length() {
return ("help", Array::new())
}
let cmd = args[start]
let rest = Array::new()
for i = start + 1; i < args.length(); i = i + 1 {
rest.push(args[i])
}
(cmd, rest)
}
///|
/// Parses the option tokens following a command into a `CliOptions`.
fn parse_options(args : Array[String]) -> CliOptions {
let o = empty_options()
let mut i = 0
while i < args.length() {
let a = args[i]
if a == "--input" ||
a == "--limits" ||
a == "--rel" ||
a == "--type" ||
a == "--hreflang" {
let value = if i + 1 < args.length() { args[i + 1] } else { "" }
if a == "--input" {
o.input = value
o.input_set = true
} else if a == "--limits" {
o.limits_preset = value
} else if a == "--rel" {
o.rel = value
} else if a == "--type" {
o.media_type = value
} else if a == "--hreflang" {
o.hreflang = value
}
i = i + 2
} else if a == "--json" {
o.json = true
i = i + 1
} else if a == "--list" {
o.list = true
i = i + 1
} else if a == "--count" {
o.count = true
i = i + 1
} else if a == "--help" || a == "-h" {
o.help = true
i = i + 1
} else if a.has_prefix("-") {
o.unknown = a
i = i + 1
} else {
o.positionals.push(a)
i = i + 1
}
}
o
}
///|
/// The version banner printed by `version` and `stats`, built from the
/// library's single-source version string.
fn version_banner() -> String {
"weblink-tool \{@weblink.library_version()} (15614376790/moon-weblink)"
}
///|
/// The limits preset named by `--limits` (default / strict / permissive).
fn resolve_limits(preset : String) -> @weblink.Limits {
if preset == "strict" {
@weblink.Limits::strict()
} else if preset == "permissive" {
@weblink.Limits::permissive()
} else {
@weblink.Limits::default()
}
}
///|
/// The input string: `--input` when set, else the first positional.
fn get_input(o : CliOptions) -> String {
if o.input_set && o.input.length() > 0 {
o.input
} else if o.positionals.length() > 0 {
o.positionals[0]
} else {
""
}
}