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 = ";..." * Entry.type "command": ";;" * Entry.type "argument": ";;;..." * * "arg_end", "arg_rest" are provided without need to specify it in the spec. * * Attributes: * - required * - help: * - default: * - map: * - 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" * */