commit d8c1dc196d3e147d21ef4eea126526595b505feb Author: Tomas Mirchev Date: Sun Oct 26 17:04:49 2025 +0200 init diff --git a/arg_parser.sh b/arg_parser.sh new file mode 100755 index 0000000..b90cd66 --- /dev/null +++ b/arg_parser.sh @@ -0,0 +1,161 @@ +#!/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/parser.js b/parser.js new file mode 100644 index 0000000..96368c1 --- /dev/null +++ b/parser.js @@ -0,0 +1,189 @@ +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" + * + */