259 lines
7.5 KiB
JavaScript
259 lines
7.5 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",
|
|
REST: "rest",
|
|
};
|
|
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);
|
|
});
|
|
|
|
// Set default values
|
|
if (!attributes[ATTR.MAP]) {
|
|
attributes[ATTR.MAP] = entry.aliases[0]; // alias 0 is always the larger format
|
|
}
|
|
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.assign(this, ctx_defaults);
|
|
},
|
|
};
|
|
for (const raw_entry of spec) {
|
|
const entry = parse_spec_entry(raw_entry, context);
|
|
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) {
|
|
const alias_id = `${cmd_id}.${entry.attr[ATTR.POSITION]}`;
|
|
spec_aliases[alias_id] = `${cmd_id}.${entry.id}`;
|
|
} else if (
|
|
entry.attr.type === ATTR_TYPE.OPTION ||
|
|
entry.attr.type === ATTR_TYPE.FLAG
|
|
) {
|
|
const alias_id = `${cmd_id}.${alias}`;
|
|
spec_aliases[alias_id] = `${cmd_id}.${entry.id}`;
|
|
} else if (entry.attr.type === ATTR_TYPE.REST) {
|
|
const alias_id = `${cmd_id}._rest`;
|
|
spec_aliases[alias_id] = `${cmd_id}.${entry.id}`;
|
|
}
|
|
});
|
|
},
|
|
}[entry.type]();
|
|
context.reset();
|
|
if (exit) break;
|
|
}
|
|
|
|
if (command && cmd_id === null) {
|
|
throw new Error(`Invalid command: ${command}`);
|
|
}
|
|
|
|
return {
|
|
data: spec_data,
|
|
aliases: spec_aliases,
|
|
cmd_id: command ? cmd_id : null,
|
|
};
|
|
}
|
|
|
|
const r = parse_spec(SPEC);
|
|
// console.log(JSON.stringify(r, null, 2));
|
|
|
|
/**
|
|
* @return {<variable_name>:<value>}
|
|
*/
|
|
const inspect = (v) => console.log(JSON.stringify(v, null, 2));
|
|
function parse_args(spec, args_raw) {
|
|
const vars = {};
|
|
const args = args_raw.split(" ");
|
|
args.shift(); // script name
|
|
const cmd = args.shift();
|
|
|
|
const get_next = () => args[0];
|
|
const consume = () => args.shift();
|
|
const consume_all = () => args.splice(0);
|
|
|
|
const cmd_spec = parse_spec(spec, cmd);
|
|
const cmd_info = cmd_spec.data[cmd_spec.cmd_id];
|
|
let positional_count = 0;
|
|
while (args.length > 0) {
|
|
const next = get_next();
|
|
const [type, next_arg] = (() => {
|
|
if (next === "--") {
|
|
return [ATTR_TYPE.REST, "_rest"]; // check if spec if expecing a rest arg
|
|
} else if (next.startsWith("--")) {
|
|
return [ATTR_TYPE.OPTION, next.slice(2)]; // invalid type. use only to know if to search per key or whole string
|
|
} else if (next.startsWith("-")) {
|
|
return [ATTR_TYPE.FLAG, next.slice(1)]; // same
|
|
} else {
|
|
return [ATTR_TYPE.POSITIONAL, positional_count++];
|
|
}
|
|
})();
|
|
const alias_id = `${cmd_info.id}.${next_arg}`;
|
|
const next_arg_id = cmd_spec.aliases[alias_id];
|
|
if (typeof next_arg_id !== "undefined") {
|
|
consume();
|
|
const next_arg_info = cmd_spec.data[next_arg_id];
|
|
if (next_arg_info.attr.type === ATTR_TYPE.OPTION) {
|
|
vars[next_arg_info.attr.map] = consume();
|
|
} else if (next_arg_info.attr.type === ATTR_TYPE.FLAG) {
|
|
vars[next_arg_info.attr.map] = true;
|
|
} else if (next_arg_info.attr.type === ATTR_TYPE.REST) {
|
|
vars[next_arg_info.attr.map] = consume_all();
|
|
} else if (next_arg_info.attr.type === ATTR_TYPE.POSITIONAL) {
|
|
vars[next_arg_info.attr.map] = next;
|
|
}
|
|
} else {
|
|
inspect(vars);
|
|
throw new Error(`Invalid argument: '${next_arg}'`);
|
|
}
|
|
}
|
|
return vars;
|
|
}
|
|
|
|
const args = "dev build -q -v --image tm0:node mybox -v -- bla1 bla2";
|
|
inspect(parse_args(SPEC, args));
|