# --- globals --- declare -A BARG_PARSED_VALUES declare -a BARG_PARSED_COMMANDS declare -A BARG_SCHEMA declare -A BARG_ALIASES BARG_HELP_ONLY=false # --- parse_arguments: take spec... '--' args... ; tokenize array; i=0 --- parse_arguments() { # split "$@" into spec_input and args by a literal "--" separator local -a spec_input=() local -a args=() local seen_sep=false for item in "$@"; do if [[ $seen_sep == false && "$item" == "--" ]]; then seen_sep=true continue fi if [[ $seen_sep == false ]]; then spec_input+=("$item") else args+=("$item") fi done # Local storage local -A schema=() local -A aliases=() local -a tokens=() # Tokenize input into tagged tokens (format: tag::value) tokenize() { local -a in=("$@") tokens=() tokens+=("root::root") local i=0 while [[ $i -lt ${#in[@]} ]]; do local token="${in[$i]}" # Rest arguments after "--" if [[ "$token" == "--" ]]; then ((i++)) local rest_args=() while [[ $i -lt ${#in[@]} ]]; do rest_args+=("${in[$i]}") ((i++)) done if [[ ${#rest_args[@]} -gt 0 ]]; 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(s): -k -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]}" for ((j = 0; j < ${#flags}; j++)); do tokens+=("narg::${flags:$j:1}") done tokens+=("value::${value}") else 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 (unchanged body except local arrays initialized) 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 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["${id}.entryType"]="command" schema["${id}.name"]="${aliases_str%%,*}" schema["${id}.help"]="$help_text" schema["${id}.args"]="" 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 ((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%%,*}" schema["${id}.entryType"]="argument" schema["${id}.name"]="$arg_name" schema["${id}.dest"]="$arg_name" schema["${id}.required"]="false" schema["${id}.type"]="positional" 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 else schema["${id}.${element}"]="true" fi done 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 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 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 local tag="${t%%::*}" local tok="${t#*::}" if [[ "$tag" == "narg" ]]; then if [[ "$tok" == "h" || "$tok" == "help" ]]; then help_present=true else other_narg=true fi fi done if [[ "$help_present" == true && "$other_narg" == false ]]; then BARG_HELP_ONLY=true fi while [[ $token_idx -lt ${#tokens[@]} ]]; do local tagged_token="${tokens[$token_idx]}" local token_tag="${tagged_token%%::*}" local token="${tagged_token#*::}" if [[ "$token_tag" == "root" ]]; then schema["${root_id}.name"]="$token" ((token_idx++)) 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}"]}" ;; unk) entry_id="${aliases["${current_cmd}.cmd::${token}"]}" 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 if [[ -z "$entry_id" ]]; then continue fi 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) 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" ((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 results into global variables 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 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" else BARG_PARSED_VALUES["$dest"]="$value" fi fi 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 } tokenize "${args[@]}" generate_schema "${spec_input[@]}" || return 1 process_tokens } # --- usage unchanged except minor safety --- barg_usage() { local cmd_path=("$@") local cmd_id="${BARG_ALIASES["cmd::root"]}" local current_name="${BARG_SCHEMA["${cmd_id}.name"]}" if [[ ${#cmd_path[@]} -gt 0 ]]; then for cmd_name in "${cmd_path[@]}"; do local next_id="${BARG_ALIASES["${cmd_id}.cmd::${cmd_name}"]}" if [[ -z "$next_id" ]]; then echo "Error: Unknown command: $cmd_name" >&2 return 1 fi cmd_id="$next_id" current_name="$cmd_name" done fi local help_text="${BARG_SCHEMA["${cmd_id}.help"]}" echo "Usage: ${cmd_path[*]} [OPTIONS] [COMMANDS]" [[ -n "$help_text" ]] && { echo "" echo "$help_text" } local cmd_args="${BARG_SCHEMA["${cmd_id}.args"]}" if [[ -n "$cmd_args" ]]; then echo "" echo "Arguments:" IFS=',' read -ra arg_ids <<<"$cmd_args" for arg_id in "${arg_ids[@]}"; do local arg_name="${BARG_SCHEMA["${arg_id}.name"]}" local arg_type="${BARG_SCHEMA["${arg_id}.type"]}" local arg_help="${BARG_SCHEMA["${arg_id}.help"]}" local required="${BARG_SCHEMA["${arg_id}.required"]}" local default="${BARG_SCHEMA["${arg_id}.value"]}" local prefix=" " case "$arg_type" in flag | option) prefix=" --${arg_name}" ;; positional) prefix=" <${arg_name}>" ;; rest) prefix=" -- <${arg_name}...>" ;; esac local suffix="" [[ "$required" == "true" ]] && suffix=" (required)" [[ -n "$default" ]] && suffix="${suffix} [default: $default]" echo "${prefix}${suffix}" [[ -n "$arg_help" ]] && echo " ${arg_help}" done fi local has_subcommands=false for key in "${!BARG_ALIASES[@]}"; do if [[ "$key" =~ ^${cmd_id}\.cmd::(.+)$ ]]; then if [[ "$has_subcommands" == "false" ]]; then echo "" echo "Commands:" has_subcommands=true fi local sub_cmd="${BASH_REMATCH[1]}" local sub_id="${BARG_ALIASES[$key]}" local sub_help="${BARG_SCHEMA["${sub_id}.help"]}" echo " ${sub_cmd}" [[ -n "$sub_help" ]] && echo " ${sub_help}" fi done } # --- export parsed values into variables before dispatch --- barg_export_vars() { local prefix="${1:-}" local k for k in "${!BARG_PARSED_VALUES[@]}"; do declare -g "${prefix}${k}=${BARG_PARSED_VALUES[$k]}" done } # --- dispatch: export then call handler --- barg_dispatch() { local cmd_path="" local func_name="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}" done if declare -f "$func_name" >/dev/null 2>&1; then barg_export_vars "$func_name" else echo "Error: No handler found for command:${cmd_path}" >&2 echo "Expected function: ${func_name}" >&2 echo "" barg_usage "${BARG_PARSED_COMMANDS[@]:1}" return 1 fi } # --- entrypoint: pass args as array with separator, honor help-only --- barg_run() { local -a spec=("${!1}") shift if ! parse_arguments "${spec[@]}" -- "$@" 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 barg_dispatch } # --- getters --- barg_get() { local key="$1" echo "${BARG_PARSED_VALUES[$key]}" } barg_has() { local key="$1" local val="${BARG_PARSED_VALUES[$key]}" [[ "$val" == "true" || "$val" == "1" ]] }