- core/framework/flag.sh: flag::define, flag::parse, accessors - core/framework/hook.sh: hook::on, hook::fire, hook::off, hook::has - core/framework/help.sh: help::section, command::help::auto - core/framework/command.sh: command::define, command::route, lazy loading - core restructure: framework/ + app/ separation - load_command: directory-based command detection - command::exists: accepts new-style commands - command::run: routing for new-style, legacy fallback - commands/logs/: migrated to new framework - logs.sh: router + command::define - show.sh: flag::define + flag::parse, no manual case blocks - clean.sh: flag::define + flag::parse - remove.sh: flag::define + flag::parse - rotate.sh: flag::define + flag::parse - logs clean: fix dry_run bool to int conversion - ctx::json_helper: fixed path after core restructure - PYTHONPATH: exported in app/core.sh
328 lines
No EOL
10 KiB
Bash
328 lines
No EOL
10 KiB
Bash
#!/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
|
|
} |