diff --git a/__test.js b/__test.js new file mode 100644 index 0000000..3d01b72 --- /dev/null +++ b/__test.js @@ -0,0 +1,15 @@ +const inputs = [ + "dev build --from mybox2 -abc -q -v --image tm0:node -v -- bla1 bla2", + "dev build --from mybox2 -abc valuec -q -v --image tm0:node mybox -v -- bla1 bla2", +]; + +let level = 1; +const commands = ["root", "build"]; +function ref(...rest) { + return commands + .slice(0, level + 1) + .concat(rest) + .join("."); +} + +console.log(ref("from")); diff --git a/arg_parser.sh b/arg_parser.sh deleted file mode 100755 index b90cd66..0000000 --- a/arg_parser.sh +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# ===== SPEC ===== -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" -) - -# ===== CONSTANTS ===== -ENTRY_COMMAND="command" -ENTRY_ARGUMENT="argument" - -ATTR_REQUIRED="required" -ATTR_HELP="help" -ATTR_DEFAULT="default" -ATTR_MAP="map" -ATTR_TYPE="type" -ATTR_REPEATABLE="repeatable" - -ATTR_TYPE_POSITIONAL="positional" -ATTR_TYPE_OPTION="option" -ATTR_TYPE_FLAG="flag" - -# ===== STORAGE ===== -declare -A SPEC_DATA -declare -A SPEC_ALIASES - -# ===== GENERATE IDS ===== -cmd_id=0 -arg_id=0 -get_cmd_id() { echo $((cmd_id++)); } -get_arg_id() { echo $((arg_id++)); } - -# ===== PARSE SPEC ENTRY ===== -parse_spec_entry() { - local raw_entry="$1" - IFS=';' read -r type alias_str rest <<<"$raw_entry" - IFS=',' read -r -a aliases <<<"$alias_str" - - declare -A attr - - if [[ "$type" == "$ENTRY_COMMAND" ]]; then - attr["$ATTR_HELP"]="$rest" - else - attr["$ATTR_REQUIRED"]=false - attr["$ATTR_REPEATABLE"]=false - attr["$ATTR_TYPE"]="$ATTR_TYPE_POSITIONAL" - attr["$ATTR_MAP"]="" - attr["$ATTR_DEFAULT"]="" - attr["$ATTR_HELP"]="" - - # Re-split rest on ';' - IFS=';' read -r -a elements <<<"$rest" - for element in "${elements[@]}"; do - [[ -z "$element" ]] && continue - IFS=':' read -r attribute value <<<"$element" - case "$attribute" in - "$ATTR_REQUIRED") attr["$ATTR_REQUIRED"]=true ;; - "$ATTR_REPEATABLE") attr["$ATTR_REPEATABLE"]=true ;; - "$ATTR_TYPE") attr["$ATTR_TYPE"]="$value" ;; - "$ATTR_MAP") attr["$ATTR_MAP"]="$value" ;; - "$ATTR_DEFAULT") attr["$ATTR_DEFAULT"]="$value" ;; - "$ATTR_HELP") attr["$ATTR_HELP"]="$value" ;; - esac - done - fi - - # Serialize attributes to string - local attr_str="" - for k in "${!attr[@]}"; do - attr_str+="$k=${attr[$k]}|" - done - attr_str="${attr_str%|}" - - echo "$type;$alias_str;$attr_str" -} - -# ===== PARSE SPEC ===== -parse_spec() { - local command_filter="${1:-}" - local skip_cmd=false - local exit_on_next_cmd=false - local current_cmd_id="" - - for raw_entry in "${SPEC[@]}"; do - local entry - entry="$(parse_spec_entry "$raw_entry")" - IFS=';' read -r type alias_str attr_str <<<"$entry" - IFS=',' read -r -a aliases <<<"$alias_str" - - if [[ "$type" == "$ENTRY_COMMAND" ]]; then - if [[ "$exit_on_next_cmd" == true ]]; then - break - fi - - if [[ -n "$command_filter" ]]; then - local found=false - for a in "${aliases[@]}"; do - [[ "$a" == "$command_filter" ]] && found=true && break - done - if [[ "$found" == false ]]; then - skip_cmd=true - continue - fi - skip_cmd=false - exit_on_next_cmd=true - fi - - current_cmd_id="$(get_cmd_id)" - SPEC_DATA["$current_cmd_id.type"]="$type" - SPEC_DATA["$current_cmd_id.aliases"]="$alias_str" - SPEC_DATA["$current_cmd_id.attr"]="$attr_str" - SPEC_DATA["$current_cmd_id.id"]="$current_cmd_id" - - for a in "${aliases[@]}"; do - SPEC_ALIASES["$a"]="$current_cmd_id" - done - elif [[ "$type" == "$ENTRY_ARGUMENT" ]]; then - if [[ "$skip_cmd" == true ]]; then - continue - fi - local this_arg_id - this_arg_id="$(get_arg_id)" - SPEC_DATA["$current_cmd_id.$this_arg_id.type"]="$type" - SPEC_DATA["$current_cmd_id.$this_arg_id.aliases"]="$alias_str" - SPEC_DATA["$current_cmd_id.$this_arg_id.attr"]="$attr_str" - SPEC_DATA["$current_cmd_id.$this_arg_id.id"]="$this_arg_id" - - for a in "${aliases[@]}"; do - SPEC_ALIASES["$current_cmd_id.$a"]="$this_arg_id" - done - fi - done -} - -# ===== MAIN ===== -parse_spec - -# Print results as JSON-like structure -echo "{" -echo " \"spec_data\": {" -for k in "${!SPEC_DATA[@]}"; do - printf ' "%s": "%s",\n' "$k" "${SPEC_DATA[$k]}" -done -echo " }," -echo " \"spec_aliases\": {" -for k in "${!SPEC_ALIASES[@]}"; do - printf ' "%s": "%s",\n' "$k" "${SPEC_ALIASES[$k]}" -done -echo " }" -echo "}" diff --git a/barg.js b/barg.js new file mode 100644 index 0000000..f7f1abf --- /dev/null +++ b/barg.js @@ -0,0 +1,281 @@ +const ELEMENTS_SEP = ";"; +const ALIAS_SEP = ","; +const ATTR_SEP = ":"; + +let var_values = {}; +let required_values = []; + +let schema = {}; +let alias_map = {}; + +let position = 0; +let level = 1; +let commands = ["root"]; + +function register_command(raw_entry) { + const header = raw_entry.split(ELEMENTS_SEP).slice(1); + const aliases = header.shift().split(ALIAS_SEP); + const name = aliases[0]; + const help = header.shift(); + + // Schema + schema[ref(name, "type")] = "command"; + schema[ref(name, "help")] = help; + + // Alias + for (const alias of aliases) { + alias_map[ref(`unk::${alias}`)] = ref(name); + } + + // Control + commands.push(name); + level += 1; + position = 0; +} + +function end_command() { + level -= 1; + commands.pop(); +} + +function register_argument(raw_entry) { + const elements = raw_entry.split(ELEMENTS_SEP).slice(1); + let aliases = elements.shift().split(ALIAS_SEP); + let name = aliases[0]; + const entry = { + required: false, + kind: "positional", + }; + for (const element of elements) { + const [attribute, attr_value] = element.split(ATTR_SEP); + + if (attribute === "required") entry.required = true; + else if (attribute === "repeatable") entry.repeatable = true; + else if (attribute === "help") entry.help = attr_value; + else if (attribute === "default") entry.default_value = attr_value; + else if (attribute === "dest") entry.dest = attr_value; + else if (attribute === "type") entry.kind = attr_value; + + // Defaults + if (entry.dest === undefined) entry.dest = name; + if (entry.value === undefined) entry.value = entry.default_value; + } + + // Alias + if (entry.kind === "positional") { + name = String(position++); + aliases = [`pos::${name}`]; + } else if (entry.kind === "rest") { + aliases = ["__rest"]; + } + + for (const alias of aliases) { + if (entry.kind === "option" || entry.kind === "flag") + alias_map[ref(`narg::${alias}`)] = ref(name); + else alias_map[ref(alias)] = ref(name); + } + + // Schema + schema[ref(name, "type")] = "argument"; + for (const [key, value] of Object.entries(entry)) { + schema[ref(name, key)] = value; + } +} + +function generate_schema(spec) { + for (const entry of spec) { + const elements = entry.split(ELEMENTS_SEP); + const type = elements.shift(); + + switch (type) { + case "command": + register_command(entry); + break; + case "end": + end_command(); + break; + case "argument": + register_argument(entry); + break; + default: + throw new Error(`Invalid entry type: '${type}'`); + } + } + + position = 0; + level = 1; + commands = ["root"]; +} + +function parse_input(spec, input) { + // rest + var_values = {}; + required_values = []; + position = 0; + level = 1; + commands = ["root"]; + schema = {}; + alias_map = {}; + + // code + generate_schema(spec); + const tokens = tokenize(input); + + // log({ schema, alias_map }); + + function next_token() { + const tagged_token = tokens.shift(); + const [type, token] = tagged_token.split("::"); + return { original: tagged_token, type, token }; + } + + function next_value() { + const tagged_token = next_token(); + if (tagged_token.type !== "unk") { + throw new Error(`Expected option value, but got: ${tagged_token.token}`); + } + return tagged_token.token; + } + + while (tokens.length > 0) { + const { + original: token, + type: token_type, + token: token_value, + } = next_token(); + if (token_type === "rest") { + var_values["__rest"] = token_value; + continue; + } + + let found = false; + for (let i = commands.length; i >= 1; i--) { + level = i; + let entry_ref = alias_map[ref(token)]; + + if (!entry_ref && level === commands.length && token_type === "unk") { + entry_ref = alias_map[ref(`pos::${position++}`)]; + } + + if (!entry_ref) { + continue; + } + + const [_token_type, token_value] = token.split("::"); + const type = schema[join(entry_ref, "type")]; + if (type === "command") { + commands.push(token_value); + level += commands.length; + } else if (type === "argument") { + const kind = schema[join(entry_ref, "kind")]; + const dest = schema[join(entry_ref, "dest")]; + const value = schema[join(entry_ref, "value")]; + + var_values[dest] = value; // default one? + + if (kind === "flag") var_values[dest] = true; + else if (kind === "option") var_values[dest] = next_value(); + else if (kind === "positional") var_values[dest] = token_value; + else if (kind === "rest") var_values[dest] = token_value; + } + + found = true; + break; + } + + if (!found) { + console.log(`(Skipping invalid argument: '${token_value}'`); + } + } + + log({ commands, var_values }); + return var_values; +} + +function tokenize(input) { + const raw_tokens = input.split(/\s+/); + raw_tokens.shift(); // drop command + + const tokens = []; + while (raw_tokens.length > 0) { + let token = take_token(raw_tokens); + let value; + + if (token === "--") { + tokens.push(`rest::${take_all(raw_tokens).join(" ")}`); + continue; + } + + if (token.startsWith("--")) { + [token, value] = token.slice(2).split("="); + tokens.push(`narg::${token}`); + } else if (token.startsWith("-")) { + [token, value] = token.slice(1).split("="); + tokens.push(...token.split("").map((t) => `narg::${t}`)); + } else { + tokens.push(`unk::${token}`); + } + + if (value) { + tokens.push(`value::${value}`); + } + } + + return tokens; +} + +function take_token(tokens) { + return tokens.shift(); +} + +function take_all(tokens) { + const all = [...tokens]; + tokens.length = 0; + return all; +} + +function ref(...rest) { + return commands.slice(0, level).concat(rest).join("."); +} + +function join(...rest) { + return rest.filter((v) => !!v).join("."); +} + +function log(text) { + console.log(JSON.stringify(text, null, 2)); +} +const SPEC = [ + "command;build;Build a dev container", + "argument;from;type:option;map:from_name;help:Name of the base 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", + "argument;name;dest:name_opt;type:option;help:Building container name", + "command;container;Container building", + "argument;dev;type:flag;help:Is dev or not", + "argument;name;required;help:Building container name", + "end", + "argument;external;required;help:Name of the external container", + "end", + "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", + "end", +]; + +const INPUTS = [ + "dev build --from base-dev --image node:18 --include src --include test -v -v -q mycontainer -- npm run start", + "dev build container --dev --name webapp externalContainer", + "dev stop mycontainer --kill", + "dev build --from alpine --image node:20 mybox -- echo hi", +]; + +INPUTS.forEach((input) => { + console.log("\n\n------------------------\n"); + console.log("> ", input); + parse_input(SPEC, input); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/parser.js b/parser.js deleted file mode 100644 index ad2babd..0000000 --- a/parser.js +++ /dev/null @@ -1,258 +0,0 @@ -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));