diff --git a/barg.sh b/barg.sh new file mode 100755 index 0000000..0826138 --- /dev/null +++ b/barg.sh @@ -0,0 +1,489 @@ +# --- 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" ]] +} diff --git a/cli.sh b/cli.sh new file mode 100755 index 0000000..c5d7798 --- /dev/null +++ b/cli.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +# Example: dev CLI tool using BARG framework + +# Source the BARG framework +source ./barg.sh + +# Define your CLI specification +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" + + "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:Secondary name" + "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;Nested container configuration" + "argument;dev;type:flag;help:Development mode" + "argument;name;required;help:Container name" + "argument;cmd;type:rest;dest:cmdRest;help:Command to run" + "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" +) + +# Command handlers - implement your logic here! + +# Root command handler +cmd() { + echo "Dev Container Tool" + echo "Use --help to see available commands" +} + +cmd_build() { + # now available directly: + # $name, $imageName, $fromName, $quiet, $verbose, $cmd, $nameOpt, $includePaths ... + echo "Building container: $name" + echo " Base image: $imageName" + [[ -n "$fromName" ]] && echo " From: $fromName" + [[ "$verbose" == "true" ]] && echo " Verbose mode enabled" + [[ "$quiet" == "true" ]] && echo " Quiet mode enabled" + [[ -n "$cmd" ]] && echo " Command: $cmd" + echo "Container built successfully!" +} + +cmd_build_container() { + # $name is the build-level name, container's name appears as $container_name due to collision resolution + echo "Building nested container configuration" + echo " Parent container: $name" + echo " Child container: $container_name" + echo " Image: $imageName" + [[ "$dev" == "true" ]] && echo " Development mode: ON" + [[ -n "$cmdRest" ]] && echo " Command: $cmdRest" + echo "Nested container configured!" +} + +cmd_stop() { + echo "Stopping container: $name" + if [[ "$kill" == "true" ]]; then + echo " Force killing..." + else + echo " Graceful shutdown..." + fi + echo "Container stopped!" +} + +# Run the CLI with all arguments +barg_run CLI_SPEC[@] "$@"