bash v5: working version
This commit is contained in:
parent
dac3ac8662
commit
4b222622fb
15
__test.js
15
__test.js
@ -1,15 +0,0 @@
|
||||
const inputs = [
|
||||
"dev build --from mybox2 -abc -q -v --image tm0:node -v -- bla1 bla2",
|
||||
"dev build --from mybox2 -abc valuec -q -v --image tm0:node mybox -v -- bla1 bla2",
|
||||
];
|
||||
|
||||
let level = 1;
|
||||
const commands = ["root", "build"];
|
||||
function ref(...rest) {
|
||||
return commands
|
||||
.slice(0, level + 1)
|
||||
.concat(rest)
|
||||
.join(".");
|
||||
}
|
||||
|
||||
console.log(ref("from"));
|
||||
435
barg
435
barg
@ -1,435 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Bash Argument Parser (BARG)
|
||||
# Requires Bash 4.3 or higher for associative arrays
|
||||
|
||||
# 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
|
||||
|
||||
# Main parsing function - all logic contained here
|
||||
parse_arguments() {
|
||||
local -a spec_input=("$@")
|
||||
local input="${spec_input[-1]}"
|
||||
unset 'spec_input[-1]'
|
||||
|
||||
# Local storage (no globals!)
|
||||
local -A schema
|
||||
local -A aliases
|
||||
local -a tokens
|
||||
|
||||
# Tokenize input into tagged tokens (format: tag::value)
|
||||
tokenize() {
|
||||
local input_str="$1"
|
||||
local -a args
|
||||
tokens=()
|
||||
|
||||
# Split input into array
|
||||
read -ra args <<<"$input_str"
|
||||
|
||||
# Root command
|
||||
local root="${args[0]}"
|
||||
tokens+=("root::${root}")
|
||||
|
||||
local i=1
|
||||
while [[ $i -lt ${#args[@]} ]]; do
|
||||
local token="${args[$i]}"
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
# Bootstrap: Pre-populate with root command
|
||||
local root_id="${aliases["cmd::root"]}"
|
||||
cmd_stack+=("$root_id")
|
||||
pos_stack+=(0)
|
||||
|
||||
# Process each token - all follow same bubble-up pattern
|
||||
while [[ $token_idx -lt ${#tokens[@]} ]]; do
|
||||
local tagged_token="${tokens[$token_idx]}"
|
||||
local token_tag="${tagged_token%%::*}"
|
||||
local token="${tagged_token#*::}"
|
||||
|
||||
# Special case: root token just sets the name, skip matching
|
||||
if [[ "$token_tag" == "root" ]]; then
|
||||
schema["${root_id}.name"]="$token"
|
||||
((token_idx++))
|
||||
continue
|
||||
fi
|
||||
|
||||
local found=false
|
||||
local level
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
((token_idx++))
|
||||
done
|
||||
|
||||
# Extract and output results
|
||||
local -a commands=()
|
||||
local -A values
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
# Only collect if value was actually set
|
||||
if [[ -n "$value" ]]; then
|
||||
# Handle duplicate destinations
|
||||
if [[ -n "${values[$dest]}" ]]; then
|
||||
values["${cmd_name}_${dest}"]="$value"
|
||||
else
|
||||
values["$dest"]="$value"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# Output results
|
||||
echo "Commands: ${commands[*]}"
|
||||
echo "Values:"
|
||||
for key in "${!values[@]}"; do
|
||||
echo " ${key}=${values[$key]}"
|
||||
done
|
||||
}
|
||||
|
||||
# Execute the pipeline
|
||||
tokenize "$input"
|
||||
generate_schema "${spec_input[@]}" || return 1
|
||||
process_tokens
|
||||
}
|
||||
|
||||
# Wrapper to handle errors gracefully
|
||||
parse_input_wrap() {
|
||||
parse_arguments "$@" 2>&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"
|
||||
)
|
||||
|
||||
# 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"
|
||||
"dev build --global --aaaaa --from alpine -qvvi node:20 --bbbbb orb mybox ccccc "
|
||||
)
|
||||
|
||||
# Run tests
|
||||
for input in "${INPUTS[@]}"; do
|
||||
echo ""
|
||||
echo "------------------------"
|
||||
echo "> $input"
|
||||
parse_input_wrap "${SPEC[@]}" "$input"
|
||||
done
|
||||
213
barg.sh
213
barg.sh
@ -1,13 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# BARG - Bash Argument Parser & CLI Framework
|
||||
|
||||
# Requires Bash 4.3+
|
||||
if [ "${BASH_VERSINFO:-0}" -lt 4 ]; then
|
||||
echo "Error: Requires Bash 4.0 or higher (current: $BASH_VERSION)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- globals ---
|
||||
declare -A BARG_PARSED_VALUES
|
||||
declare -a BARG_PARSED_COMMANDS
|
||||
declare -A BARG_SCHEMA
|
||||
declare -A BARG_ALIASES
|
||||
BARG_HELP_ONLY=false
|
||||
BARG_GENERATE_VARS=false # new flag to control var file generation
|
||||
|
||||
# --- parse_arguments: take spec... '--' args... ; tokenize array; i=0 ---
|
||||
# --- Auto-source generated ShellCheck stub vars ---
|
||||
_barg_self="${BASH_SOURCE[0]}"
|
||||
_barg_dir="$(cd "$(dirname "$_barg_self")" && pwd)"
|
||||
_barg_stub="${_barg_dir}/$(basename "${_barg_self%.*}").vars.generated.sh"
|
||||
if [[ -f "$_barg_stub" ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "$_barg_stub"
|
||||
fi
|
||||
unset _barg_self _barg_dir _barg_stub
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Parse CLI args according to spec
|
||||
# --------------------------------------------------------------------
|
||||
parse_arguments() {
|
||||
# split "$@" into spec_input and args by a literal "--" separator
|
||||
local -a spec_input=()
|
||||
local -a args=()
|
||||
local seen_sep=false
|
||||
@ -23,12 +44,11 @@ parse_arguments() {
|
||||
fi
|
||||
done
|
||||
|
||||
# Local storage
|
||||
local -A schema=()
|
||||
local -A aliases=()
|
||||
local -a tokens=()
|
||||
|
||||
# Tokenize input into tagged tokens (format: tag::value)
|
||||
# ---------------- Tokenizer ----------------
|
||||
tokenize() {
|
||||
local -a in=("$@")
|
||||
tokens=()
|
||||
@ -38,7 +58,6 @@ parse_arguments() {
|
||||
while [[ $i -lt ${#in[@]} ]]; do
|
||||
local token="${in[$i]}"
|
||||
|
||||
# Rest arguments after "--"
|
||||
if [[ "$token" == "--" ]]; then
|
||||
((i++))
|
||||
local rest_args=()
|
||||
@ -46,13 +65,10 @@ parse_arguments() {
|
||||
rest_args+=("${in[$i]}")
|
||||
((i++))
|
||||
done
|
||||
if [[ ${#rest_args[@]} -gt 0 ]]; then
|
||||
tokens+=("rest::${rest_args[*]}")
|
||||
fi
|
||||
[[ ${#rest_args[@]} -gt 0 ]] && tokens+=("rest::${rest_args[*]}")
|
||||
break
|
||||
fi
|
||||
|
||||
# Long option: --key or --key=value
|
||||
if [[ "$token" =~ ^--(.+)$ ]]; then
|
||||
local full="${BASH_REMATCH[1]}"
|
||||
if [[ "$full" =~ ^([^=]+)=(.*)$ ]]; then
|
||||
@ -65,7 +81,6 @@ parse_arguments() {
|
||||
continue
|
||||
fi
|
||||
|
||||
# Short option(s): -k -abc or -k=value
|
||||
if [[ "$token" =~ ^-(.+)$ ]]; then
|
||||
local full="${BASH_REMATCH[1]}"
|
||||
if [[ "$full" =~ ^([^=]+)=(.*)$ ]]; then
|
||||
@ -84,13 +99,12 @@ parse_arguments() {
|
||||
continue
|
||||
fi
|
||||
|
||||
# Unknown: could be command, positional, or value
|
||||
tokens+=("unk::${token}")
|
||||
((i++))
|
||||
done
|
||||
}
|
||||
|
||||
# Generate schema from spec (unchanged body except local arrays initialized)
|
||||
# ---------------- Schema builder ----------------
|
||||
generate_schema() {
|
||||
local -a spec_array=("$@")
|
||||
local entry_id=100
|
||||
@ -107,7 +121,6 @@ parse_arguments() {
|
||||
local id="$entry_id"
|
||||
local aliases_str="${elements[1]}"
|
||||
local help_text="${elements[2]}"
|
||||
|
||||
schema["${id}.entryType"]="command"
|
||||
schema["${id}.name"]="${aliases_str%%,*}"
|
||||
schema["${id}.help"]="$help_text"
|
||||
@ -128,18 +141,15 @@ parse_arguments() {
|
||||
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%%,*}"
|
||||
|
||||
schema["${id}.entryType"]="argument"
|
||||
schema["${id}.name"]="$arg_name"
|
||||
schema["${id}.dest"]="$arg_name"
|
||||
@ -163,11 +173,7 @@ parse_arguments() {
|
||||
|
||||
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
|
||||
[[ -n "$cmd_args" ]] && schema["${parent}.args"]="${cmd_args},${id}" || schema["${parent}.args"]="$id"
|
||||
|
||||
local arg_type="${schema["${id}.type"]}"
|
||||
case "$arg_type" in
|
||||
@ -189,7 +195,6 @@ parse_arguments() {
|
||||
|
||||
((entry_id++))
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Error: Invalid entry type: $entry_type" >&2
|
||||
return 1
|
||||
@ -198,17 +203,15 @@ parse_arguments() {
|
||||
done
|
||||
}
|
||||
|
||||
# Parse tokens based on schema
|
||||
# ---------------- Token processor ----------------
|
||||
process_tokens() {
|
||||
local -a cmd_stack=()
|
||||
local -a pos_stack=()
|
||||
local token_idx=0
|
||||
|
||||
local root_id="${aliases["cmd::root"]}"
|
||||
cmd_stack+=("$root_id")
|
||||
pos_stack+=(0)
|
||||
|
||||
# help-only detection at token level
|
||||
BARG_HELP_ONLY=false
|
||||
local help_present=false other_narg=false
|
||||
for t in "${tokens[@]}"; do
|
||||
@ -237,12 +240,10 @@ parse_arguments() {
|
||||
continue
|
||||
fi
|
||||
|
||||
local found=false
|
||||
local level
|
||||
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}"]}" ;;
|
||||
@ -251,17 +252,12 @@ parse_arguments() {
|
||||
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
|
||||
[[ -n "$entry_id" ]] && ((pos_stack[level] = pos + 1))
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -z "$entry_id" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
[[ -z "$entry_id" ]] && continue
|
||||
local entry_type="${schema["${entry_id}.entryType"]}"
|
||||
|
||||
case "$entry_type" in
|
||||
@ -277,17 +273,17 @@ parse_arguments() {
|
||||
flag) value="true" ;;
|
||||
option)
|
||||
local next_idx=$((token_idx + 1))
|
||||
if [[ $next_idx -ge ${#tokens[@]} ]]; then
|
||||
echo "Error: No value provided for option: $token" >&2
|
||||
((next_idx >= ${#tokens[@]})) && {
|
||||
echo "Error: No value 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
|
||||
[[ "$next_tag" =~ ^(value|unk)$ ]] || {
|
||||
echo "Error: Expected value for option $token" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
value="$next_token"
|
||||
((token_idx++))
|
||||
;;
|
||||
@ -296,46 +292,34 @@ parse_arguments() {
|
||||
;;
|
||||
esac
|
||||
|
||||
found=true
|
||||
break
|
||||
done
|
||||
|
||||
if [[ "$found" == "false" ]]; then
|
||||
echo "Warning: Invalid argument: \"$token\" ($token_tag). Skipping..." >&2
|
||||
fi
|
||||
|
||||
((token_idx++))
|
||||
done
|
||||
|
||||
# Extract results into global variables
|
||||
# extract
|
||||
BARG_PARSED_COMMANDS=()
|
||||
BARG_PARSED_VALUES=()
|
||||
|
||||
for cmd_id in "${cmd_stack[@]}"; do
|
||||
local cmd_name="${schema["${cmd_id}.name"]}"
|
||||
local cmd_args="${schema["${cmd_id}.args"]}"
|
||||
BARG_PARSED_COMMANDS+=("$cmd_name")
|
||||
|
||||
[[ -z "$cmd_args" ]] && continue
|
||||
|
||||
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"]}"
|
||||
local atype="${schema["${arg_id}.type"]}"
|
||||
|
||||
if [[ -z "$value" && "$required" == "true" && "$BARG_HELP_ONLY" != "true" ]]; then
|
||||
echo "Error: Argument \"$name\" is required in command \"$cmd_name\"" >&2
|
||||
echo "Error: Argument \"$dest\" required" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Do not set "false"/"0" defaults for flags
|
||||
if [[ "$atype" == "flag" && ("$value" == "false" || "$value" == "0") ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ -n "$value" ]]; then
|
||||
if [[ -n "${BARG_PARSED_VALUES[$dest]}" ]]; then
|
||||
BARG_PARSED_VALUES["${cmd_name}_${dest}"]="$value"
|
||||
@ -346,7 +330,6 @@ parse_arguments() {
|
||||
done
|
||||
done
|
||||
|
||||
# Copy schema and aliases to global for usage generation
|
||||
for key in "${!schema[@]}"; do BARG_SCHEMA["$key"]="${schema[$key]}"; done
|
||||
for key in "${!aliases[@]}"; do BARG_ALIASES["$key"]="${aliases[$key]}"; done
|
||||
}
|
||||
@ -356,13 +339,14 @@ parse_arguments() {
|
||||
process_tokens
|
||||
}
|
||||
|
||||
# --- usage unchanged except minor safety ---
|
||||
# --------------------------------------------------------------------
|
||||
# Usage
|
||||
# --------------------------------------------------------------------
|
||||
barg_usage() {
|
||||
local cmd_path=("$@")
|
||||
|
||||
local -a cmd_path=("$@")
|
||||
local cmd_id="${BARG_ALIASES["cmd::root"]}"
|
||||
local current_name="${BARG_SCHEMA["${cmd_id}.name"]}"
|
||||
|
||||
# resolve command path
|
||||
if [[ ${#cmd_path[@]} -gt 0 ]]; then
|
||||
for cmd_name in "${cmd_path[@]}"; do
|
||||
local next_id="${BARG_ALIASES["${cmd_id}.cmd::${cmd_name}"]}"
|
||||
@ -371,7 +355,6 @@ barg_usage() {
|
||||
return 1
|
||||
fi
|
||||
cmd_id="$next_id"
|
||||
current_name="$cmd_name"
|
||||
done
|
||||
fi
|
||||
|
||||
@ -382,6 +365,7 @@ barg_usage() {
|
||||
echo "$help_text"
|
||||
}
|
||||
|
||||
# --- list arguments ---
|
||||
local cmd_args="${BARG_SCHEMA["${cmd_id}.args"]}"
|
||||
if [[ -n "$cmd_args" ]]; then
|
||||
echo ""
|
||||
@ -410,6 +394,7 @@ barg_usage() {
|
||||
done
|
||||
fi
|
||||
|
||||
# --- list subcommands ---
|
||||
local has_subcommands=false
|
||||
for key in "${!BARG_ALIASES[@]}"; do
|
||||
if [[ "$key" =~ ^${cmd_id}\.cmd::(.+)$ ]]; then
|
||||
@ -427,63 +412,113 @@ barg_usage() {
|
||||
done
|
||||
}
|
||||
|
||||
# --- export parsed values into variables before dispatch ---
|
||||
# --------------------------------------------------------------------
|
||||
# Export parsed vars
|
||||
# --------------------------------------------------------------------
|
||||
barg_export_vars() {
|
||||
local prefix="${1:-}"
|
||||
local k
|
||||
local k base
|
||||
for k in "${!BARG_PARSED_VALUES[@]}"; do
|
||||
declare -g "${prefix}${k}=${BARG_PARSED_VALUES[$k]}"
|
||||
declare -g "${k}=${BARG_PARSED_VALUES[$k]}"
|
||||
if [[ "$k" =~ ^[^_]+_(.+)$ ]]; then
|
||||
base="${BASH_REMATCH[1]}"
|
||||
[[ -z "${!base+x}" ]] && declare -g "${base}=${BARG_PARSED_VALUES[$k]}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# --- dispatch: export then call handler ---
|
||||
# --------------------------------------------------------------------
|
||||
# Dispatch
|
||||
# --------------------------------------------------------------------
|
||||
barg_dispatch() {
|
||||
local cmd_path=""
|
||||
local func_name="cmd"
|
||||
|
||||
local func="cmd"
|
||||
for ((i = 1; i < ${#BARG_PARSED_COMMANDS[@]}; i++)); do
|
||||
local cmd="${BARG_PARSED_COMMANDS[$i]}"
|
||||
func_name="${func_name}_${cmd}"
|
||||
cmd_path="${cmd_path} ${cmd}"
|
||||
func="${func}_${BARG_PARSED_COMMANDS[$i]}"
|
||||
done
|
||||
|
||||
if declare -f "$func_name" >/dev/null 2>&1; then
|
||||
if declare -f "$func" >/dev/null 2>&1; then
|
||||
barg_export_vars
|
||||
"$func_name"
|
||||
"$func"
|
||||
else
|
||||
echo "Error: No handler found for command:${cmd_path}" >&2
|
||||
echo "Expected function: ${func_name}" >&2
|
||||
echo ""
|
||||
echo "Error: Handler not found ($func)" >&2
|
||||
barg_usage "${BARG_PARSED_COMMANDS[@]:1}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# --- entrypoint: pass args as array with separator, honor help-only ---
|
||||
# --------------------------------------------------------------------
|
||||
# Helper: collect all .dest and command_dest variants
|
||||
# --------------------------------------------------------------------
|
||||
_barg_collect_all_dests() {
|
||||
local -n _schema=$1
|
||||
local -A seen=()
|
||||
for key in "${!_schema[@]}"; do
|
||||
[[ $key =~ \.dest$ ]] || continue
|
||||
local dest="${_schema[$key]}"
|
||||
[[ -z "$dest" ]] && continue
|
||||
seen[$dest]=1
|
||||
done
|
||||
for key in "${!_schema[@]}"; do
|
||||
[[ $key =~ ^([0-9]+)\.name$ ]] || continue
|
||||
local cmd_id="${BASH_REMATCH[1]}"
|
||||
local cmd="${_schema[$key]}"
|
||||
local args_str="${_schema[${cmd_id}.args]}"
|
||||
[[ -z "$args_str" ]] && continue
|
||||
IFS=',' read -r -a arg_ids <<<"$args_str"
|
||||
for a in "${arg_ids[@]}"; do
|
||||
local d="${_schema[${a}.dest]}"
|
||||
[[ -n "$d" ]] && seen["${cmd}_${d}"]=1
|
||||
done
|
||||
done
|
||||
printf '%s\n' "${!seen[@]}" | sort
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Entry point
|
||||
# --------------------------------------------------------------------
|
||||
barg_run() {
|
||||
local -a spec=("${!1}")
|
||||
shift
|
||||
local -a input=("$@")
|
||||
|
||||
if ! parse_arguments "${spec[@]}" -- "$@" 2>&1; then
|
||||
if ! parse_arguments "${spec[@]}" -- "${input[@]}" 2>&1; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$BARG_HELP_ONLY" == "true" || "${BARG_PARSED_VALUES[help]}" == "true" ]]; then
|
||||
barg_usage "${BARG_PARSED_COMMANDS[@]:1}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local -a caller_stack=("${BASH_SOURCE[@]}")
|
||||
local caller="${caller_stack[1]}"
|
||||
local out_file="${caller%.*}.vars.generated.sh"
|
||||
|
||||
# Only generate when explicitly requested
|
||||
if [[ "$BARG_GENERATE_VARS" == "true" || "$BARG_REGEN_VARS" == "1" ]]; then
|
||||
mapfile -t vars < <(_barg_collect_all_dests BARG_SCHEMA)
|
||||
{
|
||||
echo "# ---- autogenerated: BARG vars for ShellCheck ----"
|
||||
echo "# generated from $(basename "$caller")"
|
||||
echo "# regenerate with: BARG_REGEN_VARS=1 ./$(basename "$caller") ..."
|
||||
echo "# shellcheck disable=SC2034"
|
||||
echo "if false; then"
|
||||
echo " CLI_SPEC=()"
|
||||
printf ' %s=\n' "${vars[@]}" | paste -sd' ' -
|
||||
echo "fi"
|
||||
} >"$out_file"
|
||||
fi
|
||||
|
||||
# Source file if it already exists (not required to generate)
|
||||
if [[ -f "$out_file" ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "$out_file"
|
||||
fi
|
||||
|
||||
barg_dispatch
|
||||
}
|
||||
|
||||
# --- getters ---
|
||||
barg_get() {
|
||||
local key="$1"
|
||||
echo "${BARG_PARSED_VALUES[$key]}"
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# Access helpers
|
||||
# --------------------------------------------------------------------
|
||||
barg_get() { echo "${BARG_PARSED_VALUES[$1]}"; }
|
||||
barg_has() {
|
||||
local key="$1"
|
||||
local val="${BARG_PARSED_VALUES[$key]}"
|
||||
local val="${BARG_PARSED_VALUES[$1]}"
|
||||
[[ "$val" == "true" || "$val" == "1" ]]
|
||||
}
|
||||
|
||||
@ -4,9 +4,12 @@
|
||||
|
||||
# Source the BARG framework
|
||||
source ./barg.sh
|
||||
# shellcheck source=cli.vars.generated.sh
|
||||
[[ -f "${BASH_SOURCE%.*}.vars.generated.sh" ]] &&
|
||||
source "${BASH_SOURCE%.*}.vars.generated.sh"
|
||||
|
||||
# Define your CLI specification
|
||||
CLI_SPEC=(
|
||||
export CLI_SPEC=(
|
||||
"command;dev;Dev Container Management Tool"
|
||||
"argument;help,h;type:flag;help:Show help message"
|
||||
"argument;global;type:flag;help:Enable global mode"
|
||||
42
sample.sh
42
sample.sh
@ -1,42 +0,0 @@
|
||||
#!/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