///|
/// The name the tool answers to in its usage text and its version line.
pub let program_name : String = "moonjson-toolkit"
///|
/// The version reported by `-V` / `--version`.
///
/// MoonBit cannot read `moon.mod` while building — `moon.mod` is strictly
/// parsed, has no build hooks, and the language has no way to include a file at
/// compile time — so the number has to exist here as well as there. What keeps
/// the two from drifting is `cli_wbtest.mbt`, which reads `moon.mod` on every
/// test run and fails when this constant disagrees with it. Bump `moon.mod` and
/// the suite will tell you to bump this too.
pub let version : String = "0.3.1"
///|
/// The line printed by `-V` / `--version`.
pub fn version_line() -> String {
program_name + " " + version
}
///|
/// Options accepted by the command-line interface.
pub(all) struct CliOptions {
mut files : Array[String]
mut indent : Int
mut max_depth : Int?
mut compact : Bool
mut sort_keys : Bool
mut trim_strings : Bool
mut jsonl : Bool
mut flatten : Bool
mut unflatten : Bool
mut prune_null : Bool
mut prune_empty : Bool
mut select : String?
mut sort_by : String?
mut unique : String?
mut paths : Bool
mut keys_only : Bool
mut stats : Bool
mut emit_moonbit : String?
mut validate : Bool
mut schema : String?
mut check_schema : String?
mut schema_output : String?
mut help : Bool
mut version : Bool
mut ai : Bool
mut model : String?
mut ai_base_url : String?
mut moon_deps : Bool
mut fail_fast : Bool
mut continue_on_error : Bool
mut json_out : String?
mut no_color : Bool
}
///|
/// The options used when no argument changes them.
pub fn CliOptions::default() -> CliOptions {
{
files: [],
indent: default_indent,
max_depth: None,
compact: false,
sort_keys: false,
trim_strings: false,
jsonl: false,
flatten: false,
unflatten: false,
prune_null: false,
prune_empty: false,
select: None,
sort_by: None,
unique: None,
paths: false,
keys_only: false,
stats: false,
emit_moonbit: None,
validate: false,
schema: None,
check_schema: None,
schema_output: None,
help: false,
version: false,
ai: false,
model: None,
ai_base_url: None,
moon_deps: false,
fail_fast: false,
continue_on_error: false,
json_out: None,
no_color: false,
}
}
///|
/// The text shown for `-h` / `--help`.
pub fn usage() -> String {
[
program_name + " - format, validate and analyse JSON documents",
"",
"Usage:",
" moonjson-toolkit [options]",
"",
"Input is read from standard input unless --file or --check-schema is given.",
"",
"Options:",
" -f, --file Read one or more input files instead of standard input",
" -i, --indent Spaces per nesting level, 0 to 16 (default: 2)",
" -c, --compact Print the document on one line, ignoring --indent",
" -S, --sort-keys Order the keys of every object before printing",
" --trim-strings Trim the whitespace around every string in the document",
" --flatten Collapse every nested object into dotted keys",
" --unflatten Expand every dotted key back into nested objects",
" --prune-null Remove every object member whose value is null",
" --prune-empty Remove every empty object and every empty array",
" --select Keep only the named top-level fields of an object",
" --sort-by Sort an array by the value names in each item",
" --unique [path] Drop repeated items, whole ones or by the value at ",
" --jsonl Read the input as JSON Lines, one document per line",
" --max-depth Refuse documents nested deeper than (default: 128)",
" --paths Print the path of every value instead of the document",
" --keys-only Print only the paths that name an object member",
" -v, --validate Only check the input and report the first error",
" --schema Check the input against the JSON Schema in ",
" --check-schema ",
" Compile a Schema without reading business JSON",
" --schema-output ",
" Report format for --check-schema or -v --schema",
" (default: text)",
" -h, --help Show this message and exit",
" -V, --version Show the version and exit",
" --ai Ask the AI for a quality report and suggestions",
" --model Model to ask for instead of the default",
" --ai-base-url ",
" Endpoint to post to instead of the default",
" --moon-deps Print the dependency tree of a MoonBit module",
" manifest",
" --fail-fast Stop at the first input that fails",
" --continue-on-error",
" Process every input, then summarise the failures",
" --json-out Write a statistics report as JSON to ",
" --stats Print a statistics summary instead of the document",
" --emit-moonbit [name]",
" Print the MoonBit type of the document's shape",
" --no-color Never colour the output, even on a terminal",
"",
"Short options may be combined, so -vh means -v -h. The value of --indent",
"may be attached, as in -i4, -i=4, --indent=4 or --indent 4.",
"",
"--file may be repeated. Each file is handled in turn, each under a",
"'==> path <==' heading once more than one is given, and the run exits with",
"the first non-zero code among them, with schema execution errors (2) taking",
"priority over input errors (1).",
"",
"--fail-fast stops the run at the first input that fails, so a batch of",
"files ends at the one that went wrong rather than at the end of the list.",
"Without it every input named is read and every failure is reported as it",
"happens, which is the default; --continue-on-error asks for that same",
"reading and adds a summary of it at the end, naming each input that failed",
"and the message it failed with. The two describe one run in opposite",
"directions, so asking for both is refused. Under --jsonl the input is one",
"file however many records it holds, so --fail-fast stops at the first",
"record that fails, and --continue-on-error summarises the file.",
"",
"--paths and --keys-only each replace the printed document with a list of",
"names, one per line. Asking for both prints the longer list, and -v wins",
"over either of them, since it asks for no document output at all.",
"",
"--stats replaces the document with a two-line summary of it. It joins the",
"same family: -v wins over it as it wins over the name lists, and --json-out",
"takes precedence over it, so asking for both writes the file and prints the",
"document as usual. --json-out describes one document, so it is refused when",
"several files are named.",
"",
"--emit-moonbit replaces the document with a MoonBit type for it: a struct for",
"every object the sample holds, holding the members it showed with the types",
"they showed, each struct deriving FromJson and ToJson. The name after it is",
"the name of the root type, and it is Root when no name is given. A name has",
"to start with an upper case letter and go on with letters, digits and",
"underscores, and the names the generated code is itself written with (Int,",
"Double, String, Bool, Json and Array) are refused as well: a type named after",
"one of them would be a type made of itself.",
"",
"The document the type is read off is the one the run prepared, so --sort-keys",
"decides the order the fields come out in, --select and the prunings decide",
"what there is to read, and --trim-strings decides what the strings look like.",
"A JSON key that is not a name MoonBit can spell is written as close to one as",
"the language allows, with the key in a comment beside the field: the derived",
"decoder reads the field name, so the mangled name is what the pasted type",
"will look for, and the comment is what says which key it stands for.",
"",
"What a sample cannot say is not guessed at. A member the document holds as",
"null, or holds in one record and not in the next, is left as a Json or made",
"optional rather than given a type the sample does not show. --json-out, --ai,",
"--paths and --keys-only each answer with something else in place of the",
"document, so none of them can be asked for with --emit-moonbit; --stats gives",
"way to it, and -v wins over it as it wins over the name lists.",
"",
"--schema checks the input against a JSON Schema instead of printing",
"it, and is read with -v, which is the flag that asks for a verdict rather",
"than a document. The supported JSON Schema Draft 2020-12 subset is: type,",
"required, dependentRequired, dependentSchemas, propertyNames, properties, patternProperties, minProperties, maxProperties,",
"prefixItems, items, minItems, maxItems, uniqueItems, contains, minContains, maxContains,",
"enum, const, minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf,",
"minLength, maxLength, pattern, allOf, anyOf, oneOf, not, if, then, else, additionalProperties, $defs and local JSON Pointer $ref.",
"$defs contains reusable schemas; $ref supports # and #/ JSON Pointers.",
"References resolve at compilation and siblings still apply. External references,",
"named anchors, $id and dynamic references remain unsupported.",
"Finite recursion through child instances is supported; same-instance cycles",
"and exhausted budgets are execution errors (exit 2), even inside not/anyOf.",
"Schema snapshot limits: depth 128, 100000 JSON nodes (including annotations).",
"Evaluation limits: depth 128, 100000 schema-node visits per input record.",
"prefixItems checks existing prefix positions; items checks remaining elements",
"after the prefixItems declared in the same schema object.",
"minItems/maxItems accept nonnegative integers up to 1000000000.",
"uniqueItems uses exact JSON equality: 1 and 1.0 count as duplicates.",
"contains checks every array position, including the prefix, and counts matches.",
"minContains defaults to 1; maxContains has no default upper bound. Without",
"a sibling contains they impose no constraint. Count parameters are capped at",
"1000000000; all items share the evaluation budget and propagate execution errors.",
"Example: {\"type\":\"array\",\"contains\":{\"type\":\"integer\"},\"minContains\":2,\"maxContains\":3}",
"accepts [1,\"x\",2], rejects [1,\"x\"] and rejects [1,2,3,4].",
"patternProperties searches field names; every matching rule checks the value.",
"additionalProperties excludes names declared explicitly or matched by patterns",
"in the same schema object, even when their values fail validation.",
"minProperties/maxProperties accept nonnegative integers up to 1000000000.",
"dependentRequired triggers when a member exists, including null, false, 0 or \"\".",
"Dependencies are directional. dependentSchemas checks the entire current object,",
"not the triggering member value. All dependency schemas are prechecked; only",
"triggered schemas run, sharing reference-cycle guards and evaluation budgets.",
"Dependencies: {\"type\":\"object\",\"dependentRequired\":{\"creditCard\":[\"billingAddress\"]},",
"\"dependentSchemas\":{\"creditCard\":{\"properties\":{\"billingAddress\":{\"type\":\"string\",\"minLength\":1}}}}}",
"accepts {} and {\"creditCard\":\"1234\",\"billingAddress\":\"Shanghai\"}; rejects",
"{\"creditCard\":\"1234\"} and {\"creditCard\":\"1234\",\"billingAddress\":\"\"}.",
"The older dependencies keyword remains unsupported.",
"propertyNames checks each name as a string, independently of its member value.",
"It neither requires members nor declares them for additionalProperties.",
"Every name schema is prechecked, including unused and unselected branches.",
"Name evaluations have separate cycle identities and share the normal budgets;",
"diagnostics identify the property name at the containing object pointer.",
"Names: {\"type\":\"object\",\"propertyNames\":{\"pattern\":\"^[a-z][a-z0-9_]*$\"}}",
"accepts {\"user_id\":1} and {}; rejects {\"User-ID\":1} because of its name.",
"Regex subset: literals, dot, classes, groups, alternation, repetitions, ^ and $.",
"Dot excludes LF, CR, U+2028 and U+2029. \\s uses ECMAScript whitespace;",
"\\S is its complement. Patterns compile once per schema and are reused.",
"This is not a full ECMA-262 engine; unsupported mechanisms are rejected.",
"Exclusive bounds exclude equality. multipleOf uses exact decimal divisibility",
"with a strictly positive divisor.",
"Boolean schemas are supported: true accepts every value; false rejects it.",
"if selects its sibling then when the condition matches, otherwise else.",
"All conditional branches are prechecked, but only the selected branch runs.",
"Without if, then/else do nothing; if alone imposes no assertion here.",
"Conditions and branches share the current instance and evaluation budget;",
"execution errors propagate instead of making the condition false.",
"oneOf requires exactly one matching branch. Unsupported keywords, versions",
"and mechanisms are rejected",
"before validation with exit 2. Allowed annotations are title, description,",
"$comment, default, examples and format. format is annotation only and does",
"not assert formats such as email.",
"Examples: {\"properties\":{\"id\":true},\"additionalProperties\":false}",
"allows only id; {\"oneOf\":[{\"const\":\"a\"},{\"const\":\"b\"}]} accepts a or b.",
"Money: {\"type\":\"number\",\"exclusiveMinimum\":0,\"multipleOf\":0.01}",
"accepts 12.34 and rejects 12.345 and 0.",
"Conditional: {\"type\":\"object\",\"if\":{\"required\":[\"kind\"],\"properties\":{\"kind\":{\"const\":\"business\"}}},",
"\"then\":{\"required\":[\"taxId\"]},\"else\":{\"required\":[\"name\"]}}",
"accepts {\"kind\":\"business\",\"taxId\":\"T1\"} and {\"kind\":\"personal\",\"name\":\"Li\"};",
"rejects {\"kind\":\"business\"}. required ensures kind exists; properties alone does not.",
"",
"A document that does not match is reported with every place it disagrees,",
"one to a line, and the run exits 1. A schema this tool cannot read is a",
"mistake in the command line rather than in the document: it is named, the",
"keyword to fix is quoted under it, and the run exits 2 without looking at",
"any input. Under --jsonl every record is checked on its own and reported",
"with the line it was written on.",
"",
"--flatten collapses every nested object into dotted keys, and --unflatten",
"expands them again. The two are inverses of each other, so only one of them",
"may be given. A document that names one path two ways, with a key \"a.b\"",
"beside a key \"a\", has no flattened or unflattened form and is refused with",
"both keys named.",
"",
"--prune-null drops every object member whose value is null, and --prune-empty",
"drops every empty object and every empty array, then the containers left empty",
"by that in turn. The two are independent and may be given together, in which",
"case the nulls go first, so a member left holding {} is dropped as well.",
"--prune-null keeps every item of an array: a null in an object is a member with",
"no value, while a null in an array is a value in a place, and dropping it would",
"renumber the items after it.",
"",
"--select, --sort-by and --unique rewrite the document rather than lay it out,",
"and each answers one question about what it should hold. At most one of the",
"three may be given: two of them are two answers to the same question, and",
"neither is more nearly right than the other. They run before everything else",
"that removes or reshapes values, so --select decides what --prune-null,",
"--prune-empty, --flatten and --unflatten then see. Deduplicating is the one",
"of the three that compares values with one another, and it does so through",
"the tidying the run asked for, so --trim-strings and --sort-keys share in",
"deciding what counts as the same item there.",
"",
"--select keeps the top-level fields its comma-separated list names, in the",
"order the list gives them, and drops the rest. A name the object does not",
"have is skipped rather than reported, so one selection works across a folder",
"of documents whose fields have drifted apart.",
"",
"--sort-by sorts an array by the value its path names inside each item. The",
"document may be an array itself or an object holding exactly one, which is",
"sorted where it sits; an object holding several is refused, since there is no",
"telling which was meant. Numbers are ordered as numbers and strings by code",
"point, and values of different kinds are ordered by kind, as jq orders them:",
"null, false, true, numbers, strings, arrays, objects. Two arrays, and two",
"objects, are equal and keep the order they were written in. A record whose",
"path reaches nothing is sorted as though the field held null, which puts it",
"before the records that have a value there. The sort is stable.",
"",
"--unique drops the items that repeat one already seen, keeping the first.",
"What is compared is the item as the run would print it, so --sort-keys makes",
"two objects whose members were written in a different order one item, and",
"--trim-strings makes \" a\" and \"a\" one item. With no path the whole item is",
"compared; with a path it is the value the path names that is. An item whose",
"path reaches nothing is kept, since it has no value to be compared.",
"",
"--jsonl reads the input as JSON Lines: one document per line, with blank",
"lines skipped. Each record is handled on its own, so the other options apply",
"to every one of them in turn. A record that is not valid JSON is reported",
"with its line in the file and the rest are still printed; the run exits 1 if",
"any record failed. It describes many documents at once, so it is refused",
"with --json-out and with --ai.",
"",
"--ai asks an OpenAI-compatible chat-completions API for a review of the",
"formatted document. The endpoint, the model and the key are taken from",
"--ai-base-url, --model and MOONJSON_AI_API_KEY, each falling back in turn",
"to MOONJSON_AI_BASE_URL, MOONJSON_AI_MODEL and DEEPSEEK_API_KEY, and then",
"to DeepSeek's own. There is no --api-key: a key on a command line is a key",
"in the shell history and in the process list.",
"",
"--model and --ai-base-url describe that one request, so without --ai they",
"are read, accepted and ignored.",
"",
"--moon-deps reads the input as a MoonBit module manifest and prints the",
"tree of modules it depends on, each dependency read in turn from the",
".mooncakes directory beside the manifest. Both manifest forms are read:",
"moon.mod.json, and the moon.mod whose dependencies sit in an import block.",
"A dependency that has not been downloaded is shown with nothing under it,",
"and a module required at more than one version, or a circular dependency,",
"is reported beneath the tree. The tree is the whole of the output, so no",
"other option is consulted; --ai reviews the document instead, so under it",
"--moon-deps does nothing.",
"",
"--check-schema (or --check-schema=) checks schema parameters,",
"references and this tool's supported subset, without validating business",
"data or reading standard input. A false schema also compiles successfully.",
"Success means the schema compiles within the supported subset, not full",
"JSON Schema conformance. Use it alone or with --no-color; --help and",
"--version keep their usual priority. Exit 0 means compilation succeeded;",
"exit 2 means invalid arguments, an unreadable file, invalid JSON or a",
"schema compilation problem. Example: moonjson-toolkit --check-schema schema.json",
"For --check-schema, --schema-output json emits one JSON report: success",
"on stdout (exit 0), read, parse or compile failure on stderr (exit 2),",
"with the other stream empty. Argument errors remain text. Reports are not",
"standards certification.",
"Example: moonjson-toolkit --check-schema schema.json --schema-output json",
"",
"JSON reports support -v --schema for stdin, one file or multiple --file inputs.",
"Omit --file for stdin; one file uses reportType validation, multiple files",
"use batchValidation. Batches compile the schema once and process every input",
"in command order, including repeated paths. AI, statistics, transforms,",
"exports, --fail-fast and --continue-on-error cannot be combined with this mode.",
"--max-depth and --no-color are allowed. Batch exit codes prefer 2 over 1 over 0.",
"A valid input or batch is reported on stdout (exit 0). An invalid input or an input",
"read/parse error is reported on stderr (exit 1); schema or evaluation",
"errors use stderr (exit 2). The other stream stays empty. Reports include",
"reportType validation, status valid/invalid/error and issue stage",
"schema/input/evaluation. Locations use display paths; evaluation errors",
"have separate instancePointer and schemaPointer JSON Pointers.",
"Example: moonjson-toolkit -v --schema schema.json -f data.json --schema-output json",
"Batch: moonjson-toolkit -v --schema schema.json -f first.json -f second.json --schema-output json",
"With --jsonl, JSON reports support one file or stdin, not multiple files.",
"Input is JSONL; output is one jsonlValidation JSON document, not NDJSON.",
"Blank lines are skipped; results retain actual 1-based line numbers.",
"Locations and instancePointer are relative to each record. Parse diagnostics",
"retain source line/column numbers. Every record is attempted with its own",
"evaluation budget; exits prefer 2 over 1 over 0. Empty input succeeds.",
"Only an entirely successful run writes stdout; failures write the complete",
"report to stderr with stdout empty. No partial reports or text summaries.",
"JSONL: moonjson-toolkit -v --schema schema.json --jsonl -f data.jsonl --schema-output json",
"Stdin: cat data.jsonl | moonjson-toolkit -v --schema schema.json --jsonl --schema-output json",
"",
"Exit codes:",
" 0 success",
" 1 the input could not be read, or is not valid JSON",
" 2 the command line was invalid",
" 3 the input was valid but the AI review failed",
].join("\n")
}
///|
/// Parse a signed decimal integer, rejecting empty strings, stray characters
/// and a lone sign.
fn parse_decimal(value : String) -> Int? {
let chars = value.to_array()
if chars.length() == 0 {
return None
}
let mut index = 0
let mut negative = false
if chars[0] == '-' {
negative = true
index = 1
} else if chars[0] == '+' {
index = 1
}
if index >= chars.length() {
return None
}
let mut result = 0
while index < chars.length() {
let ch = chars[index]
if ch < '0' || ch > '9' {
return None
}
result = result * 10 + (ch.to_int() - 48)
index = index + 1
}
Some(if negative { -result } else { result })
}
///|
/// Interpret the text given to `-i` / `--indent` as a width in `0..=16`.
fn parse_indent(value : String) -> Result[Int, String] {
match parse_decimal(value) {
None => Err("invalid indent value: " + value + " (expected an integer)")
Some(width) =>
if width < 0 || width > max_indent {
Err(
"indent out of range: " +
value +
" (expected 0 to " +
max_indent.to_string() +
")",
)
} else {
Ok(width)
}
}
}
///|
/// Interpret the text given to `--max-depth` as a nesting limit.
///
/// Zero is allowed and means nothing may nest at all, which is what the parser
/// does with it: a document of bare scalars still parses.
fn parse_max_depth(value : String) -> Result[Int, String] {
match parse_decimal(value) {
None => Err("invalid max-depth value: " + value + " (expected an integer)")
Some(limit) =>
if limit < 0 {
Err("max-depth out of range: " + value + " (expected 0 or more)")
} else {
Ok(limit)
}
}
}
///|
/// Resolve the value of an option, taking it from `inline` (the text after
/// `=`) or, failing that, from the next argument.
///
/// Returns the value together with the index of the next unread argument.
fn option_value(
option : String,
inline : String?,
args : Array[String],
index : Int,
) -> Result[(String, Int), String] {
match inline {
Some(value) =>
if value == "" {
Err("option " + option + " requires a non-empty value")
} else {
Ok((value, index))
}
None =>
if index < args.length() {
Ok((args[index], index + 1))
} else {
Err("option " + option + " requires a value")
}
}
}
///|
/// Split the body of a long option into its name and the text after `=`.
fn split_long_option(body : String) -> (String, String?) {
match body.find("=") {
Some(position) =>
(
body.exact_view(start=0, end=position).to_owned(),
Some(body.exact_view(start=position + 1).to_owned()),
)
None => (body, None)
}
}
///|
/// The error reported for an argument that is neither an option nor a value.
fn unexpected_argument(arg : String) -> String {
"unexpected argument: " + arg + " (use --file to name an input file)"
}
///|
/// Apply a value-taking option, reporting the first error it hits.
fn apply_value_option(
options : CliOptions,
name : String,
value : String,
) -> Result[Unit, String] {
match name {
// The one value option that accumulates rather than replaces: naming a
// second file adds a second input rather than discarding the first.
"file" => {
options.files.push(value)
Ok(())
}
"json-out" => {
options.json_out = Some(value)
Ok(())
}
"select" => {
options.select = Some(value)
Ok(())
}
"schema" => {
options.schema = Some(value)
Ok(())
}
"check-schema" =>
if value == "" {
Err("option --check-schema requires a non-empty value")
} else {
options.check_schema = Some(value)
Ok(())
}
"schema-output" =>
if value == "text" || value == "json" {
options.schema_output = Some(value)
Ok(())
} else {
Err("option --schema-output requires text or json")
}
"sort-by" => {
options.sort_by = Some(value)
Ok(())
}
"model" => {
options.model = Some(value)
Ok(())
}
"ai-base-url" => {
options.ai_base_url = Some(value)
Ok(())
}
"max-depth" =>
match parse_max_depth(value) {
Ok(limit) => {
options.max_depth = Some(limit)
Ok(())
}
Err(message) => Err(message)
}
"indent" =>
match parse_indent(value) {
Ok(width) => {
options.indent = width
Ok(())
}
Err(message) => Err(message)
}
// Unreachable: `CliOptions::parse` routes only the names above here. An
// error rather than a silent `Ok` so that adding another value-taking
// option and forgetting this match is caught rather than ignored.
_ => Err("unknown option: --" + name)
}
}
///|
/// Parse command-line arguments into `CliOptions`.
///
/// This is a pure function: it touches no file, environment or console state,
/// so every branch is cheap to unit test. The message in `Err` is written for
/// the user and is meant to be printed verbatim before exiting with code 2.
pub fn CliOptions::parse(args : Array[String]) -> Result[CliOptions, String] {
let options = CliOptions::default()
let mut index = 0
let total = args.length()
while index < total {
let arg = args[index]
index = index + 1
if arg == "--" {
// `--` ends option parsing; anything left would be a positional
// argument, which this CLI does not accept.
if index < total {
return Err(unexpected_argument(args[index]))
}
} else if arg.has_prefix("--") {
let (name, inline) = split_long_option(arg.exact_view(start=2).to_owned())
match name {
"help" => options.help = true
"version" => options.version = true
"validate" => options.validate = true
"compact" => options.compact = true
"sort-keys" => options.sort_keys = true
"trim-strings" => options.trim_strings = true
"jsonl" => options.jsonl = true
"flatten" => options.flatten = true
"unflatten" => options.unflatten = true
"prune-null" => options.prune_null = true
"prune-empty" => options.prune_empty = true
"paths" => options.paths = true
"keys-only" => options.keys_only = true
"stats" => options.stats = true
"no-color" => options.no_color = true
"ai" => options.ai = true
"moon-deps" => options.moon_deps = true
"fail-fast" => options.fail_fast = true
"continue-on-error" => options.continue_on_error = true
// One of the two options whose value is optional. Without one it
// compares whole items, which is the empty path — a path that names the
// item itself — and that is all the difference between the two forms, so
// the rest of the program reads one option rather than two.
"unique" =>
match inline {
Some(value) => options.unique = Some(value)
None =>
// A following argument is the path only if it is not another
// option: `--unique --flatten` is the whole-item form followed by
// a flag, and reading `--flatten` as a field name would leave the
// run asking for a field called `--flatten` and no flattening.
// Nothing else can follow `--unique` bare, since files are named
// with --file, so the only argument that can be meant is a path.
if index < total && !args[index].has_prefix("-") {
options.unique = Some(args[index])
index = index + 1
} else {
options.unique = Some("")
}
}
// The other one. Without a value the root type is called `Root`, which
// is the name a run that names none is asking for, so a bare flag is
// read as that name rather than as a second form of the option. A value
// written as empty, `--emit-moonbit=`, is left as the empty name it is
// and refused where names are read: a name that was written down and
// came out empty is a mistake to report, not a default to fall back on.
"emit-moonbit" =>
match inline {
Some(value) => options.emit_moonbit = Some(value)
None =>
// A following argument is the name only if it is not another
// option, for the reason `--unique` gives: `--emit-moonbit
// --compact` asks for the type under the default name and a
// compact document, not for a type called `--compact`.
if index < total && !args[index].has_prefix("-") {
options.emit_moonbit = Some(args[index])
index = index + 1
} else {
options.emit_moonbit = Some(default_root_name)
}
}
"check-schema" | "schema-output" => {
if inline == None && index < total && args[index].has_prefix("-") {
return Err("option --" + name + " requires a value")
}
match option_value("--" + name, inline, args, index) {
Err(message) => return Err(message)
Ok((value, next)) =>
match apply_value_option(options, name, value) {
Err(message) => return Err(message)
Ok(_) => index = next
}
}
}
"file"
| "json-out"
| "schema"
| "select"
| "sort-by"
| "indent"
| "max-depth"
| "model"
| "ai-base-url" =>
match option_value("--" + name, inline, args, index) {
Err(message) => return Err(message)
Ok((value, next)) =>
match apply_value_option(options, name, value) {
Err(message) => return Err(message)
Ok(_) => index = next
}
}
_ => return Err("unknown option: --" + name)
}
} else if arg.has_prefix("-") && arg.length() > 1 {
let cluster = arg.exact_view(start=1).to_owned()
let flags = cluster.to_array()
let mut position = 0
while position < flags.length() {
let flag = flags[position]
position = position + 1
match flag {
'h' => options.help = true
// Lower case is `--validate`, upper case is `--version`; they are
// one keystroke apart and mean quite different things, so both
// spellings are worth having a test for.
'V' => options.version = true
'v' => options.validate = true
'c' => options.compact = true
// Capitalised on purpose: `jq` gives `-S` to its own `--sort-keys`,
// so the letter a reader is likely to reach for is already the one
// that does this.
'S' => options.sort_keys = true
'f' | 'i' => {
let option = if flag == 'f' { "-f" } else { "-i" }
let rest = String::from_array(flags[position:])
// `-i4` and `-i=4` both attach the value to the flag, and an
// empty `rest` means the value is the next argument.
let attached = if rest.has_prefix("=") {
Some(rest.exact_view(start=1).to_owned())
} else if rest == "" {
None
} else {
Some(rest)
}
match option_value(option, attached, args, index) {
Err(message) => return Err(message)
Ok((value, next)) =>
match
apply_value_option(
options,
if flag == 'f' {
"file"
} else {
"indent"
},
value,
) {
Err(message) => return Err(message)
Ok(_) => {
index = next
// The rest of the cluster was the value, so stop here.
position = flags.length()
}
}
}
}
_ => return Err("unknown option: -" + flag.to_string())
}
}
} else {
return Err(unexpected_argument(arg))
}
}
Ok(options)
}