feat: bash v1
This commit is contained in:
parent
11ea01a88f
commit
4c821ae145
1
barg.js
1
barg.js
@ -306,6 +306,7 @@ const INPUTS = [
|
||||
"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",
|
||||
"dev build mybox --global -qvi=debian",
|
||||
];
|
||||
|
||||
INPUTS.forEach((input) => {
|
||||
|
||||
379
barg.sh
Normal file
379
barg.sh
Normal file
@ -0,0 +1,379 @@
|
||||
#!/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
|
||||
Loading…
Reference in New Issue
Block a user