- flag::define: variadic constraint args (key:value) instead of bracket string - flag::_parse_constraints_from_args: replaces flag::_parse_and_cache - flag::set_constraint: Option B syntax for post-definition constraints - choices separator: comma (choices:split,full) — no quoting needed - guard against empty _CURRENT_COMMAND in exclusive groups lookup - migrate all commands to new constraint syntax - add helpful error for unknown constraint args
399 lines
No EOL
14 KiB
Bash
399 lines
No EOL
14 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
|
|
# log::debug "parse_and_cache: key=$key"
|
|
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")
|
|
# log::debug "parse_and_cache: choices='$v'"
|
|
[[ -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"
|
|
}
|
|
|
|
# ── Constraints ────────────────────────────────────────
|
|
|
|
# flag::set_constraint <flag> <key> [value]
|
|
# Set a constraint on an already-defined flag (Option B syntax)
|
|
# Examples:
|
|
# flag::set_constraint --mode choices "split|full"
|
|
# flag::set_constraint --mode required
|
|
# flag::set_constraint --mode section Options
|
|
function flag::set_constraint() {
|
|
local flag="$1" k="$2" v="${3:-}"
|
|
local ctx="${_CURRENT_COMMAND:-__global__}"
|
|
local key="${ctx}:${flag}"
|
|
|
|
[[ -z "${_FLAG_REGISTRY[$key]+x}" ]] && \
|
|
log::error "flag::set_constraint: flag not defined: ${flag}" && return 1
|
|
|
|
case "$k" in
|
|
label) _FLAG_C_LABEL["$key"]="$v" ;;
|
|
default) _FLAG_C_DEFAULT["$key"]="$v" ;;
|
|
type) _FLAG_C_TYPE["$key"]="$v" ;;
|
|
min) _FLAG_C_MIN["$key"]="$v" ;;
|
|
max) _FLAG_C_MAX["$key"]="$v" ;;
|
|
choices) _FLAG_C_CHOICES["$key"]="$v" ;;
|
|
section)
|
|
_FLAG_C_SECTION["$key"]="$v"
|
|
declare -f help::_assign_flag_from_cache &>/dev/null && \
|
|
help::_assign_flag_from_cache "$flag" "$v"
|
|
;;
|
|
required) _FLAG_C_REQUIRED["$key"]="true" ;;
|
|
esac
|
|
}
|
|
|
|
# ── Context ───────────────────────────────────────────────────────────────
|
|
|
|
function flag::_context() { printf '%s' "${_CURRENT_COMMAND:-__global__}"; }
|
|
function flag::_key() { printf '%s:%s' "${_CURRENT_COMMAND:-__global__}" "$1"; }
|
|
|
|
# ── Registration ──────────────────────────────────────────────────────────
|
|
|
|
# flag::define <flag> [bool|value] "description" [constraints...]
|
|
# flag::define <flag[]> "description" [constraints...]
|
|
#
|
|
# Constraints (positional, after description):
|
|
# label:name Display label in help
|
|
# default:50 Default value
|
|
# type:int Value type (int, string)
|
|
# min:1 Minimum value (int)
|
|
# max:100 Maximum value (int)
|
|
# choices:a|b|c Allowed values (pipe-separated)
|
|
# section:Filters Help section
|
|
# required Mark as required (no value needed)
|
|
#
|
|
# Examples:
|
|
# flag::define --fw bool "Firewall only" section:Filters
|
|
# flag::define --limit value "Max results" default:50 type:int min:1 section:Output
|
|
# flag::define --mode value "Tunnel mode" required choices:split|full section:Options
|
|
# flag::define --svc[] "Exclude service" label:service section:Filters
|
|
function flag::define() {
|
|
local raw_flag="$1"
|
|
local is_array=false
|
|
|
|
if [[ "$raw_flag" == *"[]" ]]; then
|
|
is_array=true
|
|
raw_flag="${raw_flag%[]}"
|
|
fi
|
|
|
|
local type description
|
|
local -a constraints=()
|
|
|
|
if $is_array; then
|
|
type="array"
|
|
description="${2:-}"
|
|
shift 2
|
|
constraints=("$@")
|
|
else
|
|
type="${2:-bool}"
|
|
description="${3:-}"
|
|
shift 3
|
|
constraints=("$@")
|
|
fi
|
|
|
|
local ctx="${_CURRENT_COMMAND:-__global__}"
|
|
local key="${ctx}:${raw_flag}"
|
|
|
|
_FLAG_REGISTRY["$key"]="${type}|${description}"
|
|
_FLAG_INDEX["$ctx"]+=" ${raw_flag}"
|
|
|
|
# Parse constraints
|
|
flag::_parse_constraints_from_args "$key" "${constraints[@]:-}"
|
|
|
|
# Bool default
|
|
if [[ "$type" == "bool" && -z "${_FLAG_C_DEFAULT[$key]:-}" ]]; then
|
|
_FLAG_C_DEFAULT["$key"]="false"
|
|
fi
|
|
|
|
# Help section — constraint takes precedence, then active section
|
|
local section="${_FLAG_C_SECTION[$key]:-${_CURRENT_HELP_SECTION:-}}"
|
|
if [[ -n "$section" ]]; then
|
|
_FLAG_C_SECTION["$key"]="$section"
|
|
declare -f help::_assign_flag_from_cache &>/dev/null && \
|
|
help::_assign_flag_from_cache "$raw_flag" "$section"
|
|
fi
|
|
|
|
_FLAG_COMPLETION["$raw_flag"]=1
|
|
}
|
|
|
|
function flag::register() { _FLAG_COMPLETION["${1:-}"]=1; }
|
|
|
|
# ── Parsing ───────────────────────────────────────────────────────────────
|
|
|
|
function flag::parse() {
|
|
local ctx="${_CURRENT_COMMAND:-__global__}"
|
|
# log::debug "ctx=$ctx index='${_FLAG_INDEX[$ctx]:-EMPTY}'" >&2
|
|
|
|
# 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]:-}"
|
|
# log::debug "choices check: flag=$arg val=$val choices='${_FLAG_C_CHOICES[$key]:-EMPTY}'"
|
|
if [[ -n "$choices" ]]; then
|
|
local valid=false
|
|
local choice
|
|
local saved_ifs="$IFS"
|
|
IFS=',' read -ra choice_list <<< "$choices"
|
|
IFS="$saved_ifs"
|
|
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
|
|
_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 cmd="${_CURRENT_COMMAND%%::*}"
|
|
local groups=""
|
|
[[ -n "$cmd" ]] && groups="${_FLAG_EXCLUSIVE_GROUPS[$cmd]:-}"
|
|
|
|
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
|
|
}
|
|
|
|
# flag::_parse_constraints_from_args <key> <constraint_args...>
|
|
# Parse variadic constraint args: label:x required choices:a|b section:x
|
|
function flag::_parse_constraints_from_args() {
|
|
local key="$1"
|
|
shift
|
|
local arg k v
|
|
|
|
for arg in "$@"; do
|
|
# Handle bare keywords (no colon) — e.g. "required"
|
|
if [[ "$arg" != *:* ]]; then
|
|
case "$arg" in
|
|
required) _FLAG_C_REQUIRED["$key"]="true" ;;
|
|
optional) ;; # default, no-op
|
|
esac
|
|
continue
|
|
fi
|
|
|
|
k="${arg%%:*}"
|
|
v="${arg#*:}"
|
|
|
|
case "$k" in
|
|
label) _FLAG_C_LABEL["$key"]="$v" ;;
|
|
default) _FLAG_C_DEFAULT["$key"]="$v" ;;
|
|
type) _FLAG_C_TYPE["$key"]="$v" ;;
|
|
min) _FLAG_C_MIN["$key"]="$v" ;;
|
|
max) _FLAG_C_MAX["$key"]="$v" ;;
|
|
choices) _FLAG_C_CHOICES["$key"]="$v" ;;
|
|
section) _FLAG_C_SECTION["$key"]="$v" ;;
|
|
required) _FLAG_C_REQUIRED["$key"]="$v" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
|
|
# ── 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"
|
|
} |