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 = ";..." * 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); }); // 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 {:} */ 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));