#!/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 "}"