mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
919 lines
33 KiB
C
919 lines
33 KiB
C
/* node:util's engine-free static surface. parseArgs crosses the typed
|
|
* frontend through the checked-dynamic tree: the config is JSON-safe data,
|
|
* and the result is the ordinary Node-shaped { values, positionals,
|
|
* tokens? } tree. This keeps the parser independent of every emitted
|
|
* record/union layout while the frontend's dynCheck restores the precise
|
|
* ParsedResults<T> type at the call site.
|
|
*
|
|
* The grammar follows Node 24's util.parseArgs/tokenizeArgs: long options,
|
|
* grouped shorts, inline/separate string values, strict=false unknowns,
|
|
* -- termination, negative booleans, multiple/default accumulation, and
|
|
* token metadata. All input nodes are borrowed; the result owns fresh or
|
|
* retained children. Validation/grammar failures leave a coded TypeError
|
|
* pending and return NULL. */
|
|
#include "scr_runtime.h"
|
|
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
static const ScrDyn *pa_own_member(const ScrDyn *obj, const char *key) {
|
|
if (!obj || obj->kind != SCR_DYN_OBJ) return NULL;
|
|
return scr_dyn_obj_get(obj, key, strlen(key));
|
|
}
|
|
|
|
/* The ordinary optional-member read: absent and explicit undefined both
|
|
* answer no value. Descriptor fields whose validation depends on own
|
|
* presence use pa_own_member directly. */
|
|
static const ScrDyn *pa_member(const ScrDyn *obj, const char *key) {
|
|
const ScrDyn *v = pa_own_member(obj, key);
|
|
return v && v->kind != SCR_DYN_UNDEF ? v : NULL;
|
|
}
|
|
|
|
static bool pa_str_eq_bytes(const ScrStr *s, const char *p, size_t n) {
|
|
return s && s->len == n && memcmp(s->data, p, n) == 0;
|
|
}
|
|
|
|
static bool pa_str_eq_c(const ScrStr *s, const char *p) {
|
|
return pa_str_eq_bytes(s, p, strlen(p));
|
|
}
|
|
|
|
static ScrStr *pa_str_bytes(const char *p, size_t n) {
|
|
return scr_str_new(p, n);
|
|
}
|
|
|
|
static ScrDyn *pa_dyn_str(const ScrStr *s) {
|
|
return scr_dyn_new_str((ScrStr *)s);
|
|
}
|
|
|
|
static ScrDyn *pa_dyn_str_bytes(const char *p, size_t n) {
|
|
ScrStr *s = pa_str_bytes(p, n);
|
|
ScrDyn *d = pa_dyn_str(s);
|
|
scr_str_release(s);
|
|
return d;
|
|
}
|
|
|
|
static char *pa_path(const char *prefix, const char *name, size_t name_len,
|
|
const char *suffix) {
|
|
size_t pn = strlen(prefix), sn = strlen(suffix);
|
|
char *out = malloc(pn + name_len + sn + 1);
|
|
if (!out) {
|
|
fputs("scriptc: out of memory\n", stderr);
|
|
abort();
|
|
}
|
|
memcpy(out, prefix, pn);
|
|
memcpy(out + pn, name, name_len);
|
|
memcpy(out + pn + name_len, suffix, sn + 1);
|
|
return out;
|
|
}
|
|
|
|
static void pa_throw_text(ScrJsonBuf *b, const char *code) {
|
|
ScrStr *msg = scr_jb_finish(b);
|
|
scr_throw_error_msg_code(SCR_ERR_TYPE, msg->data, msg->len, code);
|
|
scr_str_release(msg);
|
|
}
|
|
|
|
static void pa_unknown(const ScrStr *raw, bool allow_positionals) {
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, "Unknown option '");
|
|
scr_jb_put_str(&b, raw);
|
|
scr_jb_puts(&b, "'");
|
|
if (allow_positionals) {
|
|
scr_jb_puts(&b, ". To specify a positional argument starting with a '-', place it at the end of the command after '--', as in '-- \"");
|
|
scr_jb_put_str(&b, raw);
|
|
scr_jb_putc(&b, '"');
|
|
}
|
|
pa_throw_text(&b, "ERR_PARSE_ARGS_UNKNOWN_OPTION");
|
|
}
|
|
|
|
static void pa_option_label(ScrJsonBuf *b, const ScrDyn *desc,
|
|
const ScrStr *name) {
|
|
const ScrDyn *shortv = pa_member(desc, "short");
|
|
if (shortv && shortv->kind == SCR_DYN_STR && shortv->v.str->len > 0) {
|
|
scr_jb_putc(b, '-');
|
|
scr_jb_put_str(b, shortv->v.str);
|
|
scr_jb_puts(b, ", ");
|
|
}
|
|
scr_jb_puts(b, "--");
|
|
scr_jb_put_str(b, name);
|
|
}
|
|
|
|
static void pa_missing_value(const ScrDyn *desc, const ScrStr *name) {
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, "Option '");
|
|
pa_option_label(&b, desc, name);
|
|
scr_jb_puts(&b, " <value>' argument missing");
|
|
pa_throw_text(&b, "ERR_PARSE_ARGS_INVALID_OPTION_VALUE");
|
|
}
|
|
|
|
static void pa_ambiguous(const ScrStr *raw, const ScrStr *name,
|
|
bool is_short) {
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, "Option '");
|
|
scr_jb_put_str(&b, raw);
|
|
scr_jb_puts(&b, "' argument is ambiguous.\nDid you forget to specify the option argument for '");
|
|
scr_jb_put_str(&b, raw);
|
|
scr_jb_puts(&b, "'?\nTo specify an option argument starting with a dash use '--");
|
|
scr_jb_put_str(&b, name);
|
|
scr_jb_puts(&b, "=-XYZ'");
|
|
if (is_short) {
|
|
scr_jb_puts(&b, " or '");
|
|
scr_jb_put_str(&b, raw);
|
|
scr_jb_puts(&b, "-XYZ'");
|
|
}
|
|
scr_jb_puts(&b, ".");
|
|
pa_throw_text(&b, "ERR_PARSE_ARGS_INVALID_OPTION_VALUE");
|
|
}
|
|
|
|
static void pa_takes_no_value(const ScrDyn *desc, const ScrStr *name) {
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, "Option '");
|
|
pa_option_label(&b, desc, name);
|
|
scr_jb_puts(&b, "' does not take an argument");
|
|
pa_throw_text(&b, "ERR_PARSE_ARGS_INVALID_OPTION_VALUE");
|
|
}
|
|
|
|
static void pa_unexpected(const ScrStr *arg) {
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, "Unexpected argument '");
|
|
scr_jb_put_str(&b, arg);
|
|
scr_jb_puts(&b, "'. This command does not take positional arguments");
|
|
pa_throw_text(&b, "ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL");
|
|
}
|
|
|
|
static void pa_unexpected_dyn(const ScrDyn *arg) {
|
|
if (arg->kind == SCR_DYN_STR) {
|
|
pa_unexpected(arg->v.str);
|
|
return;
|
|
}
|
|
ScrStr *shown = scr_dyn_format_j(arg);
|
|
if (!shown) return;
|
|
pa_unexpected(shown);
|
|
scr_str_release(shown);
|
|
}
|
|
|
|
static void pa_nullish_arg_length(const ScrDyn *arg) {
|
|
const char *kind = arg->kind == SCR_DYN_NULL ? "null" : "undefined";
|
|
ScrJsonBuf b;
|
|
scr_jb_init(&b);
|
|
scr_jb_puts(&b, "Cannot read properties of ");
|
|
scr_jb_puts(&b, kind);
|
|
scr_jb_puts(&b, " (reading 'length')");
|
|
ScrStr *msg = scr_jb_finish(&b);
|
|
scr_throw_error_msg(SCR_ERR_TYPE, msg->data, msg->len);
|
|
scr_str_release(msg);
|
|
}
|
|
|
|
/* A config boolean with Node's default/validation rule. */
|
|
static bool pa_config_bool(const ScrDyn *config, const char *name,
|
|
bool fallback, bool *ok) {
|
|
const ScrDyn *v = pa_member(config, name);
|
|
if (!v || v->kind == SCR_DYN_NULL) return fallback;
|
|
if (v->kind == SCR_DYN_BOOL) return v->v.b;
|
|
scr_dyn_arg_type_fail(name, "of type boolean", v);
|
|
*ok = false;
|
|
return fallback;
|
|
}
|
|
|
|
/* Descriptor type: 1 string, 0 boolean, -1 malformed. Descriptors were
|
|
* validated once before parsing, so parsing calls only see 0/1. */
|
|
static int pa_desc_type(const ScrDyn *desc) {
|
|
const ScrDyn *v = pa_member(desc, "type");
|
|
if (!v || v->kind != SCR_DYN_STR) return -1;
|
|
if (pa_str_eq_c(v->v.str, "string")) return 1;
|
|
if (pa_str_eq_c(v->v.str, "boolean")) return 0;
|
|
return -1;
|
|
}
|
|
|
|
static bool pa_desc_multiple(const ScrDyn *desc) {
|
|
const ScrDyn *v = pa_member(desc, "multiple");
|
|
return v && v->kind == SCR_DYN_BOOL && v->v.b;
|
|
}
|
|
|
|
static bool pa_default_value_ok(const ScrDyn *v, int type) {
|
|
return type == 1 ? v->kind == SCR_DYN_STR : v->kind == SCR_DYN_BOOL;
|
|
}
|
|
|
|
static ScrDyn *pa_materialize_array(const ScrDyn *value);
|
|
|
|
static bool pa_validate_option(const ScrStr *name, const ScrDyn *desc) {
|
|
char *base = pa_path("options.", name->data, name->len, "");
|
|
if (desc->kind != SCR_DYN_OBJ) {
|
|
scr_dyn_prop_type_fail(base, "of type object", desc);
|
|
free(base);
|
|
return false;
|
|
}
|
|
const ScrDyn *typev = pa_member(desc, "type");
|
|
int type = pa_desc_type(desc);
|
|
if (type < 0) {
|
|
char *path = pa_path(base, "", 0, ".type");
|
|
scr_dyn_prop_type_fail(path, "('string|boolean')",
|
|
typev ? typev : scr_dyn_undefined());
|
|
free(path);
|
|
free(base);
|
|
return false;
|
|
}
|
|
const ScrDyn *shortv = pa_own_member(desc, "short");
|
|
if (shortv) {
|
|
char *path = pa_path(base, "", 0, ".short");
|
|
if (shortv->kind != SCR_DYN_STR) {
|
|
scr_dyn_prop_type_fail(path, "of type string", shortv);
|
|
free(path);
|
|
free(base);
|
|
return false;
|
|
}
|
|
if (scr_str_utf16_len(shortv->v.str) != 1) {
|
|
scr_dyn_arg_value_fail(path, "must be a single character", shortv);
|
|
free(path);
|
|
free(base);
|
|
return false;
|
|
}
|
|
free(path);
|
|
}
|
|
const ScrDyn *multiplev = pa_own_member(desc, "multiple");
|
|
if (multiplev && multiplev->kind != SCR_DYN_BOOL) {
|
|
char *path = pa_path(base, "", 0, ".multiple");
|
|
scr_dyn_prop_type_fail(path, "of type boolean", multiplev);
|
|
free(path);
|
|
free(base);
|
|
return false;
|
|
}
|
|
const ScrDyn *def = pa_member(desc, "default");
|
|
if (def) {
|
|
char *path = pa_path(base, "", 0, ".default");
|
|
if (pa_desc_multiple(desc)) {
|
|
|
|
ScrDyn *items = pa_materialize_array(def);
|
|
if (!items) {
|
|
free(path);
|
|
free(base);
|
|
return false;
|
|
}
|
|
if (items->kind != SCR_DYN_ARR) {
|
|
scr_dyn_prop_type_fail(path, "an instance of Array", def);
|
|
scr_dyn_release(items);
|
|
free(path);
|
|
free(base);
|
|
return false;
|
|
}
|
|
for (size_t j = 0; j < items->v.arr.len; j++) {
|
|
if (!pa_default_value_ok(items->v.arr.items[j], type)) {
|
|
char suffix[48];
|
|
snprintf(suffix, sizeof suffix, "[%zu]", j);
|
|
char *item_path = pa_path(path, "", 0, suffix);
|
|
scr_dyn_prop_type_fail(item_path,
|
|
type ? "of type string" : "of type boolean",
|
|
items->v.arr.items[j]);
|
|
free(item_path);
|
|
scr_dyn_release(items);
|
|
free(path);
|
|
free(base);
|
|
return false;
|
|
}
|
|
}
|
|
scr_dyn_release(items);
|
|
} else if (!pa_default_value_ok(def, type)) {
|
|
scr_dyn_prop_type_fail(path,
|
|
type ? "of type string" : "of type boolean", def);
|
|
free(path);
|
|
free(base);
|
|
return false;
|
|
}
|
|
free(path);
|
|
}
|
|
free(base);
|
|
return true;
|
|
}
|
|
|
|
/* Validate the option schema up front, as Node does even when args is
|
|
* empty. Object.entries supplies JS own-key order (array-index names first),
|
|
* which controls both validation precedence and duplicate-short lookup. */
|
|
static bool pa_validate_options(const ScrDyn *options) {
|
|
if (!options) return true; /* null/undefined mean {} */
|
|
if (options->kind != SCR_DYN_OBJ) {
|
|
scr_dyn_arg_type_fail("options", "of type object", options);
|
|
return false;
|
|
}
|
|
ScrDyn *entries = scr_dyn_obj_entries(options);
|
|
for (size_t i = 0; i < entries->v.arr.len; i++) {
|
|
const ScrDyn *pair = entries->v.arr.items[i];
|
|
const ScrStr *name = pair->v.arr.items[0]->v.str;
|
|
const ScrDyn *desc = pair->v.arr.items[1];
|
|
if (!pa_validate_option(name, desc)) {
|
|
scr_dyn_release(entries);
|
|
return false;
|
|
}
|
|
}
|
|
scr_dyn_release(entries);
|
|
return true;
|
|
}
|
|
|
|
static const ScrDyn *pa_find_long(const ScrDyn *options, const ScrStr *name) {
|
|
if (!options) return NULL;
|
|
return scr_dyn_obj_get(options, name->data, name->len);
|
|
}
|
|
|
|
static bool pa_is_negative_name(const ScrStr *name) {
|
|
return name->len >= 3 && memcmp(name->data, "no-", 3) == 0;
|
|
}
|
|
|
|
/* Node recognizes an inline long-option value only when an '=' occurs
|
|
* after at least one name code unit, then splits at the first '='. */
|
|
static size_t pa_long_eq(const ScrStr *arg) {
|
|
bool has_inline_value = false;
|
|
for (size_t j = 3; j < arg->len; j++) {
|
|
if (arg->data[j] == '=') { has_inline_value = true; break; }
|
|
}
|
|
if (!has_inline_value) return arg->len;
|
|
for (size_t j = 2; j < arg->len; j++) {
|
|
if (arg->data[j] == '=') return j;
|
|
}
|
|
return arg->len;
|
|
}
|
|
|
|
static const ScrDyn *pa_find_short(const ScrDyn *options,
|
|
const ScrStr *short_name,
|
|
ScrStr **long_name) {
|
|
if (!options) {
|
|
*long_name = pa_str_bytes(short_name->data, short_name->len);
|
|
return NULL;
|
|
}
|
|
ScrDyn *entries = scr_dyn_obj_entries(options);
|
|
for (size_t i = 0; i < entries->v.arr.len; i++) {
|
|
const ScrDyn *pair = entries->v.arr.items[i];
|
|
const ScrStr *name = pair->v.arr.items[0]->v.str;
|
|
const ScrDyn *desc = pair->v.arr.items[1];
|
|
const ScrDyn *shortv = pa_member(desc, "short");
|
|
if (shortv && shortv->kind == SCR_DYN_STR &&
|
|
shortv->v.str->len == short_name->len &&
|
|
memcmp(shortv->v.str->data, short_name->data, short_name->len) == 0) {
|
|
*long_name = pa_str_bytes(name->data, name->len);
|
|
const ScrDyn *found = pa_find_long(options, name);
|
|
scr_dyn_release(entries);
|
|
return found;
|
|
}
|
|
}
|
|
scr_dyn_release(entries);
|
|
*long_name = pa_str_bytes(short_name->data, short_name->len);
|
|
/* Node falls back to the short spelling itself as the long option name.
|
|
* Thus `{ x: { type: "boolean" } }` accepts both `--x` and `-x` even
|
|
* without an explicit `short: "x"` descriptor. */
|
|
return pa_find_long(options, short_name);
|
|
}
|
|
|
|
static ScrDyn *pa_option_token(const ScrStr *name, const ScrStr *raw,
|
|
size_t index, const ScrDyn *value, int inline_value) {
|
|
ScrDyn *token = scr_dyn_new_obj();
|
|
scr_dyn_obj_set(token, "kind", 4, pa_dyn_str_bytes("option", 6));
|
|
scr_dyn_obj_set(token, "name", 4, pa_dyn_str(name));
|
|
scr_dyn_obj_set(token, "rawName", 7, pa_dyn_str(raw));
|
|
scr_dyn_obj_set(token, "index", 5, scr_dyn_new_num((double)index));
|
|
scr_dyn_obj_set(token, "value", 5,
|
|
value ? scr_dyn_retain((ScrDyn *)value)
|
|
: scr_dyn_retain(scr_dyn_undefined()));
|
|
scr_dyn_obj_set(token, "inlineValue", 11,
|
|
inline_value < 0 ? scr_dyn_retain(scr_dyn_undefined())
|
|
: scr_dyn_new_bool(inline_value != 0));
|
|
return token;
|
|
}
|
|
|
|
static void pa_store(ScrDyn *values, ScrDyn *tokens, const ScrDyn *desc,
|
|
const ScrStr *name, const ScrStr *raw, size_t index,
|
|
const ScrDyn *value, bool flag_value, int inline_value,
|
|
bool withhold_proto) {
|
|
/* Node deliberately withholds this key from the null-prototype values
|
|
* dictionary when it was the ORIGINAL option name. A normalized
|
|
* `--no-__proto__` passes this check before normalization in Node and is
|
|
* therefore stored safely on the null-prototype dictionary. */
|
|
if (withhold_proto && pa_str_eq_c(name, "__proto__")) {
|
|
if (tokens) {
|
|
scr_dyn_arr_push(tokens,
|
|
pa_option_token(name, raw, index, value, inline_value));
|
|
}
|
|
return;
|
|
}
|
|
ScrDyn *stored = value ? scr_dyn_retain((ScrDyn *)value)
|
|
: scr_dyn_new_bool(flag_value);
|
|
if (desc && pa_desc_multiple(desc)) {
|
|
ScrDyn *arr = scr_dyn_obj_get(values, name->data, name->len);
|
|
if (!arr) {
|
|
arr = scr_dyn_new_arr();
|
|
scr_dyn_obj_set(values, name->data, name->len, arr);
|
|
}
|
|
scr_dyn_arr_push(arr, stored);
|
|
} else {
|
|
scr_dyn_obj_set(values, name->data, name->len, stored);
|
|
}
|
|
if (tokens) {
|
|
scr_dyn_arr_push(tokens,
|
|
pa_option_token(name, raw, index, value, inline_value));
|
|
}
|
|
}
|
|
|
|
static void pa_store_flag(ScrDyn *values, ScrDyn *tokens,
|
|
const ScrDyn *options, const ScrDyn *desc,
|
|
const ScrStr *name, const ScrStr *raw, size_t index,
|
|
bool allow_negative) {
|
|
if (allow_negative && pa_is_negative_name(name)) {
|
|
ScrStr *positive = pa_str_bytes(name->data + 3, name->len - 3);
|
|
const ScrDyn *positive_desc = pa_find_long(options, positive);
|
|
pa_store(values, tokens, positive_desc, positive, raw, index,
|
|
NULL, false, -1, false);
|
|
scr_str_release(positive);
|
|
return;
|
|
}
|
|
pa_store(values, tokens, desc, name, raw, index, NULL, true, -1, true);
|
|
}
|
|
|
|
static void pa_positional(ScrDyn *positionals, ScrDyn *tokens,
|
|
const ScrDyn *value, size_t index) {
|
|
scr_dyn_arr_push(positionals, scr_dyn_retain((ScrDyn *)value));
|
|
if (!tokens) return;
|
|
ScrDyn *token = scr_dyn_new_obj();
|
|
scr_dyn_obj_set(token, "kind", 4, pa_dyn_str_bytes("positional", 10));
|
|
scr_dyn_obj_set(token, "index", 5, scr_dyn_new_num((double)index));
|
|
scr_dyn_obj_set(token, "value", 5, scr_dyn_retain((ScrDyn *)value));
|
|
scr_dyn_arr_push(tokens, token);
|
|
}
|
|
|
|
static ScrDyn *pa_default_args(void) {
|
|
ScrDyn *args = scr_dyn_new_arr();
|
|
ScrArr *argv = scr_process_argv();
|
|
/* Read the ONE live interned process.argv array: user pushes, pops, and
|
|
* replacements before parseArgs() are observable exactly as slice(2). */
|
|
for (size_t i = 2; i < argv->len; i++) {
|
|
ScrStr *arg = scr_arr_get_ref(argv, (double)i);
|
|
scr_dyn_arr_push(args, pa_dyn_str(arg));
|
|
scr_str_release(arg);
|
|
}
|
|
scr_arr_release(argv);
|
|
return args;
|
|
}
|
|
|
|
/* Materialize typed array references for indexed native reads. */
|
|
static ScrDyn *pa_materialize_array(const ScrDyn *value) {
|
|
if (value->kind == SCR_DYN_TYPED_REF) return scr_dyn_typed_ref_materialize(value);
|
|
return scr_dyn_retain((ScrDyn *)value);
|
|
}
|
|
|
|
static ScrDyn *pa_owned_member(const ScrDyn *obj, const char *key) {
|
|
if (obj->kind != SCR_DYN_OBJ) return NULL;
|
|
const ScrDyn *value = pa_own_member(obj, key);
|
|
return value ? scr_dyn_retain((ScrDyn *)value) : NULL;
|
|
}
|
|
|
|
static ScrDyn *pa_materialize_descriptor(const ScrDyn *desc) {
|
|
if (desc->kind == SCR_DYN_TYPED_REF) {
|
|
ScrDyn *view = scr_dyn_typed_ref_materialize(desc);
|
|
ScrDyn *out = pa_materialize_descriptor(view);
|
|
scr_dyn_release(view);
|
|
return out;
|
|
}
|
|
|
|
if (desc->kind != SCR_DYN_OBJ) {
|
|
return scr_dyn_retain((ScrDyn *)desc);
|
|
}
|
|
|
|
static const char *const fields[] = {"type", "short", "multiple", "default"};
|
|
ScrDyn *out = scr_dyn_new_obj();
|
|
for (size_t i = 0; i < sizeof fields / sizeof fields[0]; i++) {
|
|
ScrDyn *value = pa_owned_member(desc, fields[i]);
|
|
if (!value) {
|
|
if (scr_exc_pending()) { scr_dyn_release(out); return NULL; }
|
|
continue;
|
|
}
|
|
|
|
ScrDyn *native = strcmp(fields[i], "default") == 0 &&
|
|
value->kind == SCR_DYN_TYPED_REF
|
|
? scr_dyn_retain(value)
|
|
: pa_materialize_array(value);
|
|
scr_dyn_release(value);
|
|
if (!native) { scr_dyn_release(out); return NULL; }
|
|
scr_dyn_obj_set(out, fields[i], strlen(fields[i]), native);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static ScrDyn *pa_materialize_options(const ScrDyn *options) {
|
|
if (options->kind == SCR_DYN_TYPED_REF) {
|
|
ScrDyn *view = scr_dyn_typed_ref_materialize(options);
|
|
ScrDyn *out = pa_materialize_options(view);
|
|
scr_dyn_release(view);
|
|
return out;
|
|
}
|
|
|
|
if (options->kind != SCR_DYN_OBJ) {
|
|
return scr_dyn_retain((ScrDyn *)options);
|
|
}
|
|
|
|
ScrDyn *entries = scr_dyn_obj_entries(options);
|
|
if (!entries) return NULL;
|
|
ScrDyn *out = scr_dyn_new_obj();
|
|
for (size_t i = 0; i < entries->v.arr.len; i++) {
|
|
const ScrDyn *pair = entries->v.arr.items[i];
|
|
const ScrStr *name = pair->v.arr.items[0]->v.str;
|
|
ScrDyn *desc = pa_materialize_descriptor(pair->v.arr.items[1]);
|
|
if (!desc) {
|
|
scr_dyn_release(out);
|
|
scr_dyn_release(entries);
|
|
return NULL;
|
|
}
|
|
scr_dyn_obj_set(out, name->data, name->len, desc);
|
|
}
|
|
scr_dyn_release(entries);
|
|
return out;
|
|
}
|
|
|
|
static ScrDyn *pa_materialize_config(const ScrDyn *config) {
|
|
if (config->kind == SCR_DYN_TYPED_REF) {
|
|
ScrDyn *view = scr_dyn_typed_ref_materialize(config);
|
|
ScrDyn *out = pa_materialize_config(view);
|
|
scr_dyn_release(view);
|
|
return out;
|
|
}
|
|
if (config->kind != SCR_DYN_OBJ) {
|
|
return scr_dyn_retain((ScrDyn *)config);
|
|
}
|
|
static const char *const fields[] = {
|
|
"args", "strict", "allowPositionals", "tokens", "allowNegative", "options",
|
|
};
|
|
ScrDyn *out = scr_dyn_new_obj();
|
|
for (size_t i = 0; i < sizeof fields / sizeof fields[0]; i++) {
|
|
ScrDyn *value = pa_owned_member(config, fields[i]);
|
|
if (!value) {
|
|
if (scr_exc_pending()) { scr_dyn_release(out); return NULL; }
|
|
continue;
|
|
}
|
|
ScrDyn *native = strcmp(fields[i], "options") == 0
|
|
? pa_materialize_options(value)
|
|
: pa_materialize_array(value);
|
|
scr_dyn_release(value);
|
|
if (!native) { scr_dyn_release(out); return NULL; }
|
|
scr_dyn_obj_set(out, fields[i], strlen(fields[i]), native);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/* Whether tokenizing this string argument consumes the next array item as
|
|
* a separate value. This mirrors the greedy string-option cases only; the
|
|
* processing pass below still owns usage validation and token storage. */
|
|
static bool pa_consumes_next(const ScrDyn *options, const ScrStr *arg) {
|
|
if (arg->len >= 2 && arg->data[0] == '-' && arg->data[1] == '-') {
|
|
size_t eq = pa_long_eq(arg);
|
|
if (eq < arg->len) return false;
|
|
ScrStr *name = pa_str_bytes(arg->data + 2, eq - 2);
|
|
const ScrDyn *desc = pa_find_long(options, name);
|
|
bool consumes = desc && pa_desc_type(desc) == 1;
|
|
scr_str_release(name);
|
|
return consumes;
|
|
}
|
|
if (arg->len <= 1 || arg->data[0] != '-') return false;
|
|
double units = scr_str_utf16_len((ScrStr *)arg);
|
|
for (double at = 1; at < units; at++) {
|
|
ScrStr *short_name = scr_str_char_at((ScrStr *)arg, at);
|
|
ScrStr *name = NULL;
|
|
const ScrDyn *desc = pa_find_short(options, short_name, &name);
|
|
scr_str_release(name);
|
|
scr_str_release(short_name);
|
|
if (desc && pa_desc_type(desc) == 1) return at + 1 >= units;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
ScrDyn *scr_util_parse_args(const ScrDyn *config) {
|
|
/* Omitted config is lowered as {}, while an explicit undefined takes the
|
|
* default parameter just as Node does. Null keeps Object(null)'s useful
|
|
* failure; other primitive/array wrappers have no config members. */
|
|
if (!config || config->kind == SCR_DYN_NULL) {
|
|
static const char msg[] = "Cannot convert undefined or null to object";
|
|
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
|
|
return NULL;
|
|
}
|
|
ScrDyn *owned_config = pa_materialize_config(config);
|
|
if (!owned_config) return NULL;
|
|
const ScrDyn *cfg = owned_config->kind == SCR_DYN_OBJ ? owned_config : NULL;
|
|
/* Node validates the args container before every flag and before the
|
|
* option schema. Its element values are intentionally not prevalidated:
|
|
* non-string values can still become loose-mode positional tokens. */
|
|
ScrDyn *owned_args = NULL;
|
|
const ScrDyn *args = pa_member(cfg, "args");
|
|
if (!args || args->kind == SCR_DYN_NULL) {
|
|
owned_args = pa_default_args();
|
|
args = owned_args;
|
|
} else if (args->kind != SCR_DYN_ARR) {
|
|
scr_dyn_arg_type_fail("args", "an instance of Array", args);
|
|
scr_dyn_release(owned_config);
|
|
return NULL;
|
|
}
|
|
|
|
bool ok = true;
|
|
bool strict = pa_config_bool(cfg, "strict", true, &ok);
|
|
if (!ok) { scr_dyn_release(owned_args); scr_dyn_release(owned_config); return NULL; }
|
|
bool allow_positionals = pa_config_bool(cfg, "allowPositionals", !strict, &ok);
|
|
if (!ok) { scr_dyn_release(owned_args); scr_dyn_release(owned_config); return NULL; }
|
|
bool return_tokens = pa_config_bool(cfg, "tokens", false, &ok);
|
|
if (!ok) { scr_dyn_release(owned_args); scr_dyn_release(owned_config); return NULL; }
|
|
bool allow_negative = pa_config_bool(cfg, "allowNegative", false, &ok);
|
|
if (!ok) { scr_dyn_release(owned_args); scr_dyn_release(owned_config); return NULL; }
|
|
|
|
const ScrDyn *options = pa_member(cfg, "options");
|
|
if (options && options->kind == SCR_DYN_NULL) options = NULL;
|
|
if (!pa_validate_options(options)) {
|
|
scr_dyn_release(owned_args);
|
|
scr_dyn_release(owned_config);
|
|
return NULL;
|
|
}
|
|
|
|
/* Node tokenizes the complete input before applying strict usage checks.
|
|
* In the checked-dynamic alphabet, the only tokenizer-time exception is
|
|
* a nullish argument before `--` (other non-strings become positionals),
|
|
* so preflight that failure before the processing pass below. */
|
|
bool scan_after_terminator = false;
|
|
for (size_t i = 0; i < args->v.arr.len; i++) {
|
|
const ScrDyn *arg_value = args->v.arr.items[i];
|
|
if (scan_after_terminator) continue;
|
|
if (arg_value->kind == SCR_DYN_NULL ||
|
|
arg_value->kind == SCR_DYN_UNDEF) {
|
|
pa_nullish_arg_length(arg_value);
|
|
scr_dyn_release(owned_args);
|
|
scr_dyn_release(owned_config);
|
|
return NULL;
|
|
}
|
|
if (arg_value->kind != SCR_DYN_STR) continue;
|
|
if (pa_str_eq_c(arg_value->v.str, "--")) {
|
|
scan_after_terminator = true;
|
|
continue;
|
|
}
|
|
if (pa_consumes_next(options, arg_value->v.str) &&
|
|
i + 1 < args->v.arr.len &&
|
|
args->v.arr.items[i + 1]->kind != SCR_DYN_NULL &&
|
|
args->v.arr.items[i + 1]->kind != SCR_DYN_UNDEF) {
|
|
i++;
|
|
}
|
|
}
|
|
|
|
ScrDyn *result = scr_dyn_new_obj();
|
|
ScrDyn *values = scr_dyn_new_obj_null_proto();
|
|
ScrDyn *positionals = scr_dyn_new_arr();
|
|
ScrDyn *tokens = return_tokens ? scr_dyn_new_arr() : NULL;
|
|
bool after_terminator = false;
|
|
|
|
for (size_t i = 0; i < args->v.arr.len; i++) {
|
|
const ScrDyn *arg_value = args->v.arr.items[i];
|
|
if (after_terminator) {
|
|
if (!allow_positionals) {
|
|
pa_unexpected_dyn(arg_value);
|
|
goto fail;
|
|
}
|
|
pa_positional(positionals, tokens, arg_value, i);
|
|
continue;
|
|
}
|
|
if (arg_value->kind == SCR_DYN_NULL || arg_value->kind == SCR_DYN_UNDEF) {
|
|
pa_nullish_arg_length(arg_value);
|
|
goto fail;
|
|
}
|
|
if (arg_value->kind != SCR_DYN_STR) {
|
|
if (!allow_positionals) {
|
|
pa_unexpected_dyn(arg_value);
|
|
goto fail;
|
|
}
|
|
pa_positional(positionals, tokens, arg_value, i);
|
|
continue;
|
|
}
|
|
const ScrStr *arg = arg_value->v.str;
|
|
if (pa_str_eq_c(arg, "--")) {
|
|
after_terminator = true;
|
|
if (tokens) {
|
|
ScrDyn *token = scr_dyn_new_obj();
|
|
scr_dyn_obj_set(token, "kind", 4,
|
|
pa_dyn_str_bytes("option-terminator", 17));
|
|
scr_dyn_obj_set(token, "index", 5, scr_dyn_new_num((double)i));
|
|
scr_dyn_arr_push(tokens, token);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (arg->len >= 2 && arg->data[0] == '-' && arg->data[1] == '-') {
|
|
size_t eq = pa_long_eq(arg);
|
|
/* The presence test starts after one name byte, so `--=x` stays the
|
|
* lone option "=x". Once present, Node splits at the FIRST equals:
|
|
* `--==x` therefore has the empty name and value "=x". */
|
|
ScrStr *name = pa_str_bytes(arg->data + 2, eq - 2);
|
|
ScrStr *raw = pa_str_bytes(arg->data, eq);
|
|
const ScrDyn *desc = pa_find_long(options, name);
|
|
if (eq < arg->len) {
|
|
ScrStr *checked_name = NULL;
|
|
const ScrDyn *checked_desc = desc;
|
|
if (strict && !checked_desc && allow_negative &&
|
|
pa_is_negative_name(name)) {
|
|
checked_name = pa_str_bytes(name->data + 3, name->len - 3);
|
|
const ScrDyn *positive_desc = pa_find_long(options, checked_name);
|
|
if (positive_desc && pa_desc_type(positive_desc) == 0) {
|
|
checked_desc = positive_desc;
|
|
}
|
|
}
|
|
if (strict && !checked_desc) {
|
|
pa_unknown(raw, allow_positionals);
|
|
scr_str_release(checked_name);
|
|
scr_str_release(raw);
|
|
scr_str_release(name);
|
|
goto fail;
|
|
}
|
|
if (strict && pa_desc_type(checked_desc) == 0) {
|
|
pa_takes_no_value(checked_desc,
|
|
checked_name ? checked_name : name);
|
|
scr_str_release(checked_name);
|
|
scr_str_release(raw);
|
|
scr_str_release(name);
|
|
goto fail;
|
|
}
|
|
scr_str_release(checked_name);
|
|
ScrStr *value = pa_str_bytes(arg->data + eq + 1, arg->len - eq - 1);
|
|
ScrDyn *dyn_value = pa_dyn_str(value);
|
|
pa_store(values, tokens, desc, name, raw, i, dyn_value, true, 1, true);
|
|
scr_dyn_release(dyn_value);
|
|
scr_str_release(value);
|
|
} else if (desc && pa_desc_type(desc) == 1) {
|
|
const ScrDyn *next = i + 1 < args->v.arr.len
|
|
? args->v.arr.items[i + 1] : NULL;
|
|
if (next && next->kind != SCR_DYN_NULL && next->kind != SCR_DYN_UNDEF) {
|
|
i++;
|
|
if (strict && next->kind != SCR_DYN_STR) {
|
|
pa_missing_value(desc, name);
|
|
scr_str_release(raw);
|
|
scr_str_release(name);
|
|
goto fail;
|
|
}
|
|
if (strict && next->kind == SCR_DYN_STR &&
|
|
next->v.str->len > 1 && next->v.str->data[0] == '-') {
|
|
pa_ambiguous(raw, name, false);
|
|
scr_str_release(raw);
|
|
scr_str_release(name);
|
|
goto fail;
|
|
}
|
|
pa_store(values, tokens, desc, name, raw, i - 1, next, true, 0, true);
|
|
} else if (strict) {
|
|
pa_missing_value(desc, name);
|
|
scr_str_release(raw);
|
|
scr_str_release(name);
|
|
goto fail;
|
|
} else {
|
|
if (allow_negative && pa_is_negative_name(name)) {
|
|
ScrStr *positive = pa_str_bytes(name->data + 3, name->len - 3);
|
|
const ScrDyn *positive_desc = pa_find_long(options, positive);
|
|
pa_store(values, tokens, positive_desc, positive, raw, i,
|
|
NULL, false, -1, false);
|
|
scr_str_release(positive);
|
|
} else {
|
|
pa_store(values, tokens, desc, name, raw, i, NULL, true, -1, true);
|
|
}
|
|
}
|
|
} else {
|
|
ScrStr *positive = NULL;
|
|
const ScrDyn *positive_desc = NULL;
|
|
const bool negative = allow_negative && pa_is_negative_name(name);
|
|
if (negative) {
|
|
positive = pa_str_bytes(name->data + 3, name->len - 3);
|
|
positive_desc = pa_find_long(options, positive);
|
|
}
|
|
if (strict && !desc &&
|
|
(!negative || !positive_desc || pa_desc_type(positive_desc) != 0)) {
|
|
pa_unknown(raw, allow_positionals);
|
|
scr_str_release(positive);
|
|
scr_str_release(raw);
|
|
scr_str_release(name);
|
|
goto fail;
|
|
}
|
|
if (negative) {
|
|
pa_store(values, tokens, positive_desc, positive, raw, i,
|
|
NULL, false, -1, false);
|
|
scr_str_release(positive);
|
|
} else {
|
|
pa_store(values, tokens, desc, name, raw, i, NULL, true, -1, true);
|
|
}
|
|
}
|
|
scr_str_release(raw);
|
|
scr_str_release(name);
|
|
continue;
|
|
}
|
|
|
|
if (arg->len > 1 && arg->data[0] == '-') {
|
|
double units = scr_str_utf16_len((ScrStr *)arg);
|
|
for (double at = 1; at < units; at++) {
|
|
ScrStr *short_name = scr_str_char_at((ScrStr *)arg, at);
|
|
ScrStr *name = NULL;
|
|
const ScrDyn *desc = pa_find_short(options, short_name, &name);
|
|
char *raw_bytes = malloc(short_name->len + 1);
|
|
if (!raw_bytes) {
|
|
fputs("scriptc: out of memory\n", stderr);
|
|
abort();
|
|
}
|
|
raw_bytes[0] = '-';
|
|
memcpy(raw_bytes + 1, short_name->data, short_name->len);
|
|
ScrStr *raw = pa_str_bytes(raw_bytes, short_name->len + 1);
|
|
free(raw_bytes);
|
|
if (strict && !desc) {
|
|
pa_unknown(raw, allow_positionals);
|
|
scr_str_release(raw);
|
|
scr_str_release(name);
|
|
scr_str_release(short_name);
|
|
goto fail;
|
|
}
|
|
if (desc && pa_desc_type(desc) == 1) {
|
|
if (at + 1 < units) {
|
|
ScrStr *value = scr_str_slice((ScrStr *)arg, at + 1, INFINITY);
|
|
ScrDyn *dyn_value = pa_dyn_str(value);
|
|
pa_store(values, tokens, desc, name, raw, i, dyn_value, true, 1, true);
|
|
scr_dyn_release(dyn_value);
|
|
scr_str_release(value);
|
|
} else if (i + 1 < args->v.arr.len &&
|
|
args->v.arr.items[i + 1]->kind != SCR_DYN_NULL &&
|
|
args->v.arr.items[i + 1]->kind != SCR_DYN_UNDEF) {
|
|
const ScrDyn *value = args->v.arr.items[++i];
|
|
if (strict && value->kind != SCR_DYN_STR) {
|
|
pa_missing_value(desc, name);
|
|
scr_str_release(raw);
|
|
scr_str_release(name);
|
|
scr_str_release(short_name);
|
|
goto fail;
|
|
}
|
|
if (strict && value->kind == SCR_DYN_STR &&
|
|
value->v.str->len > 1 && value->v.str->data[0] == '-') {
|
|
pa_ambiguous(raw, name, true);
|
|
scr_str_release(raw);
|
|
scr_str_release(name);
|
|
scr_str_release(short_name);
|
|
goto fail;
|
|
}
|
|
pa_store(values, tokens, desc, name, raw, i - 1, value, true, 0, true);
|
|
} else if (strict) {
|
|
pa_missing_value(desc, name);
|
|
scr_str_release(raw);
|
|
scr_str_release(name);
|
|
scr_str_release(short_name);
|
|
goto fail;
|
|
} else {
|
|
pa_store_flag(values, tokens, options, desc, name, raw, i,
|
|
allow_negative);
|
|
}
|
|
scr_str_release(raw);
|
|
scr_str_release(name);
|
|
scr_str_release(short_name);
|
|
break;
|
|
}
|
|
pa_store_flag(values, tokens, options, desc, name, raw, i,
|
|
allow_negative);
|
|
scr_str_release(raw);
|
|
scr_str_release(name);
|
|
scr_str_release(short_name);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (!allow_positionals) {
|
|
pa_unexpected(arg);
|
|
goto fail;
|
|
}
|
|
pa_positional(positionals, tokens, arg_value, i);
|
|
}
|
|
|
|
if (options) {
|
|
ScrDyn *entries = scr_dyn_obj_entries(options);
|
|
for (size_t i = 0; i < entries->v.arr.len; i++) {
|
|
const ScrDyn *pair = entries->v.arr.items[i];
|
|
const ScrStr *name = pair->v.arr.items[0]->v.str;
|
|
const ScrDyn *desc = pair->v.arr.items[1];
|
|
if (pa_str_eq_c(name, "__proto__")) continue;
|
|
if (scr_dyn_obj_get(values, name->data, name->len)) continue;
|
|
const ScrDyn *def = pa_member(desc, "default");
|
|
if (def) {
|
|
scr_dyn_obj_set(values, name->data, name->len,
|
|
scr_dyn_retain((ScrDyn *)def));
|
|
}
|
|
}
|
|
scr_dyn_release(entries);
|
|
}
|
|
|
|
scr_dyn_obj_set(result, "values", 6, values);
|
|
scr_dyn_obj_set(result, "positionals", 11, positionals);
|
|
if (tokens) scr_dyn_obj_set(result, "tokens", 6, tokens);
|
|
scr_dyn_release(owned_args);
|
|
scr_dyn_release(owned_config);
|
|
return result;
|
|
|
|
fail:
|
|
scr_dyn_release(tokens);
|
|
scr_dyn_release(positionals);
|
|
scr_dyn_release(values);
|
|
scr_dyn_release(result);
|
|
scr_dyn_release(owned_args);
|
|
scr_dyn_release(owned_config);
|
|
return NULL;
|
|
}
|