barg-parser/barg.sh
2025-10-30 09:57:38 +02:00

380 lines
9.9 KiB
Bash

#!/usr/bin/env bash
# Bash 5+
# TODO (NOT IMPLEMENTED):
# - repeatable attribute
# ---------- helpers ----------
join() {
local out="" sep="" x
for x in "$@"; do
[[ -n "$x" ]] || continue
out+="${sep}${x}"
sep="."
done
printf '%s' "$out"
}
# ---------- tokenizer ----------
tokenize() {
local input=$1
TOKENS=()
read -r -a args <<<"$input"
local root=${args[0]}
TOKENS+=("root::${root}")
args=("${args[@]:1}")
while ((${#args[@]})); do
local token=${args[0]}
args=("${args[@]:1}")
if [[ "$token" == "--" ]]; then
TOKENS+=("rest::${args[*]}")
break
fi
if [[ "$token" == --* ]]; then
local kv=${token#--}
local key=${kv%%=*}
TOKENS+=("narg::${key}")
if [[ "$kv" == *"="* ]]; then
local value=${kv#*=}
TOKENS+=("value::${value}")
fi
continue
fi
if [[ "$token" == -* ]]; then
local sv=${token#-}
local flags=${sv%%=*}
local value=
if [[ "$sv" == *"="* ]]; then value=${sv#*=}; fi
local i ch
for ((i = 0; i < ${#flags}; i++)); do
ch=${flags:i:1}
TOKENS+=("narg::${ch}")
done
[[ -n "$value" ]] && TOKENS+=("value::${value}")
continue
fi
TOKENS+=("unk::${token}")
done
}
# ---------- parser ----------
declare -A S A
generate_schema() {
S=()
A=()
local spec_name=$1
local -n SPEC_REF="$spec_name"
local _id=100
local level=-1
local -a pos=()
local -a cmds=()
for entry in "${SPEC_REF[@]}"; do
IFS=';' read -r -a parts <<<"$entry"
local type=${parts[0]}
case "$type" in
command)
local id=$_id
_id=$((_id + 1))
IFS=',' read -r -a aliases <<<"${parts[1]}"
local help="${parts[2]}"
S["$id.entryType"]="command"
S["$id.name"]="${aliases[0]}"
S["$id.help"]="$help"
S["$id.args"]=""
if ((${#cmds[@]} == 0)); then
A["cmd::root"]=$id
else
local last=$((${#cmds[@]} - 1))
for alias in "${aliases[@]}"; do
A["$(join "${cmds[$last]}" "cmd::${alias}")"]=$id
done
fi
level=$((level + 1))
cmds+=("$id")
pos+=(0)
;;
end)
level=$((level - 1))
if ((${#cmds[@]} > 0)); then cmds=("${cmds[@]:0:${#cmds[@]}-1}"); fi
if ((${#pos[@]} > 0)); then pos=("${pos[@]:0:${#pos[@]}-1}"); fi
;;
argument)
local id=$_id
_id=$((_id + 1))
IFS=',' read -r -a aliases <<<"${parts[1]}"
local name="${aliases[0]}"
local dest="$name"
local required="false"
local atype="positional"
local value=""
local help=""
local i kv k v
for ((i = 2; i < ${#parts[@]}; i++)); do
kv=${parts[i]}
if [[ "$kv" == *":"* ]]; then
k=${kv%%:*}
v=${kv#*:}
else
k=$kv
v="true"
fi
case "$k" in
dest) dest="$v" ;;
required) required="$v" ;;
type) atype="$v" ;;
help) help="$v" ;;
default) value="$v" ;;
repeatable) S["$id.repeatable"]="$v" ;;
esac
done
S["$id.entryType"]="argument"
S["$id.name"]="$name"
S["$id.dest"]="$dest"
S["$id.required"]="$required"
S["$id.type"]="$atype"
[[ -n "$help" ]] && S["$id.help"]="$help"
[[ -n "$value" ]] && S["$id.value"]="$value"
local last=$((${#cmds[@]} - 1))
local cmdArgsKey
cmdArgsKey="$(join "${cmds[$last]}" "args")"
if [[ -z "${S[$cmdArgsKey]}" ]]; then
S["$cmdArgsKey"]="$id"
else
S["$cmdArgsKey"]+=",${id}"
fi
case "$atype" in
positional)
A["$(join "${cmds[$last]}" "pos::${pos[$last]}")"]=$id
pos[$last]=$((pos[$last] + 1))
;;
rest)
A["$(join "${cmds[$last]}" "rest")"]=$id
;;
option | flag)
for alias in "${aliases[@]}"; do
A["$(join "${cmds[$last]}" "narg::${alias}")"]=$id
done
;;
esac
;;
*)
printf 'Error: Invalid entry type: "%s"\n' "$type" >&2
return 1
;;
esac
done
}
print_result() {
local -a COMMANDS=("${!1}")
declare -n VALUES_REF="$2"
printf 'commands:'
local c
for c in "${COMMANDS[@]}"; do printf ' %s' "$c"; done
printf '\n'
printf 'values:\n'
local k v
for k in "${!VALUES_REF[@]}"; do
v=${VALUES_REF[$k]}
if [[ "$v" == *" "* ]]; then
printf ' %s="%s"\n' "$k" "$v"
else
printf ' %s=%s\n' "$k" "$v"
fi
done
}
parse_input() {
local spec_name=$1
local input=$2
tokenize "$input"
generate_schema "$spec_name" || return 1
local -a cmds=()
local -a pos=()
local tagged token tokenTag
while ((${#TOKENS[@]})); do
tagged=${TOKENS[0]}
TOKENS=("${TOKENS[@]:1}")
tokenTag=${tagged%%::*}
token=${tagged#*::}
local found=0
local level start_level entryId id
start_level=$((${#cmds[@]} ? ${#cmds[@]} - 1 : 0))
for ((level = start_level; level >= 0; level--)); do
entryId=""
case "$tokenTag" in
root)
entryId="${A["cmd::root"]}"
;;
rest)
((${#cmds[@]})) || {
entryId=""
break
}
entryId="${A["$(join "${cmds[$level]}" "rest")"]}"
;;
narg)
((${#cmds[@]})) || {
entryId=""
break
}
entryId="${A["$(join "${cmds[$level]}" "$tagged")"]}"
;;
unk)
((${#cmds[@]})) || { entryId=""; } # cannot be positional before a command
if [[ -z "$entryId" && ${#cmds[@]} -gt 0 ]]; then
id="${A["$(join "${cmds[$level]}" "cmd::${token}")"]}"
if [[ -z "$id" && $level -eq $((${#pos[@]} - 1)) ]]; then
id="${A["$(join "${cmds[$level]}" "pos::${pos[$level]}")"]}"
[[ -n "$id" ]] && pos[$level]=$((pos[$level] + 1))
fi
entryId="$id"
fi
;;
esac
[[ -z "$entryId" ]] && continue
local entryType=${S["$entryId.entryType"]}
case "$entryType" in
command)
cmds+=("$entryId")
pos+=(0)
if [[ "$tokenTag" == "root" ]]; then
S["$entryId.name"]="$token"
fi
;;
argument)
local atype=${S["$entryId.type"]}
local val=""
case "$atype" in
rest | positional) val="$token" ;;
flag) val="true" ;;
option)
if ((${#TOKENS[@]} == 0)); then
printf 'Error: No value provided for option argument: "%s"\n' "$token" >&2
return 1
fi
local nTagged=${TOKENS[0]}
local nTag=${nTagged%%::*}
local nTok=${nTagged#*::}
if [[ "$nTag" != "value" && "$nTag" != "unk" ]]; then
printf 'Error: Expected option argument, but got: "%s" (%s). Skipping...\n' "$nTok" "$nTag" >&2
else
TOKENS=("${TOKENS[@]:1}")
val="$nTok"
fi
;;
esac
S["$entryId.value"]="$val"
;;
esac
found=1
break
done
if ((found == 0)); then
printf 'Invalid argument: "%s" (%s). Skipping...\n' "$token" "$tokenTag"
fi
done
# finalize
local -a COMMANDS=()
declare -A VALUES=()
local cmdId cmdName cmdArgs argId name dest value required
for cmdId in "${cmds[@]}"; do
cmdName=${S["$cmdId.name"]}
COMMANDS+=("$cmdName")
cmdArgs=${S["$cmdId.args"]}
[[ -z "$cmdArgs" ]] && continue
IFS=',' read -r -a argIds <<<"$cmdArgs"
for argId in "${argIds[@]}"; do
name=${S["$argId.name"]}
dest=${S["$argId.dest"]}
value=${S["$argId.value"]}
required=${S["$argId.required"]}
if [[ "$required" == "true" && -z "$value" ]]; then
printf 'Error: Argument "%s" is required in command "%s"\n' "$name" "$cmdName" >&2
return 1
fi
if [[ -n "${VALUES[$dest]+x}" ]]; then
VALUES["${cmdName}_${dest}"]="$value"
else
VALUES["$dest"]="$value"
fi
done
done
print_result COMMANDS[@] VALUES
}
parse_input_wrap() {
local spec_name=$1
local input=$2
parse_input "$spec_name" "$input" || printf 'Error: Failed to parse input\n'
}
# ---------- spec and tests ----------
SPEC=(
"command;barg;Barg - Bash Argument Parser"
"argument;global;type:flag"
"command;build;Build a dev container"
"argument;from;type:option;dest:fromName;help:Name of the base container"
"argument;name;type:positional;required;help:Name of the container"
"argument;nametwo;help:Name of the container"
"argument;image,i;type:option;required;dest:imageName;help:Base image"
"argument;include,I;type:option;repeatable;dest:includePaths;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;help:Command and args to run inside the container"
"argument;name;dest:nameOpt;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"
"argument;cmd;type:rest;dest:cmdRest;help:Command and args to run inside the container"
"end"
"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"
"end"
)
INPUTS=(
"dev build -i myimage buildtwo --from base-dev --image node:18 --include src --include test -v -v -q mycontainer -- npm run start"
"dev build buildname container -i myimage --dev --name webapp externalContainer"
"dev --global build --image debian mybox container --dev --name webapp externalContainer -- echo hi"
"dev stop mycontainer --kill"
"dev stop mycontainer --kill -- not specified one"
"dev build --from alpine --image node:20 mybox -- echo hi"
"dev build --aaaaa --from alpine --image node:20 --bbbbb orb mybox ccccc -- echo hi"
)
for input in "${INPUTS[@]}"; do
printf "\n\n------------------------\n\n> %s\n" "$input"
parse_input_wrap SPEC "$input"
done