bash v3
This commit is contained in:
parent
dacb3aeb09
commit
c46260d048
760
barg
760
barg
@ -1,386 +1,432 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
join() {
|
||||
local out="" sep=""
|
||||
for x in "$@"; do
|
||||
if [[ -n "$x" ]]; then
|
||||
out+="${sep}${x}"
|
||||
sep="."
|
||||
fi
|
||||
done
|
||||
printf '%s' "$out"
|
||||
}
|
||||
# Bash Argument Parser (BARG)
|
||||
# Requires Bash 4.3 or higher for associative arrays
|
||||
|
||||
tokenize_argv() {
|
||||
shift
|
||||
TOKENS=("root::dev")
|
||||
local -a argv=("$@")
|
||||
# Check Bash version
|
||||
if [ "${BASH_VERSINFO:-0}" -lt 4 ]; then
|
||||
echo "Error: This script requires Bash 4.0 or higher (current: $BASH_VERSION)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
while ((${#argv[@]})); do
|
||||
local token=${argv[0]}
|
||||
argv=("${argv[@]:1}")
|
||||
# Main parsing function - all logic contained here
|
||||
parse_arguments() {
|
||||
local -a spec_input=("$@")
|
||||
local input="${spec_input[-1]}"
|
||||
unset 'spec_input[-1]'
|
||||
|
||||
# Rest
|
||||
if [[ "$token" == "--" ]]; then
|
||||
TOKENS+=("rest::${argv[*]}")
|
||||
break
|
||||
fi
|
||||
# Local storage (no globals!)
|
||||
local -A schema
|
||||
local -A aliases
|
||||
local -a tokens
|
||||
|
||||
# Named Argument + Value: Option or Flag (long form)
|
||||
if [[ "$token" == --* ]]; then
|
||||
local key=${token#--}
|
||||
TOKENS+=("narg::${key%%=*}")
|
||||
[[ "$key" == *"="* ]] && TOKENS+=("value::${key#*=}")
|
||||
continue
|
||||
fi
|
||||
# Tokenize input into tagged tokens (format: tag::value)
|
||||
tokenize() {
|
||||
local input_str="$1"
|
||||
local -a args
|
||||
tokens=()
|
||||
|
||||
# Named Argument + Value: Option or Flag (short form)
|
||||
if [[ "$token" == -* ]]; then
|
||||
local key=${token#-}
|
||||
local flags=${key%%=*}
|
||||
local value=
|
||||
[[ "$key" == *"="* ]] && value=${key#*=}
|
||||
# Split input into array
|
||||
read -ra args <<<"$input_str"
|
||||
|
||||
local index flag
|
||||
for ((index = 0; index < ${#flags}; index++)); do
|
||||
flag=${flags:index:1}
|
||||
TOKENS+=("narg::${flag}")
|
||||
done
|
||||
# Root command
|
||||
local root="${args[0]}"
|
||||
tokens+=("root::${root}")
|
||||
|
||||
[[ -n "$value" ]] && TOKENS+=("value::${value}")
|
||||
continue
|
||||
fi
|
||||
local i=1
|
||||
while [[ $i -lt ${#args[@]} ]]; do
|
||||
local token="${args[$i]}"
|
||||
|
||||
# Unknown: Command, Positional or Value
|
||||
TOKENS+=("unk::${token}")
|
||||
done
|
||||
}
|
||||
|
||||
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 elements <<<"$entry"
|
||||
case "${elements[0]}" in
|
||||
command)
|
||||
local id=$_id
|
||||
((_id++))
|
||||
IFS=',' read -r -a aliases <<<"${elements[1]}"
|
||||
|
||||
# Schema
|
||||
S["$id.entryType"]="command"
|
||||
S["$id.name"]="${aliases[0]}"
|
||||
S["$id.help"]="${elements[2]}"
|
||||
S["$id.args"]=""
|
||||
|
||||
# Aliases
|
||||
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
|
||||
|
||||
# Control
|
||||
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 <<<"${elements[1]}"
|
||||
local name="${aliases[0]}"
|
||||
local dest="$name" required="false" atype="positional" value="" help=""
|
||||
local i kv k v
|
||||
for ((i = 2; i < ${#elements[@]}; i++)); do
|
||||
kv=${elements[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' "${elements[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
|
||||
# Rest arguments (everything after --)
|
||||
if [[ "$token" == "--" ]]; then
|
||||
((i++))
|
||||
local rest_args=""
|
||||
while [[ $i -lt ${#args[@]} ]]; do
|
||||
if [[ -n "$rest_args" ]]; then
|
||||
rest_args="${rest_args} ${args[$i]}"
|
||||
else
|
||||
TOKENS=("${TOKENS[@]:1}")
|
||||
val="$nTok"
|
||||
rest_args="${args[$i]}"
|
||||
fi
|
||||
((i++))
|
||||
done
|
||||
if [[ -n "$rest_args" ]]; then
|
||||
tokens+=("rest::${rest_args}")
|
||||
fi
|
||||
break
|
||||
fi
|
||||
|
||||
# Long option: --key or --key=value
|
||||
if [[ "$token" =~ ^--(.+)$ ]]; then
|
||||
local full="${BASH_REMATCH[1]}"
|
||||
if [[ "$full" =~ ^([^=]+)=(.*)$ ]]; then
|
||||
tokens+=("narg::${BASH_REMATCH[1]}")
|
||||
tokens+=("value::${BASH_REMATCH[2]}")
|
||||
else
|
||||
tokens+=("narg::${full}")
|
||||
fi
|
||||
((i++))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Short option: -k or -abc or -k=value
|
||||
if [[ "$token" =~ ^-(.+)$ ]]; then
|
||||
local full="${BASH_REMATCH[1]}"
|
||||
if [[ "$full" =~ ^([^=]+)=(.*)$ ]]; then
|
||||
local flags="${BASH_REMATCH[1]}"
|
||||
local value="${BASH_REMATCH[2]}"
|
||||
# Split flags into individual characters
|
||||
for ((j = 0; j < ${#flags}; j++)); do
|
||||
tokens+=("narg::${flags:$j:1}")
|
||||
done
|
||||
tokens+=("value::${value}")
|
||||
else
|
||||
# Split all flags
|
||||
for ((j = 0; j < ${#full}; j++)); do
|
||||
tokens+=("narg::${full:$j:1}")
|
||||
done
|
||||
fi
|
||||
((i++))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Unknown: could be command, positional, or value
|
||||
tokens+=("unk::${token}")
|
||||
((i++))
|
||||
done
|
||||
}
|
||||
|
||||
# Generate schema from spec
|
||||
generate_schema() {
|
||||
local -a spec_array=("$@")
|
||||
local entry_id=100
|
||||
local level=-1
|
||||
local -a cmd_stack=()
|
||||
local -a pos_stack=()
|
||||
|
||||
for entry in "${spec_array[@]}"; do
|
||||
# Split entry by semicolons
|
||||
IFS=';' read -ra elements <<<"$entry"
|
||||
local entry_type="${elements[0]}"
|
||||
|
||||
case "$entry_type" in
|
||||
command)
|
||||
local id="$entry_id"
|
||||
local aliases_str="${elements[1]}"
|
||||
local help_text="${elements[2]}"
|
||||
|
||||
# Schema entries
|
||||
schema["${id}.entryType"]="command"
|
||||
schema["${id}.name"]="${aliases_str%%,*}" # First alias
|
||||
schema["${id}.help"]="$help_text"
|
||||
schema["${id}.args"]=""
|
||||
|
||||
# Register aliases
|
||||
if [[ ${#cmd_stack[@]} -eq 0 ]]; then
|
||||
aliases["cmd::root"]="$id"
|
||||
else
|
||||
IFS=',' read -ra alias_list <<<"$aliases_str"
|
||||
for alias in "${alias_list[@]}"; do
|
||||
local parent="${cmd_stack[-1]}"
|
||||
aliases["${parent}.cmd::${alias}"]="$id"
|
||||
done
|
||||
fi
|
||||
|
||||
# Push to stacks
|
||||
((level++))
|
||||
cmd_stack+=("$id")
|
||||
pos_stack+=(0)
|
||||
((entry_id++))
|
||||
;;
|
||||
|
||||
end)
|
||||
((level--))
|
||||
unset 'cmd_stack[-1]'
|
||||
unset 'pos_stack[-1]'
|
||||
;;
|
||||
|
||||
argument)
|
||||
local id="$entry_id"
|
||||
local aliases_str="${elements[1]}"
|
||||
local arg_name="${aliases_str%%,*}"
|
||||
|
||||
# Default attributes
|
||||
schema["${id}.entryType"]="argument"
|
||||
schema["${id}.name"]="$arg_name"
|
||||
schema["${id}.dest"]="$arg_name"
|
||||
schema["${id}.required"]="false"
|
||||
schema["${id}.type"]="positional"
|
||||
|
||||
# Parse additional attributes
|
||||
for ((i = 2; i < ${#elements[@]}; i++)); do
|
||||
local element="${elements[$i]}"
|
||||
if [[ "$element" =~ ^([^:]+):(.*)$ ]]; then
|
||||
local attr_key="${BASH_REMATCH[1]}"
|
||||
local attr_value="${BASH_REMATCH[2]}"
|
||||
if [[ "$attr_key" == "default" ]]; then
|
||||
schema["${id}.value"]="$attr_value"
|
||||
else
|
||||
schema["${id}.${attr_key}"]="$attr_value"
|
||||
fi
|
||||
elif [[ "$element" =~ ^([^:]+)$ ]]; then
|
||||
local attr_key="${BASH_REMATCH[1]}"
|
||||
schema["${id}.${attr_key}"]="true"
|
||||
fi
|
||||
done
|
||||
|
||||
# Add to parent command's args list
|
||||
local parent="${cmd_stack[-1]}"
|
||||
local cmd_args="${schema["${parent}.args"]}"
|
||||
if [[ -n "$cmd_args" ]]; then
|
||||
schema["${parent}.args"]="${cmd_args},${id}"
|
||||
else
|
||||
schema["${parent}.args"]="$id"
|
||||
fi
|
||||
|
||||
# Register aliases based on type
|
||||
local arg_type="${schema["${id}.type"]}"
|
||||
case "$arg_type" in
|
||||
positional)
|
||||
local pos="${pos_stack[-1]}"
|
||||
aliases["${parent}.pos::${pos}"]="$id"
|
||||
pos_stack[-1]=$((pos + 1))
|
||||
;;
|
||||
rest)
|
||||
aliases["${parent}.rest"]="$id"
|
||||
;;
|
||||
option | flag)
|
||||
IFS=',' read -ra alias_list <<<"$aliases_str"
|
||||
for alias in "${alias_list[@]}"; do
|
||||
aliases["${parent}.narg::${alias}"]="$id"
|
||||
done
|
||||
;;
|
||||
esac
|
||||
|
||||
((entry_id++))
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Error: Invalid entry type: $entry_type" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Parse tokens based on schema
|
||||
process_tokens() {
|
||||
local -a cmd_stack=()
|
||||
local -a pos_stack=()
|
||||
local token_idx=0
|
||||
|
||||
# Process each token
|
||||
while [[ $token_idx -lt ${#tokens[@]} ]]; do
|
||||
local tagged_token="${tokens[$token_idx]}"
|
||||
local token_tag="${tagged_token%%::*}"
|
||||
local token="${tagged_token#*::}"
|
||||
|
||||
local found=false
|
||||
local level
|
||||
|
||||
# Special handling for root - must be processed first
|
||||
if [[ "$token_tag" == "root" ]]; then
|
||||
local root_id="${aliases["cmd::root"]}"
|
||||
if [[ -n "$root_id" ]]; then
|
||||
schema["${root_id}.name"]="$token"
|
||||
cmd_stack+=("$root_id")
|
||||
pos_stack+=(0)
|
||||
found=true
|
||||
fi
|
||||
((token_idx++))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Bubble up: try matching from current level to root
|
||||
for ((level = ${#cmd_stack[@]} - 1; level >= 0; level--)); do
|
||||
local entry_id=""
|
||||
local current_cmd="${cmd_stack[$level]}"
|
||||
|
||||
case "$token_tag" in
|
||||
rest)
|
||||
entry_id="${aliases["${current_cmd}.rest"]}"
|
||||
;;
|
||||
narg)
|
||||
entry_id="${aliases["${current_cmd}.narg::${token}"]}"
|
||||
;;
|
||||
unk)
|
||||
# Try as command
|
||||
entry_id="${aliases["${current_cmd}.cmd::${token}"]}"
|
||||
|
||||
# Try as positional (only on last level)
|
||||
if [[ -z "$entry_id" && $level -eq $((${#cmd_stack[@]} - 1)) ]]; then
|
||||
local pos="${pos_stack[$level]}"
|
||||
entry_id="${aliases["${current_cmd}.pos::${pos}"]}"
|
||||
if [[ -n "$entry_id" ]]; then
|
||||
pos_stack[$level]=$((pos + 1))
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
S["$entryId.value"]="$val"
|
||||
|
||||
# No match at this level, try parent
|
||||
if [[ -z "$entry_id" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Found match - process it
|
||||
local entry_type="${schema["${entry_id}.entryType"]}"
|
||||
|
||||
case "$entry_type" in
|
||||
command)
|
||||
cmd_stack+=("$entry_id")
|
||||
pos_stack+=(0)
|
||||
;;
|
||||
|
||||
argument)
|
||||
local arg_type="${schema["${entry_id}.type"]}"
|
||||
local value=""
|
||||
|
||||
case "$arg_type" in
|
||||
rest | positional)
|
||||
value="$token"
|
||||
;;
|
||||
flag)
|
||||
value="true"
|
||||
;;
|
||||
option)
|
||||
# Next token should be the value
|
||||
local next_idx=$((token_idx + 1))
|
||||
|
||||
if [[ $next_idx -ge ${#tokens[@]} ]]; then
|
||||
echo "Error: No value provided for option: $token" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local next_tagged="${tokens[$next_idx]}"
|
||||
local next_tag="${next_tagged%%::*}"
|
||||
local next_token="${next_tagged#*::}"
|
||||
|
||||
if [[ "$next_tag" != "value" && "$next_tag" != "unk" ]]; then
|
||||
echo "Error: Expected value for option, got: $next_token ($next_tag)" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
value="$next_token"
|
||||
# Skip next token
|
||||
((token_idx++))
|
||||
;;
|
||||
esac
|
||||
|
||||
schema["${entry_id}.value"]="$value"
|
||||
;;
|
||||
esac
|
||||
|
||||
found=true
|
||||
break
|
||||
done
|
||||
|
||||
if [[ "$found" == "false" ]]; then
|
||||
echo "Warning: Invalid argument: \"$token\" ($token_tag). Skipping..." >&2
|
||||
fi
|
||||
|
||||
found=1
|
||||
break
|
||||
((token_idx++))
|
||||
done
|
||||
|
||||
((found == 0)) && printf 'Invalid argument: "%s" (%s). Skipping...\n' "${tagged#*::}" "$tokenTag"
|
||||
done
|
||||
# Extract and output results
|
||||
local -a commands=()
|
||||
local -A values
|
||||
|
||||
# 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
|
||||
for cmd_id in "${cmd_stack[@]}"; do
|
||||
local cmd_name="${schema["${cmd_id}.name"]}"
|
||||
local cmd_args="${schema["${cmd_id}.args"]}"
|
||||
commands+=("$cmd_name")
|
||||
|
||||
if [[ -z "$cmd_args" ]]; then
|
||||
continue
|
||||
fi
|
||||
if [[ -n "${VALUES[$dest]+x}" ]]; then VALUES["${cmdName}_${dest}"]="$value"; else VALUES["$dest"]="$value"; fi
|
||||
|
||||
IFS=',' read -ra arg_ids <<<"$cmd_args"
|
||||
for arg_id in "${arg_ids[@]}"; do
|
||||
local name="${schema["${arg_id}.name"]}"
|
||||
local dest="${schema["${arg_id}.dest"]}"
|
||||
local value="${schema["${arg_id}.value"]}"
|
||||
local required="${schema["${arg_id}.required"]}"
|
||||
|
||||
# Check required
|
||||
if [[ -z "$value" && "$required" == "true" ]]; then
|
||||
echo "Error: Argument \"$name\" is required in command \"$cmd_name\"" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Handle duplicate destinations
|
||||
if [[ -n "${values[$dest]}" ]]; then
|
||||
values["${cmd_name}_${dest}"]="$value"
|
||||
else
|
||||
values["$dest"]="$value"
|
||||
fi
|
||||
done
|
||||
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
|
||||
# Output results
|
||||
echo "Commands: ${commands[*]}"
|
||||
echo "Values:"
|
||||
for key in "${!values[@]}"; do
|
||||
echo " ${key}=${values[$key]}"
|
||||
done
|
||||
}
|
||||
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
|
||||
|
||||
# Execute the pipeline
|
||||
tokenize "$input"
|
||||
generate_schema "${spec_input[@]}" || return 1
|
||||
process_tokens
|
||||
}
|
||||
|
||||
# --- public API --------------------------------------------------------------
|
||||
usage() {
|
||||
local rootId=${A["cmd::root"]}
|
||||
_barg_usage_for "$rootId" "$rootId"
|
||||
# Wrapper to handle errors gracefully
|
||||
parse_input_wrap() {
|
||||
parse_arguments "$@" 2>&1
|
||||
}
|
||||
|
||||
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
|
||||
# Example spec
|
||||
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"
|
||||
)
|
||||
|
||||
# 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
|
||||
# Test inputs
|
||||
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"
|
||||
)
|
||||
|
||||
# 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
|
||||
}
|
||||
# Run tests
|
||||
for input in "${INPUTS[@]}"; do
|
||||
echo ""
|
||||
echo "------------------------"
|
||||
echo "> $input"
|
||||
parse_input_wrap "${SPEC[@]}" "$input"
|
||||
done
|
||||
|
||||
Loading…
Reference in New Issue
Block a user