diff --git a/__test.js b/__test.js deleted file mode 100644 index 3d01b72..0000000 --- a/__test.js +++ /dev/null @@ -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")); diff --git a/barg b/barg deleted file mode 100755 index 46a8b6d..0000000 --- a/barg +++ /dev/null @@ -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 diff --git a/barg.sh b/barg.sh index 0826138..5426eeb 100755 --- a/barg.sh +++ b/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" ]] } diff --git a/cli.sh b/example.sh similarity index 94% rename from cli.sh rename to example.sh index c5d7798..a27ef71 100755 --- a/cli.sh +++ b/example.sh @@ -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" diff --git a/barg.js b/javascript/barg.js similarity index 100% rename from barg.js rename to javascript/barg.js diff --git a/package.json b/javascript/package.json similarity index 100% rename from package.json rename to javascript/package.json diff --git a/sample.sh b/sample.sh deleted file mode 100755 index 44f4ab3..0000000 --- a/sample.sh +++ /dev/null @@ -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 "$@"