barg-parser/parser.js
2025-10-26 17:04:49 +02:00

190 lines
5.0 KiB
JavaScript

const SPEC = [
"command;build;Build a dev container",
"argument;name;type:positional;required;help:Name of the container",
"argument;image,i;type:option;required;map:image_name;help:Base image",
"argument;include,I;type:option;repeatable;map:include_paths;help:Include paths",
"argument;verbose,v;type:flag;repeatable;default:0;help:Increase verbosity (-v, -vv, ...)",
"argument;quiet,q;type:flag;default:false;help:Suppress all output",
"argument;cmd;type:rest;map:cmd_rest;help:Command and args to run inside the container",
"command;stop;Stop a dev container",
"argument;name;type:positional;required;help:Name of the container",
"argument;kill,k;type:flag;default:false;help:Force kill the container",
];
const ENTRY = {
COMMAND: "command",
ARGUMENT: "argument",
};
const ATTR = {
REQUIRED: "required",
HELP: "help",
DEFAULT: "default",
MAP: "map",
TYPE: "type",
REPEATABLE: "repeatable",
POSITION: "position",
};
const ATTR_TYPE = {
POSITIONAL: "positional",
OPTION: "option",
FLAG: "flag",
};
const CONTEXT = {
POS_COUNT: "pos_count",
};
/**
* Spec = Entry[]
* Entry = "<element>;...<element>"
* Entry.type "command": "<entry_type>;<identifiers>;<help_text>"
* Entry.type "argument": "<entry_type>;<identifiers>;<attribute>;...<attribute>"
*
* "arg_end", "arg_rest" are provided without need to specify it in the spec.
*
* Attributes:
* - required
* - help:<text>
* - default:<value>
* - map:<var_name>
* - type:<"positional"|"option"|"flag">
* For type.{option,flag}:
* - repeatable
*
*/
function parse_spec_entry(raw_entry, context) {
const elements = raw_entry.split(";");
const entry = {
type: elements.shift(),
aliases: elements.shift().split(","),
};
const attributes = {
[ENTRY.COMMAND]: () => {
return { help: elements.shift() };
},
[ENTRY.ARGUMENT]: () => {
const attributes = {
[ATTR.REQUIRED]: false,
[ATTR.REPEATABLE]: false,
[ATTR.TYPE]: ATTR_TYPE.POSITIONAL,
[ATTR.POSITION]: 0,
[ATTR.MAP]: null,
[ATTR.DEFAULT]: null,
[ATTR.HELP]: null,
};
elements.forEach((element) => {
const [attribute, attribute_value = null] = element.split(":");
({
[ATTR.REQUIRED]: (_) => {
attributes[ATTR.REQUIRED] = true;
},
[ATTR.REPEATABLE]: (_) => {
attributes[ATTR.REPEATABLE] = true;
},
[ATTR.TYPE]: (value) => {
attributes[ATTR.TYPE] = value;
if (value === ATTR_TYPE.POSITIONAL) {
attributes[ATTR.POSITION] = context[CONTEXT.POS_COUNT];
context[CONTEXT.POS_COUNT] += 1;
}
},
[ATTR.MAP]: (value) => {
attributes[ATTR.MAP] = value;
},
[ATTR.DEFAULT]: (value) => {
attributes[ATTR.DEFAULT] = value;
},
[ATTR.HELP]: (value) => {
attributes[ATTR.HELP] = value;
},
})[attribute](attribute_value);
});
return attributes;
},
}[entry.type]();
return { ...entry, attr: attributes };
}
function generate_id() {
let id = 0;
return () => id++;
}
function parse_spec(spec, command = null) {
const spec_data = {};
const spec_aliases = {};
const get_cmd_id = generate_id();
const get_arg_id = generate_id();
let skip_cmd = false;
let exit_on_next_cmd = false;
let cmd_id = null;
const ctx_defaults = { [CONTEXT.POS_COUNT]: 0 };
const context = {
...ctx_defaults,
reset() {
Object.assing(this, defaults);
},
};
for (const raw_entry of spec) {
const entry = parse_spec_entry(raw_entry, context);
console.log(entry.type);
const exit = {
[ENTRY.COMMAND]: () => {
// Previous command was populated
if (exit_on_next_cmd) return true;
// Looking for a single command
if (command !== null) {
// Not this one
if (!entry.aliases.includes(command)) {
skip_cmd = true;
return;
}
// Populate and exit
skip_cmd = false;
exit_on_next_cmd = true;
}
entry.id = cmd_id = get_cmd_id();
spec_data[entry.id] = entry;
entry.aliases.forEach((alias) => {
spec_aliases[alias] = entry.id;
});
},
[ENTRY.ARGUMENT]: () => {
// Do not populate command
if (skip_cmd) return;
entry.id = get_arg_id();
spec_data[`${cmd_id}.${entry.id}`] = entry;
entry.aliases.forEach((alias) => {
if (entry.attr.type === ATTR_TYPE.POSITIONAL) {
spec_aliases[`${cmd_id}.${entry.attr[ATTR.POSITION]}`] = entry.id;
} else {
spec_aliases[`${cmd_id}.${alias}`] = entry.id;
}
});
},
}[entry.type]();
if (exit) break;
}
return { spec_data, spec_aliases };
}
const r = parse_spec(SPEC, "build");
console.log(JSON.stringify(r, null, 2));
/**
* INPUT: build -i tm0:node mybox
*
* spec_aliases[build] -> "0"
* -i -> option/flag short -> spec_aliases["0.i"] -> "1"
*
*/