#!/usr/bin/env bash # core/framework/flag.sh # Optimized: pre-parsed constraints + per-command flag index # ── Storage ──────────────────────────────────────────────────────────────── declare -gA _FLAG_REGISTRY=() # "ctx:--flag" → "type|description" declare -gA _FLAG_INDEX=() # "ctx" → " --flag1 --flag2 ..." (space-separated) # Pre-parsed constraints declare -gA _FLAG_C_DEFAULT=() declare -gA _FLAG_C_TYPE=() declare -gA _FLAG_C_MIN=() declare -gA _FLAG_C_MAX=() declare -gA _FLAG_C_CHOICES=() declare -gA _FLAG_C_REQUIRED=() declare -gA _FLAG_C_LABEL=() declare -gA _FLAG_C_SECTION=() # Runtime values declare -gA _FLAG_VALUES=() declare -gA _FLAG_ARRAYS=() declare -gA _FLAG_SET=() declare -ga _FLAG_ARGS=() # Completion registry declare -gA _FLAG_COMPLETION=() # ── Pure bash constraint extractor ──────────────────────────────────────── function flag::_extract_constraint() { local constraints="$1" lookup="$2" constraints="${constraints#[}"; constraints="${constraints%]}" local rest="$constraints" while [[ -n "$rest" ]]; do local pair="${rest%%,*}" pair="${pair# }"; pair="${pair% }" local k="${pair%%=*}"; local v="${pair#*=}" k="${k% }"; v="${v# }"; v="${v//\"/}" if [[ "$k" == "$lookup" ]]; then printf '%s' "$v" return 0 fi [[ "$rest" == *,* ]] && rest="${rest#*,}" || break done } function flag::_parse_and_cache() { local key="$1" constraints="$2" [[ -z "$constraints" ]] && return 0 local v v=$(flag::_extract_constraint "$constraints" "default") [[ -n "$v" ]] && _FLAG_C_DEFAULT["$key"]="$v" v=$(flag::_extract_constraint "$constraints" "type") [[ -n "$v" ]] && _FLAG_C_TYPE["$key"]="$v" v=$(flag::_extract_constraint "$constraints" "min") [[ -n "$v" ]] && _FLAG_C_MIN["$key"]="$v" v=$(flag::_extract_constraint "$constraints" "max") [[ -n "$v" ]] && _FLAG_C_MAX["$key"]="$v" v=$(flag::_extract_constraint "$constraints" "choices") [[ -n "$v" ]] && _FLAG_C_CHOICES["$key"]="$v" v=$(flag::_extract_constraint "$constraints" "required") [[ "$v" == "true" ]] && _FLAG_C_REQUIRED["$key"]="true" v=$(flag::_extract_constraint "$constraints" "label") [[ -n "$v" ]] && _FLAG_C_LABEL["$key"]="$v" v=$(flag::_extract_constraint "$constraints" "section") [[ -n "$v" ]] && _FLAG_C_SECTION["$key"]="$v" } # ── Context ─────────────────────────────────────────────────────────────── function flag::_context() { printf '%s' "${_CURRENT_COMMAND:-__global__}"; } function flag::_key() { printf '%s:%s' "${_CURRENT_COMMAND:-__global__}" "$1"; } # ── Registration ────────────────────────────────────────────────────────── function flag::define() { local raw_flag="$1" is_array=false if [[ "$raw_flag" == *"[]" ]]; then is_array=true; raw_flag="${raw_flag%[]}" fi local type description constraints="" if $is_array; then type="array"; description="${2:-}"; constraints="${3:-}" else type="${2:-bool}"; description="${3:-}"; constraints="${4:-}" fi local ctx="${_CURRENT_COMMAND:-__global__}" local key="${ctx}:${raw_flag}" _FLAG_REGISTRY["$key"]="${type}|${description}" # Per-command index — fast lookup in flag::parse _FLAG_INDEX["$ctx"]+=" ${raw_flag}" # Pre-parse constraints flag::_parse_and_cache "$key" "$constraints" # Bool default if [[ "$type" == "bool" && -z "${_FLAG_C_DEFAULT[$key]:-}" ]]; then _FLAG_C_DEFAULT["$key"]="false" fi # Help section assignment (no subshell — uses cached value) if declare -f help::_assign_flag_from_cache &>/dev/null; then help::_assign_flag_from_cache "$raw_flag" "${_FLAG_C_SECTION[$key]:-}" fi _FLAG_COMPLETION["$raw_flag"]=1 } function flag::register() { _FLAG_COMPLETION["${1:-}"]=1; } # ── Parsing ─────────────────────────────────────────────────────────────── function flag::parse() { local ctx="${_CURRENT_COMMAND:-__global__}" # Reset runtime _FLAG_VALUES=(); _FLAG_ARRAYS=(); _FLAG_SET=(); _FLAG_ARGS=() # Check for --help/-h first (fast path) local arg for arg in "$@"; do if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then local cmd="${_CURRENT_COMMAND%%::*}" declare -f hook::fire &>/dev/null && \ hook::fire "command:help:${cmd}" "$cmd" "${_CURRENT_COMMAND##*::}" return 1 fi done # Initialize from per-command index (fast — no full registry scan) local flag key type default_val for flag in ${_FLAG_INDEX[$ctx]:-}; do key="${ctx}:${flag}" [[ -z "${_FLAG_REGISTRY[$key]+x}" ]] && continue type="${_FLAG_REGISTRY[$key]%%|*}" default_val="${_FLAG_C_DEFAULT[$key]:-}" case "$type" in bool) _FLAG_VALUES["$flag"]="${default_val:-false}" ;; value) [[ -n "$default_val" ]] && _FLAG_VALUES["$flag"]="$default_val" ;; array) _FLAG_ARRAYS["$flag"]="" ;; esac done # Parse args while [[ $# -gt 0 ]]; do arg="$1" if [[ "$arg" == "--" ]]; then shift; _FLAG_ARGS+=("$@"); break fi if [[ "$arg" != --* ]]; then _FLAG_ARGS+=("$arg"); shift; continue fi key="${ctx}:${arg}" if [[ -z "${_FLAG_REGISTRY[$key]+x}" ]]; then log::error "Unknown flag: ${arg}"; return 1 fi type="${_FLAG_REGISTRY[$key]%%|*}" case "$type" in bool) _FLAG_VALUES["$arg"]="true" _FLAG_SET["$arg"]="1" shift ;; value) if [[ $# -lt 2 || "$2" == --* ]]; then log::error "Flag ${arg} requires a value"; return 1 fi local val="$2" local vtype="${_FLAG_C_TYPE[$key]:-}" if [[ "$vtype" == "int" ]]; then if ! [[ "$val" =~ ^[0-9]+$ ]]; then log::error "Flag ${arg} requires an integer, got: ${val}"; return 1 fi local min="${_FLAG_C_MIN[$key]:-}" max="${_FLAG_C_MAX[$key]:-}" [[ -n "$min" && "$val" -lt "$min" ]] && \ log::error "Flag ${arg} minimum is ${min}, got: ${val}" && return 1 [[ -n "$max" && "$val" -gt "$max" ]] && \ log::error "Flag ${arg} maximum is ${max}, got: ${val}" && return 1 fi local choices="${_FLAG_C_CHOICES[$key]:-}" if [[ -n "$choices" ]]; then local valid=false choice local IFS='|' for choice in $choices; do [[ "$val" == "$choice" ]] && valid=true && break done unset IFS if ! $valid; then log::error "Flag ${arg} must be one of: ${choices//|/, }, got: ${val}" return 1 fi fi _FLAG_VALUES["$arg"]="$val" _FLAG_SET["$arg"]="1" shift 2 ;; array) if [[ $# -lt 2 || "$2" == --* ]]; then log::error "Flag ${arg} requires a value"; return 1 fi if [[ -n "${_FLAG_ARRAYS[$arg]:-}" ]]; then _FLAG_ARRAYS["$arg"]+=$'\n'"$2" else _FLAG_ARRAYS["$arg"]="$2" fi _FLAG_SET["$arg"]="1" shift 2 ;; esac done # Validate required for flag in ${_FLAG_INDEX[$ctx]:-}; do key="${ctx}:${flag}" [[ -z "${_FLAG_C_REQUIRED[$key]:-}" ]] && continue if [[ -z "${_FLAG_SET[$flag]+x}" && -z "${_FLAG_VALUES[$flag]:-}" ]]; then log::error "Flag ${flag} is required"; return 1 fi done # Validate exclusive groups local groups="${_FLAG_EXCLUSIVE_GROUPS[${_CURRENT_COMMAND%%::*}]:-}" if [[ -n "$groups" ]]; then local group while IFS= read -r group; do [[ -z "$group" ]] && continue local -a members=() local OLD_IFS="$IFS"; IFS=','; read -ra members <<< "$group"; IFS="$OLD_IFS" local found_count=0 found_flags="" member for member in "${members[@]}"; do flag::set "$member" && (( found_count++ )) && found_flags+=" $member" done if [[ $found_count -gt 1 ]]; then log::error "Flags${found_flags} are mutually exclusive"; return 1 fi done < <(printf '%s' "$groups" | tr '|' '\n') fi return 0 } # ── Accessors ───────────────────────────────────────────────────────────── function flag::bool() { [[ "${_FLAG_VALUES[$1]:-false}" == "true" ]]; } function flag::value() { printf '%s' "${_FLAG_VALUES[$1]:-}"; } function flag::array() { printf '%s' "${_FLAG_ARRAYS[$1]:-}"; } function flag::set() { [[ -n "${_FLAG_SET[$1]+x}" ]]; } function flag::args() { printf '%s ' "${_FLAG_ARGS[@]:-}"; } function flag::args_array() { local -n _arr_ref="$1" _arr_ref=("${_FLAG_ARGS[@]:-}") } # ── Reset ───────────────────────────────────────────────────────────────── function flag::_reset_runtime() { _FLAG_VALUES=(); _FLAG_ARRAYS=(); _FLAG_SET=(); _FLAG_ARGS=() } function flag::_reset_registry() { local ctx="${1:-$(flag::_context)}" local key for key in "${!_FLAG_REGISTRY[@]}"; do [[ "$key" == "${ctx}:"* ]] || continue unset "_FLAG_REGISTRY[$key]" "_FLAG_C_DEFAULT[$key]" "_FLAG_C_TYPE[$key]" unset "_FLAG_C_MIN[$key]" "_FLAG_C_MAX[$key]" "_FLAG_C_CHOICES[$key]" unset "_FLAG_C_REQUIRED[$key]" "_FLAG_C_LABEL[$key]" "_FLAG_C_SECTION[$key]" done unset "_FLAG_INDEX[$ctx]" } # ── Help bridge ─────────────────────────────────────────────────────────── function help::_assign_flag_from_cache() { local flag="$1" section="$2" local ctx="${_CURRENT_COMMAND:-__global__}" [[ -z "$section" ]] && section="${_CURRENT_HELP_SECTION:-}" [[ -z "$section" ]] && return 0 local skey="${ctx}:${section}" if [[ -z "${_HELP_SECTIONS[$skey]+x}" ]]; then _HELP_SECTIONS["$skey"]=$(( _HELP_SECTION_COUNT++ )) fi _HELP_FLAG_SECTION["${ctx}:${flag}"]="$section" }