init
This commit is contained in:
commit
d8c1dc196d
161
arg_parser.sh
Executable file
161
arg_parser.sh
Executable file
@ -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 "}"
|
||||
189
parser.js
Normal file
189
parser.js
Normal file
@ -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 = "<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);
|
||||
});
|
||||
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"
|
||||
*
|
||||
*/
|
||||
Loading…
Reference in New Issue
Block a user