#!/usr/bin/env bash # core/framework/flag.sh # # Declarative flag registration and parsing framework. # # Registration (in on_load): # flag::define --name value "Filter by peer name" # flag::define --fw bool "Firewall events only" # flag::define --limit value "Max results" [default=50, type=int, min=1] # flag::define --exclude[] "Exclude service" [label="service"] # flag::exclusive --fw --wg # # Parsing (in run): # flag::parse "$@" || return 1 # # Accessors: # flag::bool --fw → exits 0/1 # flag::value --name → prints value or default # flag::array --exclude → prints newline-separated values # flag::set --name → exits 0 if explicitly passed # flag::args → prints remaining positional args # ── Storage ──────────────────────────────────────────────────────────────── # Per-command flag registry — populated by flag::define during on_load # Key: "cmd::subcmd:--flag" # Value: "type|description|constraints" declare -gA _FLAG_REGISTRY=() # Runtime values — populated by flag::parse during run declare -gA _FLAG_VALUES=() # --flag → value (bool: "true"/"false") declare -gA _FLAG_ARRAYS=() # --flag → newline-separated values declare -gA _FLAG_SET=() # --flag → "1" if explicitly passed declare -ga _FLAG_ARGS=() # remaining positional args # Defaults — populated at registration time declare -gA _FLAG_DEFAULTS=() # "cmd::subcmd:--flag" → default value # ── Helpers ───────────────────────────────────────────────────────────────── function flag::_context() { echo "${_CURRENT_COMMAND:-__global__}" } function flag::_key() { echo "$(flag::_context):${1}" } function flag::_parse_constraints() { # Parse "[key=val, key=val]" constraint string # Output: "key=val\nkey=val" local constraints="${1:-}" constraints="${constraints#[}" constraints="${constraints%]}" echo "$constraints" | tr ',' '\n' | sed 's/^ *//' | sed 's/ *$//' } function flag::_constraint_get() { local constraints="$1" key="$2" echo "$constraints" | while IFS='=' read -r k v; do k="${k// /}" if [[ "$k" == "$key" ]]; then echo "${v//\"/}" return 0 fi done } # ── Registration ───────────────────────────────────────────────────────────── # flag::define --flag [bool|value] "description" [constraints] # flag::define --flag[] "description" [constraints] ← array flag function flag::define() { local raw_flag="$1" local is_array=false # Detect array flag syntax: --flag[] 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 key key=$(flag::_key "$raw_flag") _FLAG_REGISTRY["$key"]="${type}|${description}|${constraints}" # Extract and store default if [[ -n "$constraints" ]]; then local parsed_constraints parsed_constraints=$(flag::_parse_constraints "$constraints") local default_val default_val=$(flag::_constraint_get "$parsed_constraints" "default") if [[ -n "$default_val" ]]; then _FLAG_DEFAULTS["$key"]="$default_val" fi fi # Set bool default to false if not specified if [[ "$type" == "bool" && -z "${_FLAG_DEFAULTS[$key]:-}" ]]; then _FLAG_DEFAULTS["$key"]="false" fi # Register for shell completion (backward compat) flag::register "$raw_flag" 2>/dev/null || true help::_assign_flag_section "$raw_flag" "$constraints" } # flag::register remains for backward compat and completion-only registration # Already defined in command.sh — this is a no-op if already defined function flag::register() { : # handled by command.sh completion registry } # ── Parsing ────────────────────────────────────────────────────────────────── function flag::parse() { local ctx ctx=$(flag::_context) # Reset runtime state _FLAG_VALUES=() _FLAG_ARRAYS=() _FLAG_SET=() _FLAG_ARGS=() # Initialize bools to false, values to defaults local key for key in "${!_FLAG_REGISTRY[@]}"; do [[ "$key" != "${ctx}:"* ]] && continue local flag="${key#${ctx}:}" local reg="${_FLAG_REGISTRY[$key]}" local type="${reg%%|*}" local default_val="${_FLAG_DEFAULTS[$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 local arg="$1" if [[ "$arg" == "--" ]]; then shift _FLAG_ARGS+=("$@") break fi if [[ "$arg" != --* ]]; then _FLAG_ARGS+=("$arg") shift continue fi local key key=$(flag::_key "$arg") if [[ -z "${_FLAG_REGISTRY[$key]+x}" ]]; then log::error "Unknown flag: ${arg}" return 1 fi local reg="${_FLAG_REGISTRY[$key]}" local type="${reg%%|*}" local constraints constraints=$(echo "$reg" | cut -d'|' -f3) 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" # Type validation if [[ -n "$constraints" ]]; then local parsed parsed=$(flag::_parse_constraints "$constraints") local vtype vtype=$(flag::_constraint_get "$parsed" "type") if [[ "$vtype" == "int" ]]; then if ! [[ "$val" =~ ^[0-9]+$ ]]; then log::error "Flag ${arg} requires an integer, got: ${val}" return 1 fi local min max min=$(flag::_constraint_get "$parsed" "min") max=$(flag::_constraint_get "$parsed" "max") [[ -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 choices=$(flag::_constraint_get "$parsed" "choices") if [[ -n "$choices" ]]; then local valid=false local choice IFS='|' read -ra choice_list <<< "$choices" for choice in "${choice_list[@]}"; do [[ "$val" == "$choice" ]] && valid=true && break done if ! $valid; then log::error "Flag ${arg} must be one of: ${choices//|/, }, got: ${val}" return 1 fi 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 flags for key in "${!_FLAG_REGISTRY[@]}"; do [[ "$key" != "${ctx}:"* ]] && continue local flag="${key#${ctx}:}" local reg="${_FLAG_REGISTRY[$key]}" local constraints constraints=$(echo "$reg" | cut -d'|' -f3) if [[ -n "$constraints" ]]; then local parsed parsed=$(flag::_parse_constraints "$constraints") local required required=$(flag::_constraint_get "$parsed" "required") if [[ "$required" == "true" && -z "${_FLAG_SET[$flag]+x}" ]]; then # Check if provided by defaults if [[ -z "${_FLAG_VALUES[$flag]:-}" ]]; then log::error "Flag ${flag} is required" return 1 fi fi fi done return 0 } # ── Accessors ───────────────────────────────────────────────────────────────── # flag::bool --flag → exits 0 if true, 1 if false function flag::bool() { [[ "${_FLAG_VALUES[$1]:-false}" == "true" ]] } # flag::value --flag → prints value or empty string function flag::value() { echo "${_FLAG_VALUES[$1]:-}" } # flag::array --flag → prints newline-separated values function flag::array() { echo "${_FLAG_ARRAYS[$1]:-}" } # flag::set --flag → exits 0 if explicitly passed by user function flag::set() { [[ -n "${_FLAG_SET[$1]+x}" ]] } # flag::args → prints positional args array function flag::args() { echo "${_FLAG_ARGS[@]:-}" } # flag::args_array → populates a nameref array with positional 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() { # Clear registry entries for a specific command context local ctx="${1:-$(flag::_context)}" local key for key in "${!_FLAG_REGISTRY[@]}"; do [[ "$key" == "${ctx}:"* ]] && unset "_FLAG_REGISTRY[$key]" done for key in "${!_FLAG_DEFAULTS[@]}"; do [[ "$key" == "${ctx}:"* ]] && unset "_FLAG_DEFAULTS[$key]" done }