bash v2
This commit is contained in:
parent
4c821ae145
commit
6726f04f77
372
barg
Normal file
372
barg
Normal file
@ -0,0 +1,372 @@
|
|||||||
|
# barg - Bash argument parser + tiny CLI framework
|
||||||
|
# Bash 5+
|
||||||
|
|
||||||
|
# --- helpers -----------------------------------------------------------------
|
||||||
|
join() {
|
||||||
|
local out="" sep=""
|
||||||
|
for x in "$@"; do [[ -n "$x" ]] && {
|
||||||
|
out+="${sep}${x}"
|
||||||
|
sep="."
|
||||||
|
}; done
|
||||||
|
printf '%s' "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- tokenizer ---------------------------------------------------------------
|
||||||
|
tokenize_argv() {
|
||||||
|
local prog=$1
|
||||||
|
shift
|
||||||
|
TOKENS=()
|
||||||
|
TOKENS+=("root::${prog}")
|
||||||
|
local -a args=("$@")
|
||||||
|
while ((${#args[@]})); do
|
||||||
|
local t=${args[0]}
|
||||||
|
args=("${args[@]:1}")
|
||||||
|
if [[ "$t" == "--" ]]; then
|
||||||
|
TOKENS+=("rest::${args[*]}")
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [[ "$t" == --* ]]; then
|
||||||
|
local kv=${t#--}
|
||||||
|
local key=${kv%%=*}
|
||||||
|
TOKENS+=("narg::${key}")
|
||||||
|
[[ "$kv" == *"="* ]] && TOKENS+=("value::${kv#*=}")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "$t" == -* ]]; then
|
||||||
|
local sv=${t#-}
|
||||||
|
local flags=${sv%%=*}
|
||||||
|
local val=
|
||||||
|
[[ "$sv" == *"="* ]] && val=${sv#*=}
|
||||||
|
local i ch
|
||||||
|
for ((i = 0; i < ${#flags}; i++)); do
|
||||||
|
ch=${flags:i:1}
|
||||||
|
TOKENS+=("narg::${ch}")
|
||||||
|
done
|
||||||
|
[[ -n "$val" ]] && TOKENS+=("value::${val}")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
TOKENS+=("unk::${t}")
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- schema + parse ----------------------------------------------------------
|
||||||
|
declare -A S A
|
||||||
|
|
||||||
|
_generate_schema() {
|
||||||
|
S=()
|
||||||
|
A=()
|
||||||
|
local spec_name=$1
|
||||||
|
local -n SPEC_REF="$spec_name"
|
||||||
|
local _id=100
|
||||||
|
local -a pos=() cmds=()
|
||||||
|
local root_added_help=0
|
||||||
|
|
||||||
|
for entry in "${SPEC_REF[@]}"; do
|
||||||
|
IFS=';' read -r -a parts <<<"$entry"
|
||||||
|
case "${parts[0]}" in
|
||||||
|
command)
|
||||||
|
local id=$_id
|
||||||
|
((_id++))
|
||||||
|
IFS=',' read -r -a aliases <<<"${parts[1]}"
|
||||||
|
S["$id.entryType"]="command"
|
||||||
|
S["$id.name"]="${aliases[0]}"
|
||||||
|
S["$id.help"]="${parts[2]}"
|
||||||
|
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
|
||||||
|
|
||||||
|
cmds+=("$id")
|
||||||
|
pos+=(0)
|
||||||
|
|
||||||
|
# inject -h/--help only on root
|
||||||
|
if ((root_added_help == 0)); then
|
||||||
|
root_added_help=1
|
||||||
|
local hid=$_id
|
||||||
|
((_id++))
|
||||||
|
S["$hid.entryType"]="argument"
|
||||||
|
S["$hid.name"]="help"
|
||||||
|
S["$hid.dest"]="help"
|
||||||
|
S["$hid.required"]="false"
|
||||||
|
S["$hid.type"]="flag"
|
||||||
|
S["$hid.help"]="Show help"
|
||||||
|
local cmdArgsKey
|
||||||
|
cmdArgsKey="$(join "${cmds[0]}" "args")"
|
||||||
|
S["$cmdArgsKey"]="${S[$cmdArgsKey]:+${S[$cmdArgsKey]},}$hid"
|
||||||
|
A["$(join "${cmds[0]}" "narg::help")"]=$hid
|
||||||
|
A["$(join "${cmds[0]}" "narg::h")"]=$hid
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
end)
|
||||||
|
if ((${#cmds[@]})); then cmds=("${cmds[@]:0:${#cmds[@]}-1}"); fi
|
||||||
|
if ((${#pos[@]})); then pos=("${pos[@]:0:${#pos[@]}-1}"); fi
|
||||||
|
;;
|
||||||
|
argument)
|
||||||
|
local id=$_id
|
||||||
|
((_id++))
|
||||||
|
IFS=',' read -r -a aliases <<<"${parts[1]}"
|
||||||
|
local name="${aliases[0]}"
|
||||||
|
local dest="$name" required="false" atype="positional" value="" 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" ;; # not implemented
|
||||||
|
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")"
|
||||||
|
S["$cmdArgsKey"]="${S[$cmdArgsKey]:+${S[$cmdArgsKey]},}$id"
|
||||||
|
|
||||||
|
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' "${parts[0]}" >&2
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
_parse_tokens() {
|
||||||
|
# outputs: COMMANDS[], VALUES[]
|
||||||
|
local -a cmds=() pos=()
|
||||||
|
COMMANDS=()
|
||||||
|
declare -gA VALUES=()
|
||||||
|
|
||||||
|
while ((${#TOKENS[@]})); do
|
||||||
|
local tagged=${TOKENS[0]}
|
||||||
|
TOKENS=("${TOKENS[@]:1}")
|
||||||
|
local tokenTag=${tagged%%::*}
|
||||||
|
local token=${tagged#*::}
|
||||||
|
local found=0
|
||||||
|
|
||||||
|
local start_level
|
||||||
|
if ((${#cmds[@]})); then start_level=$((${#cmds[@]} - 1)); else start_level=0; fi
|
||||||
|
|
||||||
|
local level
|
||||||
|
for ((level = start_level; level >= 0; level--)); do
|
||||||
|
local entryId=""
|
||||||
|
case "$tokenTag" in
|
||||||
|
root) entryId="${A["cmd::root"]}" ;;
|
||||||
|
rest) ((${#cmds[@]})) && entryId="${A["$(join "${cmds[$level]}" "rest")"]}" ;;
|
||||||
|
narg) ((${#cmds[@]})) && entryId="${A["$(join "${cmds[$level]}" "$tagged")"]}" ;;
|
||||||
|
unk)
|
||||||
|
if ((${#cmds[@]})); then
|
||||||
|
local 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"]}
|
||||||
|
if [[ "$entryType" == "command" ]]; then
|
||||||
|
cmds+=("$entryId")
|
||||||
|
pos+=(0)
|
||||||
|
[[ "$tokenTag" == "root" ]] && S["$entryId.name"]="$token"
|
||||||
|
else
|
||||||
|
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 for option "%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 value, got "%s" (%s)\n' "$nTok" "$nTag" >&2
|
||||||
|
else
|
||||||
|
TOKENS=("${TOKENS[@]:1}")
|
||||||
|
val="$nTok"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
S["$entryId.value"]="$val"
|
||||||
|
fi
|
||||||
|
|
||||||
|
found=1
|
||||||
|
break
|
||||||
|
done
|
||||||
|
|
||||||
|
((found == 0)) && printf 'Invalid argument: "%s" (%s). Skipping...\n' "${tagged#*::}" "$tokenTag"
|
||||||
|
done
|
||||||
|
|
||||||
|
# finalize
|
||||||
|
local cmdId
|
||||||
|
for cmdId in "${cmds[@]}"; do
|
||||||
|
local cmdName=${S["$cmdId.name"]}
|
||||||
|
COMMANDS+=("$cmdName")
|
||||||
|
local cmdArgs=${S["$cmdId.args"]}
|
||||||
|
[[ -z "$cmdArgs" ]] && continue
|
||||||
|
IFS=',' read -r -a argIds <<<"$cmdArgs"
|
||||||
|
local argId
|
||||||
|
for argId in "${argIds[@]}"; do
|
||||||
|
local name=${S["$argId.name"]}
|
||||||
|
local dest=${S["$argId.dest"]}
|
||||||
|
local value=${S["$argId.value"]}
|
||||||
|
local required=${S["$argId.required"]}
|
||||||
|
if [[ "$required" == "true" && -z "$value" ]]; then
|
||||||
|
printf 'Error: Argument "%s" is required for "%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
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- usage printer -----------------------------------------------------------
|
||||||
|
_barg_aliases_for() { # <cmdId> <argId> -> prints "-x --xyz " list
|
||||||
|
local cmdId=$1 argId=$2 k v a out=()
|
||||||
|
for k in "${!A[@]}"; do
|
||||||
|
[[ $k == "$cmdId.narg::"* ]] || continue
|
||||||
|
v=${A[$k]} # ← fixed bracket
|
||||||
|
[[ "$v" != "$argId" ]] && continue
|
||||||
|
a=${k##*::}
|
||||||
|
if ((${#a} > 1)); then out+=("--$a"); else out+=("-$a"); fi
|
||||||
|
done
|
||||||
|
printf '%s ' "${out[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
_barg_children_of() { # <cmdId> -> "id<TAB>name<TAB>help"
|
||||||
|
local cmdId=$1 k childId
|
||||||
|
declare -A seen=()
|
||||||
|
for k in "${!A[@]}"; do
|
||||||
|
[[ $k == "$cmdId.cmd::"* ]] || continue
|
||||||
|
childId=${A[$k]}
|
||||||
|
[[ -n "${seen[$childId]}" ]] && continue
|
||||||
|
seen[$childId]=1
|
||||||
|
printf '%s\t%s\t%s\n' "$childId" "${S["$childId.name"]}" "${S["$childId.help"]}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
_barg_usage_for() { # <rootId> [<cmdId>]
|
||||||
|
local rootId=$1 showId=${2:-$1}
|
||||||
|
local prog=${S["$rootId.name"]}
|
||||||
|
printf 'Usage: %s <command> [args]\n' "$prog"
|
||||||
|
printf '\nCommands:\n'
|
||||||
|
local name help
|
||||||
|
while IFS=$'\t' read -r _ name help; do
|
||||||
|
printf ' %-12s %s\n' "$name" "$help"
|
||||||
|
done < <(_barg_children_of "$rootId")
|
||||||
|
|
||||||
|
printf '\nArgs for "%s":\n' "${S["$showId.name"]}"
|
||||||
|
local args=${S["$showId.args"]}
|
||||||
|
[[ -z "$args" ]] && {
|
||||||
|
printf ' (none)\n'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
IFS=',' read -r -a argIds <<<"$args"
|
||||||
|
local aid atype req nm dest desc aliases
|
||||||
|
for aid in "${argIds[@]}"; do
|
||||||
|
atype=${S["$aid.type"]}
|
||||||
|
req=${S["$aid.required"]}
|
||||||
|
nm=${S["$aid.name"]}
|
||||||
|
dest=${S["$aid.dest"]}
|
||||||
|
desc=${S["$aid.help"]}
|
||||||
|
aliases="$(_barg_aliases_for "$showId" "$aid")"
|
||||||
|
case "$atype" in
|
||||||
|
option) printf ' %-16s -- %s (dest=%s)%s\n' "$aliases" "${desc:-option}" "$dest" "$([[ $req == true ]] && echo ' [required]')" ;;
|
||||||
|
flag) printf ' %-16s -- %s (flag)\n' "$aliases" "${desc:-flag}" ;;
|
||||||
|
positional) printf ' %-16s -- %s (positional dest=%s)%s\n' "$nm" "${desc:-positional}" "$dest" "$([[ $req == true ]] && echo ' [required]')" ;;
|
||||||
|
rest) printf ' %-16s -- %s (-- terminator)\n' "$nm" "${desc:-rest}" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- public API --------------------------------------------------------------
|
||||||
|
usage() {
|
||||||
|
local rootId=${A["cmd::root"]}
|
||||||
|
_barg_usage_for "$rootId" "$rootId"
|
||||||
|
}
|
||||||
|
|
||||||
|
barg::dispatch() {
|
||||||
|
local spec_name=$1
|
||||||
|
shift
|
||||||
|
local prog=${BARG_PROG:-$(basename "$0")}
|
||||||
|
tokenize_argv "$prog" "$@"
|
||||||
|
_generate_schema "$spec_name" || return 1
|
||||||
|
_parse_tokens || return 1
|
||||||
|
|
||||||
|
# Help or no subcommand -> usage of deepest reachable context
|
||||||
|
if [[ "${VALUES[help]}" == "true" || ${#COMMANDS[@]} -le 1 ]]; then
|
||||||
|
local rootId=${A["cmd::root"]}
|
||||||
|
local showId=$rootId parent=$rootId i
|
||||||
|
for ((i = 1; i < ${#COMMANDS[@]}; i++)); do
|
||||||
|
local name=${COMMANDS[i]}
|
||||||
|
local key
|
||||||
|
key="$(join "$parent" "cmd::${name}")"
|
||||||
|
local child=${A[$key]}
|
||||||
|
[[ -n "$child" ]] && {
|
||||||
|
showId=$child
|
||||||
|
parent=$child
|
||||||
|
}
|
||||||
|
done
|
||||||
|
_barg_usage_for "$rootId" "$showId"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Export parsed values as plain vars
|
||||||
|
local k
|
||||||
|
for k in "${!VALUES[@]}"; do
|
||||||
|
local var=${k//[^a-zA-Z0-9_]/_}
|
||||||
|
printf -v "$var" '%s' "${VALUES[$k]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Call deepest command function
|
||||||
|
local last=$((${#COMMANDS[@]} - 1))
|
||||||
|
local target=${COMMANDS[$last]}
|
||||||
|
local fn="cmd_${target}"
|
||||||
|
if [[ $(type -t "$fn") == "function" ]]; then
|
||||||
|
"$fn"
|
||||||
|
else
|
||||||
|
printf 'Error: command handler not found: %s\n' "$fn" >&2
|
||||||
|
local rootId=${A["cmd::root"]}
|
||||||
|
_barg_usage_for "$rootId" "$rootId"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
379
barg.sh
379
barg.sh
@ -1,379 +0,0 @@
|
|||||||
#!/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
|
|
||||||
42
sample.sh
Executable file
42
sample.sh
Executable file
@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# sample.sh
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
|
||||||
|
source "$(dirname "$0")/barg" || {
|
||||||
|
echo "barg not found" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
SPEC=(
|
||||||
|
"command;dev;Dev tool"
|
||||||
|
"argument;global;type:flag;help:Global toggle"
|
||||||
|
"command;build;Build a dev container"
|
||||||
|
"argument;from;type:option;dest:fromName;help:Base container"
|
||||||
|
"argument;name;type:positional;required;help:Container name"
|
||||||
|
"argument;image,i;type:option;required;dest:imageName;help:Base image"
|
||||||
|
"argument;verbose,v;type:flag;default:false;help:Verbose output"
|
||||||
|
"argument;cmd;type:rest;help:Command to run"
|
||||||
|
"end"
|
||||||
|
"command;stop;Stop a dev container"
|
||||||
|
"argument;name;type:positional;required;help:Container name"
|
||||||
|
"argument;kill,k;type:flag;default:false;help:Force kill"
|
||||||
|
"end"
|
||||||
|
"end"
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd_build() {
|
||||||
|
echo "cmd_build:"
|
||||||
|
echo " fromName = ${fromName}"
|
||||||
|
echo " name = ${name}"
|
||||||
|
echo " imageName = ${imageName}"
|
||||||
|
echo " verbose = ${verbose}"
|
||||||
|
echo " cmd = ${cmd}"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_stop() {
|
||||||
|
echo "cmd_stop:"
|
||||||
|
echo " name = ${name}"
|
||||||
|
echo " kill = ${kill}"
|
||||||
|
}
|
||||||
|
|
||||||
|
barg::dispatch SPEC "$@"
|
||||||
Loading…
Reference in New Issue
Block a user