wgctl/core/framework/flag.sh
Nuno Duque Nunes f61bc59446 feat: block/unblock migration, help.sh optimization
- commands/block/: block.sh, show.sh, helpers.sh
- commands/unblock/: unblock.sh, show.sh, helpers.sh
- flag::define array type: --ip[], --subnet[], --port[], --service[]
- help.sh: use pre-cached _FLAG_C_* arrays instead of flag::_parse_constraints
- remove flag::_parse_constraints/flag::_constraint_get calls from help.sh
- adopt local var; var=value pattern for safe assignment
2026-05-30 12:31:41 +00:00

296 lines
No EOL
10 KiB
Bash

#!/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"
}