feat: command framework + logs migration
- 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
This commit is contained in:
parent
290ac24d88
commit
8ed491313d
13 changed files with 1730 additions and 92 deletions
30
commands/logs/clean.sh
Normal file
30
commands/logs/clean.sh
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/logs/clean.sh
|
||||
|
||||
function cmd::logs::clean::on_load() {
|
||||
flag::define --force bool "Skip confirmation"
|
||||
flag::define --dry-run bool "Show what would be removed"
|
||||
}
|
||||
|
||||
function cmd::logs::clean::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local force=false dry_run=false
|
||||
flag::bool --force && force=true
|
||||
flag::bool --dry-run && dry_run=true
|
||||
|
||||
local dry_run_flag="0"
|
||||
$dry_run && dry_run_flag="1"
|
||||
|
||||
local count
|
||||
count=$(json::clean_handshakes \
|
||||
"$(ctx::events_log)" "$dry_run_flag" 2>/dev/null) || {
|
||||
log::error "Failed to clean handshakes"
|
||||
return 1
|
||||
}
|
||||
|
||||
if $dry_run; then
|
||||
log::wg_warning "Dry run — would remove ${count} keepalive handshake(s)"
|
||||
else
|
||||
log::wg_success "Removed ${count} keepalive handshake(s)"
|
||||
fi
|
||||
}
|
||||
11
commands/logs/logs.sh
Normal file
11
commands/logs/logs.sh
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/logs/logs.sh — router only
|
||||
|
||||
function cmd::logs::on_load() {
|
||||
command::define show "Show WireGuard and firewall logs" [*]
|
||||
command::define clean "Remove keepalive handshakes" [c]
|
||||
command::define remove "Remove log entries" [rm, del]
|
||||
command::define rotate "Remove entries older than N days"
|
||||
}
|
||||
|
||||
hook::on "command:help:logs" command::help::auto
|
||||
22
commands/logs/remove.sh
Normal file
22
commands/logs/remove.sh
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/logs/remove.sh
|
||||
|
||||
function cmd::logs::remove::on_load() {
|
||||
flag::define --name value "Filter by peer name" [label="name"]
|
||||
flag::define --since value "Remove entries since" [label="time"]
|
||||
flag::define --fw bool "Remove firewall logs only"
|
||||
flag::define --wg bool "Remove WireGuard logs only"
|
||||
flag::define --force bool "Skip confirmation"
|
||||
}
|
||||
|
||||
function cmd::logs::remove::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local name; name=$(flag::value --name)
|
||||
local since; since=$(flag::value --since)
|
||||
local force=false fw=false wg=false
|
||||
flag::bool --force && force=true
|
||||
flag::bool --fw && fw=true
|
||||
flag::bool --wg && wg=true
|
||||
|
||||
cmd::logs::_remove_impl "$name" "$since" "$fw" "$wg" "$force"
|
||||
}
|
||||
18
commands/logs/rotate.sh
Normal file
18
commands/logs/rotate.sh
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/logs/rotate.sh
|
||||
|
||||
function cmd::logs::rotate::on_load() {
|
||||
flag::define --days value "Remove entries older than N days" [default=30, type=int, min=1, label="days"]
|
||||
flag::define --force bool "Skip confirmation"
|
||||
flag::define --dry-run bool "Show what would be removed"
|
||||
}
|
||||
|
||||
function cmd::logs::rotate::run() {
|
||||
flag::parse "$@" || return 1
|
||||
local days; days=$(flag::value --days)
|
||||
local force=false dry_run=false
|
||||
flag::bool --force && force=true
|
||||
flag::bool --dry-run && dry_run=true
|
||||
|
||||
cmd::logs::_rotate_impl "$days" "$force" "$dry_run"
|
||||
}
|
||||
504
commands/logs/show.sh
Normal file
504
commands/logs/show.sh
Normal file
|
|
@ -0,0 +1,504 @@
|
|||
#!/usr/bin/env bash
|
||||
# commands/logs/show.sh
|
||||
|
||||
FW_EVENTS_LOG="$(ctx::fw_events_log)"
|
||||
WG_EVENTS_LOG="$(ctx::events_log)"
|
||||
|
||||
function cmd::logs::show::on_load() {
|
||||
command::mixin json_output [section="Output"]
|
||||
|
||||
help::section "Filters"
|
||||
flag::define --name value "Filter by peer name" [label="name", section="Filters"]
|
||||
flag::define --type value "Filter by device type" [label="type", section="Filters"]
|
||||
flag::define --since value "Show events since (2h, 7d)" [label="time", section="Filters"]
|
||||
flag::define --service value "Filter by service/IP" [label="service", section="Filters"]
|
||||
flag::define --event value "Filter wg events" [label="type", section="Filters"]
|
||||
flag::define --fw bool "Show firewall drops only" [section="Filters"]
|
||||
flag::define --wg bool "Show WireGuard events only" [section="Filters"]
|
||||
flag::define --merged bool "Show all events interleaved" [section="Filters"]
|
||||
|
||||
help::section "Output"
|
||||
flag::define --limit value "Max results per source" [default=50, type=int, min=1, section="Output"]
|
||||
flag::define --ascending bool "Sort ascending" [section="Output"]
|
||||
flag::define --descending bool "Sort descending" [section="Output"]
|
||||
flag::define --resolved bool "Resolve endpoints" [section="Output"]
|
||||
flag::define --detailed bool "Show per-event detail" [section="Output"]
|
||||
flag::define --raw bool "Skip service resolution" [section="Output"]
|
||||
flag::define --follow bool "Follow live" [section="Output"]
|
||||
|
||||
flag::exclusive --fw --wg
|
||||
flag::exclusive --ascending --descending
|
||||
}
|
||||
|
||||
function cmd::logs::show::run() {
|
||||
flag::parse "$@" || return 1
|
||||
|
||||
local name; name=$(flag::value --name)
|
||||
local type; type=$(flag::value --type)
|
||||
local limit; limit=$(flag::value --limit)
|
||||
local since; since=$(flag::value --since)
|
||||
local filter_service; filter_service=$(flag::value --service)
|
||||
local filter_event; filter_event=$(flag::value --event)
|
||||
local net_file=""
|
||||
|
||||
local fw_only=false wg_only=false merged=false
|
||||
local follow=false raw=false resolved=false detailed=false
|
||||
flag::bool --fw && fw_only=true
|
||||
flag::bool --wg && wg_only=true
|
||||
flag::bool --merged && merged=true
|
||||
flag::bool --follow && follow=true
|
||||
flag::bool --raw && raw=true
|
||||
flag::bool --resolved && resolved=true
|
||||
flag::bool --detailed && detailed=true
|
||||
|
||||
local sort_order="desc"
|
||||
flag::bool --ascending && sort_order="asc"
|
||||
flag::bool --descending && sort_order="desc"
|
||||
|
||||
local collapse=1
|
||||
$detailed && collapse=0
|
||||
|
||||
if [[ -n "$name" && -n "$type" ]]; then
|
||||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||
fi
|
||||
|
||||
local filter_ip=""
|
||||
if [[ -n "$name" ]]; then
|
||||
filter_ip=$(peers::get_ip "$name")
|
||||
[[ -z "$filter_ip" ]] && log::error "Could not find IP for: $name" && return 1
|
||||
fi
|
||||
|
||||
$fw_only && $wg_only && fw_only=false && wg_only=false
|
||||
|
||||
if $follow; then
|
||||
cmd::logs::follow "$filter_ip" "$name" "$type" "$fw_only" "$wg_only"
|
||||
return
|
||||
fi
|
||||
|
||||
$raw || net_file="$(ctx::net)"
|
||||
|
||||
local filter_dest_ip="" filter_dest_port=""
|
||||
if [[ -n "$filter_service" ]]; then
|
||||
if [[ "$filter_service" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(:[0-9]+)?$ ]]; then
|
||||
filter_dest_ip="${filter_service%%:*}"
|
||||
local maybe_port="${filter_service##*:}"
|
||||
[[ "$maybe_port" != "$filter_dest_ip" ]] && filter_dest_port="$maybe_port"
|
||||
else
|
||||
local svc_resolved
|
||||
svc_resolved=$(net::resolve "$filter_service" 2>/dev/null | head -1)
|
||||
if [[ -n "$svc_resolved" ]]; then
|
||||
filter_dest_ip="${svc_resolved%%:*}"
|
||||
local rest="${svc_resolved#*:}"
|
||||
[[ "$rest" != "$filter_dest_ip" ]] && filter_dest_port="${rest%%:*}"
|
||||
else
|
||||
log::error "Service not found: ${filter_service}"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if $merged; then
|
||||
log::section "WireGuard Activity Log"
|
||||
printf "\n"
|
||||
cmd::logs::show_merged "$filter_ip" "$name" "$type" "$limit" "$net_file" "$since"
|
||||
return
|
||||
fi
|
||||
|
||||
local fw_output="" wg_output=""
|
||||
|
||||
$wg_only || fw_output=$(cmd::logs::show_fw_events \
|
||||
"$filter_ip" "$name" "$type" "$limit" "$net_file" \
|
||||
"$collapse" "$since" "$filter_dest_ip" "$filter_dest_port" "$sort_order" "$resolved")
|
||||
|
||||
$fw_only || wg_output=$(cmd::logs::show_wg_events \
|
||||
"$filter_ip" "$name" "$type" "$limit" \
|
||||
"$collapse" "$since" "$filter_event" "$sort_order" "$resolved")
|
||||
|
||||
if [[ -z "$(echo "$fw_output" | tr -d '[:space:]')" && \
|
||||
-z "$(echo "$wg_output" | tr -d '[:space:]')" ]]; then
|
||||
log::wg_warning "No logs found"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log::section "WireGuard Activity Log"
|
||||
printf "\n"
|
||||
|
||||
if [[ -n "$fw_output" && -n "$wg_output" ]]; then
|
||||
printf "%s\n\n" "$fw_output"
|
||||
printf "%s\n" "$wg_output"
|
||||
elif [[ -n "$fw_output" ]]; then
|
||||
printf "%s\n" "$fw_output"
|
||||
else
|
||||
printf "%s\n" "$wg_output"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function cmd::logs::show_fw_events() {
|
||||
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" \
|
||||
limit="${4:-50}" net_file="${5:-}" collapse="${6:-1}" \
|
||||
since="${7:-}" filter_dest_ip="${8:-}" filter_dest_port="${9:-}" \
|
||||
sort_order="${10:-desc}" resolved_only="${11:-false}"
|
||||
|
||||
[[ ! -f "$FW_EVENTS_LOG" ]] && return 0
|
||||
|
||||
local data
|
||||
data=$(json::fw_events \
|
||||
"$FW_EVENTS_LOG" "$filter_ip" "$filter_type" \
|
||||
"$(ctx::clients)" "${net_file:-}" \
|
||||
"$limit" "$collapse" "$since" \
|
||||
"$filter_dest_ip" "$filter_dest_port" \
|
||||
"$sort_order" \
|
||||
2>/dev/null)
|
||||
|
||||
[[ -z "$data" ]] && return 0
|
||||
|
||||
# ── Collect unique endpoints for batch resolution ──
|
||||
local -a ep_list=()
|
||||
while IFS='|' read -r ts client dest_ip dest_port proto svc count src_endpoint; do
|
||||
[[ -z "$ts" || -z "$src_endpoint" ]] && continue
|
||||
ep_list+=("$src_endpoint")
|
||||
done <<< "$data"
|
||||
|
||||
declare -A resolve_cache=()
|
||||
if [[ ${#ep_list[@]} -gt 0 ]]; then
|
||||
while IFS='|' read -r ip name; do
|
||||
[[ -n "$ip" ]] && resolve_cache["$ip"]="$name"
|
||||
done < <(json::batch_resolve "${ep_list[@]}" 2>/dev/null)
|
||||
fi
|
||||
|
||||
# ── Pass 1: measure widths ──
|
||||
local w_client=16 w_dest=20 w_endpoint=0
|
||||
local resolved_data=""
|
||||
|
||||
while IFS='|' read -r ts client dest_ip dest_port proto svc count src_endpoint; do
|
||||
[[ -z "$ts" ]] && continue
|
||||
|
||||
(( ${#client} > w_client )) && w_client=${#client}
|
||||
|
||||
local svc_display=""
|
||||
if [[ -n "$svc" ]]; then
|
||||
[[ -n "$dest_port" ]] && svc_display="${svc}/${proto}" \
|
||||
|| svc_display="${svc} (${proto})"
|
||||
else
|
||||
[[ -n "$dest_port" ]] && svc_display="${dest_ip}:${dest_port}/${proto}" \
|
||||
|| svc_display="${dest_ip} (${proto})"
|
||||
fi
|
||||
|
||||
local measure_len
|
||||
if $resolved_only; then
|
||||
measure_len=${#svc_display}
|
||||
else
|
||||
local raw_plain=""
|
||||
[[ -n "$svc" && -n "$dest_port" ]] && raw_plain=" (${dest_ip}:${dest_port})"
|
||||
[[ -n "$svc" && -z "$dest_port" ]] && raw_plain=" (${dest_ip})"
|
||||
measure_len=$(( ${#svc_display} + ${#raw_plain} ))
|
||||
fi
|
||||
(( measure_len > w_dest )) && w_dest=$measure_len
|
||||
|
||||
local src_resolved=""
|
||||
if [[ -n "$src_endpoint" ]]; then
|
||||
src_resolved="${resolve_cache[$src_endpoint]:-}"
|
||||
[[ "$src_resolved" == "$src_endpoint" ]] && src_resolved=""
|
||||
|
||||
local ep_measure_len
|
||||
if $resolved_only; then
|
||||
ep_measure_len=${#src_resolved}
|
||||
[[ -z "$src_resolved" ]] && ep_measure_len=${#src_endpoint}
|
||||
else
|
||||
ep_measure_len=${#src_endpoint}
|
||||
[[ -n "$src_resolved" ]] && \
|
||||
ep_measure_len=$(( ${#src_endpoint} + 4 + ${#src_resolved} ))
|
||||
fi
|
||||
(( ep_measure_len > w_endpoint )) && w_endpoint=$ep_measure_len
|
||||
fi
|
||||
|
||||
resolved_data+="${ts}|${client}|${dest_ip}|${dest_port}|${proto}|${svc}|${count}|${src_endpoint}|${src_resolved}"$'\n'
|
||||
done <<< "$data"
|
||||
|
||||
(( w_client += 2 ))
|
||||
(( w_dest += 2 ))
|
||||
[[ "$w_endpoint" -gt 0 ]] && (( w_endpoint += 2 ))
|
||||
|
||||
# ── Pass 2: render ──
|
||||
ui::logs::fw_section_header
|
||||
while IFS='|' read -r ts client dest_ip dest_port proto svc count src_endpoint src_resolved; do
|
||||
[[ -z "$ts" ]] && continue
|
||||
ui::logs::fw_row "$ts" "$client" "$dest_ip" "$dest_port" \
|
||||
"$proto" "$svc" "$count" "$w_client" "$w_dest" \
|
||||
"$src_endpoint" "$src_resolved" "$w_endpoint" "$resolved_only"
|
||||
done <<< "$resolved_data"
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
function cmd::logs::show_wg_events() {
|
||||
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" \
|
||||
limit="${4:-50}" collapse="${5:-1}" \
|
||||
since="${6:-}" filter_event="${7:-}" sort_order="${8:-desc}" \
|
||||
resolved_only="${9:-false}"
|
||||
|
||||
[[ ! -f "$WG_EVENTS_LOG" ]] && return 0
|
||||
|
||||
local data
|
||||
data=$(json::wg_events \
|
||||
"$WG_EVENTS_LOG" "$filter_name" "$filter_type" \
|
||||
"$limit" "$collapse" "$since" "$filter_event" \
|
||||
"$(ctx::endpoint_cache)" "$sort_order" \
|
||||
2>/dev/null)
|
||||
|
||||
[[ -z "$data" ]] && return 0
|
||||
|
||||
# ── Collect unique endpoints for batch resolution ──
|
||||
local -a ep_list=()
|
||||
while IFS='|' read -r ts client endpoint event count gap_seconds; do
|
||||
[[ -z "$ts" || -z "$endpoint" ]] && continue
|
||||
ep_list+=("$endpoint")
|
||||
done <<< "$data"
|
||||
|
||||
declare -A resolve_cache=()
|
||||
if [[ ${#ep_list[@]} -gt 0 ]]; then
|
||||
while IFS='|' read -r ip name; do
|
||||
[[ -n "$ip" ]] && resolve_cache["$ip"]="$name"
|
||||
done < <(json::batch_resolve "${ep_list[@]}" 2>/dev/null)
|
||||
fi
|
||||
|
||||
# ── Measure widths ──
|
||||
local w_client=16 w_endpoint=16
|
||||
local resolved_data=""
|
||||
|
||||
while IFS='|' read -r ts client endpoint event count gap_seconds; do
|
||||
[[ -z "$ts" ]] && continue
|
||||
(( ${#client} > w_client )) && w_client=${#client}
|
||||
|
||||
local resolved=""
|
||||
if [[ -n "$endpoint" ]]; then
|
||||
resolved="${resolve_cache[$endpoint]:-}"
|
||||
[[ "$resolved" == "$endpoint" ]] && resolved=""
|
||||
fi
|
||||
|
||||
local ep_raw="${endpoint:--}"
|
||||
local ep_measure_len
|
||||
if $resolved_only; then
|
||||
local ep_display="${resolved:-$endpoint}"
|
||||
[[ -z "$ep_display" ]] && ep_display="-"
|
||||
ep_measure_len=${#ep_display}
|
||||
else
|
||||
ep_measure_len=${#ep_raw}
|
||||
[[ -n "$resolved" && -n "$endpoint" ]] && \
|
||||
ep_measure_len=$(( ${#endpoint} + 4 + ${#resolved} ))
|
||||
fi
|
||||
(( ep_measure_len > w_endpoint )) && w_endpoint=$ep_measure_len
|
||||
|
||||
resolved_data+="${ts}|${client}|${endpoint}|${event}|${count}|${gap_seconds}|${resolved}"$'\n'
|
||||
done <<< "$data"
|
||||
|
||||
(( w_client += 2 ))
|
||||
(( w_endpoint += 2 ))
|
||||
|
||||
# ── Render ──
|
||||
ui::logs::wg_section_header
|
||||
while IFS='|' read -r ts client endpoint event count gap_seconds resolved; do
|
||||
[[ -z "$ts" ]] && continue
|
||||
if $resolved_only; then
|
||||
local ep_display="${resolved:-$endpoint}"
|
||||
[[ -z "$ep_display" ]] && ep_display="-"
|
||||
ui::logs::wg_row "$ts" "$client" "$ep_display" "$event" \
|
||||
"$count" "$w_client" "$w_endpoint" "$gap_seconds" ""
|
||||
else
|
||||
ui::logs::wg_row "$ts" "$client" "$endpoint" "$event" \
|
||||
"$count" "$w_client" "$w_endpoint" "$gap_seconds" "$resolved"
|
||||
fi
|
||||
done <<< "$resolved_data"
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
function cmd::logs::show_merged() {
|
||||
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" \
|
||||
limit="${4:-50}" net_file="${5:-}" since="${6:-}"
|
||||
|
||||
local fw_data wg_data
|
||||
fw_data=$(json::fw_events \
|
||||
"$FW_EVENTS_LOG" "$filter_ip" "$filter_type" \
|
||||
"$(ctx::clients)" "${net_file:-}" \
|
||||
"$limit" "1" "$since" "" "" \
|
||||
2>/dev/null)
|
||||
wg_data=$(json::wg_events \
|
||||
"$WG_EVENTS_LOG" "$filter_name" "$filter_type" \
|
||||
"$limit" "1" "$since" "" \
|
||||
2>/dev/null)
|
||||
|
||||
local w_client=16 w_dest=20
|
||||
while IFS='|' read -r ts client rest; do
|
||||
[[ -z "$ts" ]] && continue
|
||||
(( ${#client} > w_client )) && w_client=${#client}
|
||||
done < <(echo "$fw_data"; echo "$wg_data")
|
||||
(( w_client += 2 ))
|
||||
|
||||
local merged_data
|
||||
merged_data=$(
|
||||
while IFS='|' read -r ts client dest_ip dest_port proto svc count; do
|
||||
[[ -z "$ts" ]] && continue
|
||||
echo "fw|${ts}|${client}|${dest_ip}|${dest_port}|${proto}|${svc}|${count}"
|
||||
done <<< "$fw_data"
|
||||
while IFS='|' read -r ts client endpoint event count; do
|
||||
[[ -z "$ts" ]] && continue
|
||||
echo "wg|${ts}|${client}|${endpoint}|${event}|${count}"
|
||||
done <<< "$wg_data"
|
||||
)
|
||||
|
||||
while IFS='|' read -r source ts rest; do
|
||||
[[ -z "$source" ]] && continue
|
||||
case "$source" in
|
||||
fw)
|
||||
IFS='|' read -r client dest_ip dest_port proto svc count <<< "$rest"
|
||||
local dest_display
|
||||
if [[ -n "$svc" ]]; then
|
||||
[[ -n "$dest_port" ]] && dest_display="${svc}/${proto}" || dest_display="${svc} (${proto})"
|
||||
else
|
||||
[[ -n "$dest_port" ]] && dest_display="${dest_ip}:${dest_port}/${proto}" || dest_display="${dest_ip} (${proto})"
|
||||
fi
|
||||
(( ${#dest_display} > w_dest )) && w_dest=${#dest_display}
|
||||
;;
|
||||
esac
|
||||
done <<< "$merged_data"
|
||||
(( w_dest += 2 ))
|
||||
|
||||
while IFS='|' read -r source ts rest; do
|
||||
[[ -z "$source" ]] && continue
|
||||
case "$source" in
|
||||
fw)
|
||||
IFS='|' read -r client dest_ip dest_port proto svc count <<< "$rest"
|
||||
ui::watch::fw_row "$ts" "$client" \
|
||||
"$(ui::logs::build_dest "$dest_ip" "$dest_port" "$proto" "$svc")" \
|
||||
"$w_client" "$w_dest"
|
||||
;;
|
||||
wg)
|
||||
IFS='|' read -r client endpoint event count <<< "$rest"
|
||||
ui::watch::wg_row "$ts" "$client" "$endpoint" "$event" \
|
||||
"$w_client" "$w_dest"
|
||||
;;
|
||||
esac
|
||||
done < <(echo "$merged_data" | sort -t'|' -k2,2)
|
||||
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
function cmd::logs::follow() {
|
||||
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}"
|
||||
local fw_only="${4:-false}" wg_only="${5:-false}"
|
||||
|
||||
log::section "WireGuard Live Log (Ctrl+C to stop)"
|
||||
printf "\n"
|
||||
|
||||
local restricted_only=false blocked_only=false
|
||||
$fw_only && restricted_only=true
|
||||
$wg_only && blocked_only=true
|
||||
|
||||
monitor::live "$filter_name" "$filter_type" "" \
|
||||
"$blocked_only" "$restricted_only" "false" "false"
|
||||
}
|
||||
|
||||
function cmd::logs::remove() {
|
||||
local name="" type="" before="" force=false
|
||||
local fw_only=false wg_only=false all=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--type) type="$2"; shift 2 ;;
|
||||
--before) before="$2"; shift 2 ;;
|
||||
--fw) fw_only=true; shift ;;
|
||||
--wg) wg_only=true; shift ;;
|
||||
--all) all=true; shift ;;
|
||||
--force) force=true; shift ;;
|
||||
--help) cmd::logs::help; return ;;
|
||||
*)
|
||||
log::error "Unknown flag: $1"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! $all && [[ -z "$name" && -z "$before" ]]; then
|
||||
log::error "Specify --name, --before, or --all"
|
||||
cmd::logs::help
|
||||
return 1
|
||||
fi
|
||||
|
||||
local filter_ip=""
|
||||
if [[ -n "$name" ]]; then
|
||||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||
filter_ip=$(peers::get_ip "$name")
|
||||
fi
|
||||
|
||||
local desc=""
|
||||
$all && desc="all entries"
|
||||
[[ -n "$name" ]] && desc="entries for '${name}'"
|
||||
[[ -n "$before" ]] && desc="${desc:+$desc, }entries older than ${before} days"
|
||||
$fw_only && desc="${desc} (fw only)"
|
||||
$wg_only && desc="${desc} (wg only)"
|
||||
|
||||
if ! $force; then
|
||||
read -r -p "Remove ${desc}? [y/N] " confirm
|
||||
case "$confirm" in
|
||||
[yY]*) ;;
|
||||
*) log::info "Aborted"; return 0 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
local result
|
||||
result=$(json::remove_events_filtered \
|
||||
"$WG_EVENTS_LOG" "$FW_EVENTS_LOG" \
|
||||
"${name:-}" "${filter_ip:-}" \
|
||||
"$fw_only" "$wg_only" \
|
||||
"${before:-}")
|
||||
|
||||
local removed_wg removed_fw
|
||||
IFS="|" read -r removed_wg removed_fw <<< "$result"
|
||||
local total=$(( removed_wg + removed_fw ))
|
||||
|
||||
if [[ "$total" -eq 0 ]]; then
|
||||
log::wg_warning "No log entries found matching the criteria"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log::wg_success "Removed ${total} log entries (wg: ${removed_wg}, fw: ${removed_fw})"
|
||||
}
|
||||
|
||||
function cmd::logs::rotate() {
|
||||
local days=7 force=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--days) days="$2"; shift 2 ;;
|
||||
--force) force=true; shift ;;
|
||||
--help) cmd::logs::help; return ;;
|
||||
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
$force || {
|
||||
read -r -p "Remove log entries older than ${days} days? [y/N] " confirm
|
||||
case "$confirm" in
|
||||
[yY]*) ;;
|
||||
*) log::info "Aborted"; return 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
local result
|
||||
result=$(json::remove_events_filtered \
|
||||
"$WG_EVENTS_LOG" "$FW_EVENTS_LOG" \
|
||||
"" "" "false" "false" "$days")
|
||||
|
||||
local removed_wg removed_fw
|
||||
IFS="|" read -r removed_wg removed_fw <<< "$result"
|
||||
local total=$(( removed_wg + removed_fw ))
|
||||
|
||||
if [[ "$total" -eq 0 ]]; then
|
||||
log::wg_warning "No log entries older than ${days} days"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log::wg_success "Rotated ${total} entries older than ${days} days (wg: ${removed_wg}, fw: ${removed_fw})"
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@ function ctx::endpoint_cache() { echo "${_CTX_DAEMON}/endpoint_cache.json";
|
|||
function ctx::accept_events_log() { echo "${_CTX_DAEMON}/accept_events.log"; }
|
||||
|
||||
# Tool paths
|
||||
function ctx::json_helper() { echo "${_CTX_WGCTL}/core/json_helper.py"; }
|
||||
function ctx::json_helper() { echo "$(ctx::app)/json_helper.py"; }
|
||||
function ctx::monitor_script() { echo "${_CTX_WGCTL}/daemon/wgctl-monitor.py"; }
|
||||
function ctx::lib() { echo "${_CTX_WGCTL}/core/lib"; }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
JSON_HELPER="$(ctx::app)/json_helper.py"
|
||||
# echo "JSON_HELPER: $JSON_HELPER"
|
||||
JSON_HELPER="$(ctx::json_helper)"
|
||||
|
||||
function json::get() { python3 "$JSON_HELPER" get "$@" </dev/null; }
|
||||
function json::set() { python3 "$JSON_HELPER" set "$@" </dev/null; }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,264 @@
|
|||
#!/usr/bin/env bash
|
||||
# core/framework/command.sh
|
||||
#
|
||||
# Command definition, routing, and lazy loading.
|
||||
#
|
||||
# Usage in on_load:
|
||||
# command::define show "Show logs" [*]
|
||||
# command::define clean "Remove keepalives" [c, dump]
|
||||
# command::define watch "Watch live" [w, follow]
|
||||
#
|
||||
# Routing:
|
||||
# wgctl logs → cmd::logs::show::run (default)
|
||||
# wgctl logs clean → cmd::logs::clean::run
|
||||
# wgctl logs c → cmd::logs::clean::run (alias)
|
||||
# wgctl logs --fw → cmd::logs::show::run (default, flag passed through)
|
||||
|
||||
# ── Storage ───────────────────────────────────────────────────────────────────
|
||||
|
||||
# Command definitions: "cmd:subcmd" → "desc|aliases|is_default"
|
||||
declare -gA _COMMAND_DEFS=()
|
||||
|
||||
# Alias map: "cmd:alias" → "subcmd"
|
||||
declare -gA _COMMAND_ALIASES_MAP=()
|
||||
|
||||
# Default subcommand per command: "cmd" → "subcmd"
|
||||
declare -gA _COMMAND_DEFAULT=()
|
||||
|
||||
# Currently loading command context (set by load_command)
|
||||
declare -g _CURRENT_LOADING_CMD=""
|
||||
|
||||
# Currently executing command context (set by command::run)
|
||||
declare -g _CURRENT_COMMAND=""
|
||||
|
||||
# ── Definition ────────────────────────────────────────────────────────────────
|
||||
|
||||
# command::define <subcmd> "description" [*] or [alias1, alias2]
|
||||
# [*] marks as default subcommand
|
||||
function command::define() {
|
||||
local subcmd="${1:-}"
|
||||
local desc="${2:-}"
|
||||
local meta="${3:-}" # [*] or [alias1, alias2] or [*, alias1]
|
||||
local cmd="${_CURRENT_LOADING_CMD:-}"
|
||||
|
||||
[[ -z "$cmd" || -z "$subcmd" ]] && return 1
|
||||
|
||||
local is_default=false
|
||||
local aliases=""
|
||||
|
||||
if [[ -n "$meta" ]]; then
|
||||
# Strip brackets
|
||||
local inner="${meta#[}"
|
||||
inner="${inner%]}"
|
||||
|
||||
# Check for * (default marker)
|
||||
if [[ "$inner" == *"*"* ]]; then
|
||||
is_default=true
|
||||
inner="${inner//\*/}"
|
||||
inner="${inner//,/}"
|
||||
inner="${inner// /}"
|
||||
fi
|
||||
|
||||
# Remaining are aliases
|
||||
aliases="$inner"
|
||||
# Clean up leading/trailing commas and spaces
|
||||
aliases=$(echo "$aliases" | sed 's/^[, ]*//' | sed 's/[, ]*$//' | tr -s ' ,')
|
||||
fi
|
||||
|
||||
# Store definition: desc|aliases|is_default
|
||||
_COMMAND_DEFS["${cmd}:${subcmd}"]="${desc}|${aliases}|${is_default}"
|
||||
|
||||
# Register as default
|
||||
if $is_default; then
|
||||
_COMMAND_DEFAULT["$cmd"]="$subcmd"
|
||||
fi
|
||||
|
||||
# Register aliases
|
||||
if [[ -n "$aliases" ]]; then
|
||||
local alias
|
||||
# Split aliases on comma or space
|
||||
while IFS=',' read -ra alias_list; do
|
||||
for alias in "${alias_list[@]}"; do
|
||||
alias="${alias// /}"
|
||||
[[ -n "$alias" ]] && _COMMAND_ALIASES_MAP["${cmd}:${alias}"]="$subcmd"
|
||||
done
|
||||
done <<< "$aliases"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Routing ───────────────────────────────────────────────────────────────────
|
||||
|
||||
# command::route <cmd> <args...>
|
||||
# Determines which subcommand to run and routes to it.
|
||||
# Returns: sets _ROUTED_SUBCMD and _ROUTED_ARGS
|
||||
declare -g _ROUTED_SUBCMD=""
|
||||
declare -ga _ROUTED_ARGS=()
|
||||
|
||||
function command::route() {
|
||||
local cmd="$1"
|
||||
shift
|
||||
local -a args=("$@")
|
||||
|
||||
_ROUTED_SUBCMD=""
|
||||
_ROUTED_ARGS=()
|
||||
|
||||
# Check if _COMMAND_DEFS has any entries for this cmd
|
||||
local has_defs=false
|
||||
local key
|
||||
for key in "${!_COMMAND_DEFS[@]}"; do
|
||||
[[ "$key" == "${cmd}:"* ]] && has_defs=true && break
|
||||
done
|
||||
|
||||
if ! $has_defs; then
|
||||
# No subcommands defined — run directly
|
||||
_ROUTED_SUBCMD=""
|
||||
_ROUTED_ARGS=("${args[@]:-}")
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Find first non-flag arg
|
||||
local first_nonflag="" first_nonflag_idx=-1
|
||||
local i
|
||||
for (( i=0; i<${#args[@]}; i++ )); do
|
||||
if [[ "${args[$i]}" != --* && "${args[$i]}" != -* ]]; then
|
||||
first_nonflag="${args[$i]}"
|
||||
first_nonflag_idx=$i
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
local matched_subcmd=""
|
||||
|
||||
if [[ -n "$first_nonflag" ]]; then
|
||||
# Check direct match
|
||||
if [[ -n "${_COMMAND_DEFS[${cmd}:${first_nonflag}]+x}" ]]; then
|
||||
matched_subcmd="$first_nonflag"
|
||||
# Check alias
|
||||
elif [[ -n "${_COMMAND_ALIASES_MAP[${cmd}:${first_nonflag}]+x}" ]]; then
|
||||
matched_subcmd="${_COMMAND_ALIASES_MAP[${cmd}:${first_nonflag}]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$matched_subcmd" ]]; then
|
||||
# Remove matched subcmd from args
|
||||
local -a new_args=()
|
||||
for (( i=0; i<${#args[@]}; i++ )); do
|
||||
[[ $i -eq $first_nonflag_idx ]] && continue
|
||||
new_args+=("${args[$i]}")
|
||||
done
|
||||
_ROUTED_SUBCMD="$matched_subcmd"
|
||||
_ROUTED_ARGS=("${new_args[@]:-}")
|
||||
else
|
||||
# Use default subcommand
|
||||
local default_subcmd="${_COMMAND_DEFAULT[$cmd]:-}"
|
||||
_ROUTED_SUBCMD="$default_subcmd"
|
||||
_ROUTED_ARGS=("${args[@]:-}")
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Loading ───────────────────────────────────────────────────────────────────
|
||||
|
||||
# command::load_subcmd <cmd> <subcmd>
|
||||
# Lazy-loads a subcommand's file and calls its on_load
|
||||
function command::load_subcmd() {
|
||||
local cmd="$1" subcmd="$2"
|
||||
[[ -z "$cmd" || -z "$subcmd" ]] && return 1
|
||||
|
||||
# Determine file path — commands/<cmd>/<subcmd>.sh
|
||||
local cmd_dir
|
||||
cmd_dir="${WGCTL_DIR}/commands/${cmd}"
|
||||
|
||||
local subcmd_file="${cmd_dir}/${subcmd}.sh"
|
||||
[[ ! -f "$subcmd_file" ]] && return 1
|
||||
|
||||
_CURRENT_LOADING_CMD="${cmd}::${subcmd}"
|
||||
_CURRENT_COMMAND="${cmd}::${subcmd}"
|
||||
source "$subcmd_file"
|
||||
|
||||
local on_load_fn="cmd::${cmd}::${subcmd}::on_load"
|
||||
if declare -f "$on_load_fn" &>/dev/null; then
|
||||
"$on_load_fn"
|
||||
fi
|
||||
|
||||
_CURRENT_LOADING_CMD=""
|
||||
}
|
||||
|
||||
# ── Run ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
# command::run_routed <cmd> <subcmd> <args...>
|
||||
# Runs a subcommand after routing is resolved.
|
||||
function command::run_routed() {
|
||||
local cmd="$1" subcmd="$2"
|
||||
shift 2
|
||||
local -a args=("$@")
|
||||
|
||||
_CURRENT_COMMAND="${cmd}::${subcmd}"
|
||||
|
||||
# Apply command defaults (only for default subcommand)
|
||||
local default_subcmd="${_COMMAND_DEFAULT[$cmd]:-}"
|
||||
local -a default_args=()
|
||||
if [[ "$subcmd" == "$default_subcmd" || -z "$default_subcmd" ]]; then
|
||||
local defaults="${_COMMAND_DEFAULTS[$cmd]:-}"
|
||||
if [[ -n "$defaults" ]]; then
|
||||
read -ra default_args <<< "$defaults"
|
||||
fi
|
||||
fi
|
||||
|
||||
local -a user_args=()
|
||||
[[ ${#args[@]} -gt 0 ]] && user_args=("${args[@]}")
|
||||
|
||||
# Resolve exclusive group conflicts
|
||||
local groups="${_FLAG_EXCLUSIVE_GROUPS[$cmd]:-}"
|
||||
if [[ -n "$groups" && ${#default_args[@]} -gt 0 && ${#user_args[@]} -gt 0 ]]; then
|
||||
command::_resolve_conflicts default_args user_args "$groups"
|
||||
fi
|
||||
|
||||
# Combine args
|
||||
local -a final_args=()
|
||||
for _d in "${default_args[@]:-}"; do
|
||||
[[ -n "$_d" ]] && final_args+=("$_d")
|
||||
done
|
||||
for _u in "${user_args[@]:-}"; do
|
||||
[[ -n "$_u" ]] && final_args+=("$_u")
|
||||
done
|
||||
|
||||
# Preprocess mixin flags (--json, --no-color etc)
|
||||
command::_preprocess_flags final_args
|
||||
|
||||
# Run
|
||||
local run_fn="cmd::${cmd}::${subcmd}::run"
|
||||
if declare -f "$run_fn" &>/dev/null; then
|
||||
if [[ ${#final_args[@]} -gt 0 ]]; then
|
||||
"$run_fn" "${final_args[@]}"
|
||||
else
|
||||
"$run_fn"
|
||||
fi
|
||||
else
|
||||
log::error "Run function not found: ${run_fn}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
# command::has_subcmds <cmd>
|
||||
# Returns 0 if command has defined subcommands
|
||||
function command::has_subcmds() {
|
||||
local cmd="$1" key
|
||||
for key in "${!_COMMAND_DEFS[@]}"; do
|
||||
[[ "$key" == "${cmd}:"* ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# command::subcmds <cmd>
|
||||
# Prints list of subcommand names
|
||||
function command::subcmds() {
|
||||
local cmd="$1" key
|
||||
for key in "${!_COMMAND_DEFS[@]}"; do
|
||||
[[ "$key" == "${cmd}:"* ]] && echo "${key#${cmd}:}"
|
||||
done
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Command Registry
|
||||
|
|
@ -31,68 +291,73 @@ function command::fn() {
|
|||
|
||||
function command::has_function() { declare -F "$(command::fn "$1" "$2")" >/dev/null 2>&1; }
|
||||
function command::is_auto_load() { declare -F "$(command::fn "$1" on_load)" >/dev/null 2>&1; }
|
||||
function command::exists() { command::has_function "$1" run; }
|
||||
function command::exists() {
|
||||
local name="$1"
|
||||
# New-style: has subcommands defined
|
||||
command::has_subcmds "$name" && return 0
|
||||
# Legacy: has cmd::name::run function
|
||||
declare -f "$(command::fn "$name" run)" &>/dev/null
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Runner
|
||||
# ============================================
|
||||
|
||||
# function command::run() {
|
||||
# local cmd="$1"
|
||||
# shift
|
||||
|
||||
# command::_reset_mixin_state # reset values only, keep _ACTIVE_MIXINS
|
||||
|
||||
# local -a args=("$@")
|
||||
# command::_preprocess_flags args
|
||||
|
||||
# local fn
|
||||
# fn=$(command::fn "$cmd" run)
|
||||
# core::call_function "$fn" ${args[@]+"${args[@]}"}
|
||||
# }
|
||||
|
||||
function command::run() {
|
||||
local cmd="$1"
|
||||
shift
|
||||
|
||||
command::_reset_mixin_state
|
||||
|
||||
# Build default args from config
|
||||
# Check if this command uses new directory-based routing
|
||||
if command::has_subcmds "$cmd"; then
|
||||
# Route to subcommand
|
||||
command::route "$cmd" "$@"
|
||||
local subcmd="$_ROUTED_SUBCMD"
|
||||
local -a routed_args=("${_ROUTED_ARGS[@]:-}")
|
||||
|
||||
# Lazy load subcommand file
|
||||
local subcmd_file="$(ctx::commands)/${cmd}/${subcmd}.sh"
|
||||
if [[ -f "$subcmd_file" ]]; then
|
||||
_CURRENT_LOADING_CMD="${cmd}::${subcmd}"
|
||||
_CURRENT_COMMAND="${cmd}::${subcmd}"
|
||||
source "$subcmd_file"
|
||||
core::call_if_exists "cmd::${cmd}::${subcmd}::on_load"
|
||||
_CURRENT_LOADING_CMD=""
|
||||
fi
|
||||
|
||||
command::run_routed "$cmd" "$subcmd" "${routed_args[@]:-}"
|
||||
return $?
|
||||
fi
|
||||
|
||||
# Legacy path — existing command::run logic
|
||||
local -a default_args=()
|
||||
local defaults="${_COMMAND_DEFAULTS[$cmd]:-}"
|
||||
if [[ -n "$defaults" ]]; then
|
||||
read -ra default_args <<< "$defaults"
|
||||
fi
|
||||
|
||||
local -a user_args=("$@")
|
||||
local -a user_args=()
|
||||
[[ $# -gt 0 ]] && user_args=("$@")
|
||||
|
||||
# Resolve exclusive group conflicts — user args override defaults
|
||||
local groups="${_FLAG_EXCLUSIVE_GROUPS[$cmd]:-}"
|
||||
if [[ -n "$groups" && ${#default_args[@]} -gt 0 && ${#user_args[@]} -gt 0 ]]; then
|
||||
command::_resolve_conflicts default_args user_args "$groups"
|
||||
fi
|
||||
|
||||
local -a cleaned_defaults=()
|
||||
for _d in "${default_args[@]:-}"; do
|
||||
[[ -n "$_d" ]] && cleaned_defaults+=("$_d")
|
||||
done
|
||||
default_args=("${cleaned_defaults[@]:-}")
|
||||
|
||||
local -a args=()
|
||||
for _d in "${default_args[@]:-}"; do
|
||||
[[ -n "$_d" ]] && args+=("$_d")
|
||||
done
|
||||
for _u in "${user_args[@]:-}"; do
|
||||
[[ -n "$_u" ]] && args+=("$_u")
|
||||
done
|
||||
for _d in "${default_args[@]:-}"; do [[ -n "$_d" ]] && args+=("$_d"); done
|
||||
for _u in "${user_args[@]:-}"; do [[ -n "$_u" ]] && args+=("$_u"); done
|
||||
|
||||
# Preprocess mixin flags (--json, --no-color etc)
|
||||
command::_preprocess_flags args
|
||||
|
||||
local fn
|
||||
fn=$(command::fn "$cmd" run)
|
||||
core::call_function "$fn" ${args[@]+"${args[@]}"}
|
||||
if [[ ${#args[@]} -gt 0 ]]; then
|
||||
core::call_function "$fn" "${args[@]}"
|
||||
else
|
||||
core::call_function "$fn"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -108,23 +373,33 @@ function core::call_function() {
|
|||
|
||||
function load_command() {
|
||||
local name="$1"
|
||||
|
||||
command::loaded "$name" && return 0
|
||||
|
||||
# Check for new directory-based structure first
|
||||
local cmd_dir
|
||||
cmd_dir="$(ctx::commands)/${name}"
|
||||
local cmd_file="${cmd_dir}/${name}.sh"
|
||||
|
||||
if [[ -d "$cmd_dir" && -f "$cmd_file" ]]; then
|
||||
source "$cmd_file"
|
||||
_LOADED_COMMANDS["$name"]=1
|
||||
_CURRENT_LOADING_CMD="$name"
|
||||
core::call_if_exists "cmd::${name}::on_load"
|
||||
_CURRENT_LOADING_CMD=""
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Fall back to legacy .command.sh
|
||||
local path
|
||||
path="$(ctx::commands)/${name}.command.sh"
|
||||
|
||||
if [[ ! -f "$path" ]]; then
|
||||
log::error "Command not found: ${name} (${path})"
|
||||
return 1
|
||||
fi
|
||||
|
||||
source "$path"
|
||||
_LOADED_COMMANDS["$name"]=1
|
||||
|
||||
_CURRENT_LOADING_CMD="$name"
|
||||
core::call_if_exists "$(command::fn "$name" on_load)"
|
||||
_CURRENT_LOADING_CMD=""
|
||||
|
||||
return 0
|
||||
}
|
||||
|
|
|
|||
130
core/framework/command.sh.bak
Normal file
130
core/framework/command.sh.bak
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# ============================================
|
||||
# Command Registry
|
||||
# ============================================
|
||||
|
||||
declare -A _LOADED_COMMANDS=()
|
||||
|
||||
readonly _COMMAND_NAMESPACE="cmd"
|
||||
readonly _COMMAND_AUTO_LOAD_HOOK="on_load"
|
||||
_CURRENT_LOADING_CMD=""
|
||||
|
||||
# ============================================
|
||||
# Helpers
|
||||
# ============================================
|
||||
|
||||
function command::loaded() { [[ -n "${_LOADED_COMMANDS["$1"]:-}" ]]; }
|
||||
|
||||
# Convert path-style name to namespace
|
||||
# e.g. service/wireguard -> service::wireguard
|
||||
function command::to_namespace() { echo "${1//\//:}"; }
|
||||
|
||||
# Build fully qualified function name
|
||||
# e.g. command::fn "add" "run" -> cmd::add::run
|
||||
# e.g. command::fn "service/wg" "run" -> cmd::service::wg::run
|
||||
function command::fn() {
|
||||
local name namespace
|
||||
namespace=$(command::to_namespace "$1")
|
||||
echo "${_COMMAND_NAMESPACE}::${namespace}::${2}"
|
||||
}
|
||||
|
||||
function command::has_function() { declare -F "$(command::fn "$1" "$2")" >/dev/null 2>&1; }
|
||||
function command::is_auto_load() { declare -F "$(command::fn "$1" on_load)" >/dev/null 2>&1; }
|
||||
function command::exists() { command::has_function "$1" run; }
|
||||
|
||||
# ============================================
|
||||
# Runner
|
||||
# ============================================
|
||||
|
||||
# function command::run() {
|
||||
# local cmd="$1"
|
||||
# shift
|
||||
|
||||
# command::_reset_mixin_state # reset values only, keep _ACTIVE_MIXINS
|
||||
|
||||
# local -a args=("$@")
|
||||
# command::_preprocess_flags args
|
||||
|
||||
# local fn
|
||||
# fn=$(command::fn "$cmd" run)
|
||||
# core::call_function "$fn" ${args[@]+"${args[@]}"}
|
||||
# }
|
||||
|
||||
function command::run() {
|
||||
local cmd="$1"
|
||||
shift
|
||||
|
||||
command::_reset_mixin_state
|
||||
|
||||
# Build default args from config
|
||||
local -a default_args=()
|
||||
local defaults="${_COMMAND_DEFAULTS[$cmd]:-}"
|
||||
if [[ -n "$defaults" ]]; then
|
||||
read -ra default_args <<< "$defaults"
|
||||
fi
|
||||
|
||||
local -a user_args=("$@")
|
||||
[[ $# -gt 0 ]] && user_args=("$@")
|
||||
|
||||
# Resolve exclusive group conflicts — user args override defaults
|
||||
local groups="${_FLAG_EXCLUSIVE_GROUPS[$cmd]:-}"
|
||||
if [[ -n "$groups" && ${#default_args[@]} -gt 0 && ${#user_args[@]} -gt 0 ]]; then
|
||||
command::_resolve_conflicts default_args user_args "$groups"
|
||||
fi
|
||||
|
||||
local -a cleaned_defaults=()
|
||||
for _d in "${default_args[@]:-}"; do
|
||||
[[ -n "$_d" ]] && cleaned_defaults+=("$_d")
|
||||
done
|
||||
default_args=("${cleaned_defaults[@]:-}")
|
||||
|
||||
local -a args=()
|
||||
for _d in "${default_args[@]:-}"; do
|
||||
[[ -n "$_d" ]] && args+=("$_d")
|
||||
done
|
||||
for _u in "${user_args[@]:-}"; do
|
||||
[[ -n "$_u" ]] && args+=("$_u")
|
||||
done
|
||||
|
||||
# Preprocess mixin flags (--json, --no-color etc)
|
||||
command::_preprocess_flags args
|
||||
|
||||
local fn
|
||||
fn=$(command::fn "$cmd" run)
|
||||
core::call_function "$fn" ${args[@]+"${args[@]}"}
|
||||
}
|
||||
|
||||
|
||||
function core::call_function() {
|
||||
local fn="$1"
|
||||
shift
|
||||
"$fn" "$@"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Loader
|
||||
# ============================================
|
||||
|
||||
function load_command() {
|
||||
local name="$1"
|
||||
|
||||
command::loaded "$name" && return 0
|
||||
|
||||
local path
|
||||
path="$(ctx::commands)/${name}.command.sh"
|
||||
|
||||
if [[ ! -f "$path" ]]; then
|
||||
log::error "Command not found: ${name} (${path})"
|
||||
return 1
|
||||
fi
|
||||
|
||||
source "$path"
|
||||
_LOADED_COMMANDS["$name"]=1
|
||||
|
||||
_CURRENT_LOADING_CMD="$name"
|
||||
core::call_if_exists "$(command::fn "$name" on_load)"
|
||||
_CURRENT_LOADING_CMD=""
|
||||
|
||||
return 0
|
||||
}
|
||||
|
|
@ -1,66 +1,328 @@
|
|||
#!/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
|
||||
|
||||
# ============================================
|
||||
# Flag Registry
|
||||
# ============================================
|
||||
# ── Storage ────────────────────────────────────────────────────────────────
|
||||
|
||||
declare -A _REGISTERED_FLAGS=()
|
||||
# Per-command flag registry — populated by flag::define during on_load
|
||||
# Key: "cmd::subcmd:--flag"
|
||||
# Value: "type|description|constraints"
|
||||
declare -gA _FLAG_REGISTRY=()
|
||||
|
||||
# ============================================
|
||||
# Registration
|
||||
# ============================================
|
||||
# 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
|
||||
|
||||
function flag::register() {
|
||||
local flag="$1"
|
||||
_REGISTERED_FLAGS["$flag"]=1
|
||||
# Defaults — populated at registration time
|
||||
declare -gA _FLAG_DEFAULTS=() # "cmd::subcmd:--flag" → default value
|
||||
|
||||
# ── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function flag::_context() {
|
||||
echo "${_CURRENT_COMMAND:-__global__}"
|
||||
}
|
||||
|
||||
function flag::registered() {
|
||||
[[ -n "${_REGISTERED_FLAGS["$1"]:-}" ]]
|
||||
function flag::_key() {
|
||||
echo "$(flag::_context):${1}"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Parsing
|
||||
# ============================================
|
||||
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/ *$//'
|
||||
}
|
||||
|
||||
# Parse flags from args into associative array
|
||||
# Usage: flag::parse "$@"
|
||||
# Access: flag::get --name
|
||||
declare -A _FLAG_VALUES=()
|
||||
|
||||
function flag::parse() {
|
||||
_FLAG_VALUES=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--*)
|
||||
local key="$1"
|
||||
# Boolean flag (no value follows, or next arg is another flag)
|
||||
if [[ $# -eq 1 || "$2" == --* ]]; then
|
||||
_FLAG_VALUES["$key"]="true"
|
||||
shift
|
||||
else
|
||||
_FLAG_VALUES["$key"]="$2"
|
||||
shift 2
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
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
|
||||
}
|
||||
|
||||
function flag::get() {
|
||||
echo "${_FLAG_VALUES["$1"]:-}"
|
||||
# ── 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"
|
||||
}
|
||||
|
||||
function flag::enabled() {
|
||||
[[ "${_FLAG_VALUES["$1"]:-}" == "true" ]]
|
||||
# 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() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
_FLAG_VALUES["$key"]="$value"
|
||||
[[ -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
|
||||
}
|
||||
66
core/framework/flag.sh.bak
Normal file
66
core/framework/flag.sh.bak
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# ============================================
|
||||
# Flag Registry
|
||||
# ============================================
|
||||
|
||||
declare -A _REGISTERED_FLAGS=()
|
||||
|
||||
# ============================================
|
||||
# Registration
|
||||
# ============================================
|
||||
|
||||
function flag::register() {
|
||||
local flag="$1"
|
||||
_REGISTERED_FLAGS["$flag"]=1
|
||||
}
|
||||
|
||||
function flag::registered() {
|
||||
[[ -n "${_REGISTERED_FLAGS["$1"]:-}" ]]
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Parsing
|
||||
# ============================================
|
||||
|
||||
# Parse flags from args into associative array
|
||||
# Usage: flag::parse "$@"
|
||||
# Access: flag::get --name
|
||||
declare -A _FLAG_VALUES=()
|
||||
|
||||
function flag::parse() {
|
||||
_FLAG_VALUES=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--*)
|
||||
local key="$1"
|
||||
# Boolean flag (no value follows, or next arg is another flag)
|
||||
if [[ $# -eq 1 || "$2" == --* ]]; then
|
||||
_FLAG_VALUES["$key"]="true"
|
||||
shift
|
||||
else
|
||||
_FLAG_VALUES["$key"]="$2"
|
||||
shift 2
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
function flag::get() {
|
||||
echo "${_FLAG_VALUES["$1"]:-}"
|
||||
}
|
||||
|
||||
function flag::enabled() {
|
||||
[[ "${_FLAG_VALUES["$1"]:-}" == "true" ]]
|
||||
}
|
||||
|
||||
function flag::set() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
_FLAG_VALUES["$key"]="$value"
|
||||
}
|
||||
|
|
@ -1 +1,276 @@
|
|||
#!/usr/bin/env bash
|
||||
# core/framework/help.sh
|
||||
#
|
||||
# Dynamic help generation from command::define and flag::define metadata.
|
||||
#
|
||||
# Usage in on_load:
|
||||
# help::section "Filters"
|
||||
# help::section "Output"
|
||||
#
|
||||
# Flags assigned to sections via flag::define constraint:
|
||||
# flag::define --fw bool "Firewall only" [section="Filters"]
|
||||
#
|
||||
# Or via active section (set before flag::define calls):
|
||||
# help::section "Filters"
|
||||
# command::mixin time_filter [section="Filters"]
|
||||
# flag::define --fw bool "Firewall only" # inherits "Filters"
|
||||
#
|
||||
# Auto-generate help:
|
||||
# hook::on "command:help" command::help::auto
|
||||
#
|
||||
# Or custom:
|
||||
# hook::on "command:help" cmd::logs::help
|
||||
|
||||
# ── Storage ───────────────────────────────────────────────────────────────────
|
||||
|
||||
# Section registry: "ctx:section_name" → order_index
|
||||
declare -gA _HELP_SECTIONS=()
|
||||
declare -gi _HELP_SECTION_COUNT=0
|
||||
|
||||
# Flag-to-section mapping: "ctx:--flag" → section_name
|
||||
declare -gA _HELP_FLAG_SECTION=()
|
||||
|
||||
# Current active section (set by help::section, used by subsequent flag::define)
|
||||
declare -g _CURRENT_HELP_SECTION=""
|
||||
|
||||
# Command descriptions: "ctx" → description
|
||||
declare -gA _HELP_CMD_DESC=()
|
||||
|
||||
# ── Section registration ──────────────────────────────────────────────────────
|
||||
|
||||
# help::section "Section Name"
|
||||
# Registers a section and sets it as active for subsequent flag::define calls
|
||||
function help::section() {
|
||||
local name="${1:-}"
|
||||
[[ -z "$name" ]] && return 1
|
||||
|
||||
local ctx
|
||||
ctx="${_CURRENT_COMMAND:-__global__}"
|
||||
local key="${ctx}:${name}"
|
||||
|
||||
if [[ -z "${_HELP_SECTIONS[$key]+x}" ]]; then
|
||||
_HELP_SECTIONS["$key"]=$(( _HELP_SECTION_COUNT++ ))
|
||||
fi
|
||||
|
||||
_CURRENT_HELP_SECTION="$name"
|
||||
}
|
||||
|
||||
# Called by flag::define to record which section a flag belongs to
|
||||
# Checks constraint [section="..."] first, falls back to _CURRENT_HELP_SECTION
|
||||
function help::_assign_flag_section() {
|
||||
local flag="$1" constraints="${2:-}"
|
||||
local ctx="${_CURRENT_COMMAND:-__global__}"
|
||||
|
||||
# Check constraint
|
||||
local section=""
|
||||
if [[ -n "$constraints" ]]; then
|
||||
local parsed
|
||||
parsed=$(flag::_parse_constraints "$constraints")
|
||||
section=$(flag::_constraint_get "$parsed" "section")
|
||||
fi
|
||||
|
||||
# Fall back to active section
|
||||
[[ -z "$section" ]] && section="${_CURRENT_HELP_SECTION:-}"
|
||||
|
||||
# Register section if new
|
||||
if [[ -n "$section" ]]; then
|
||||
local skey="${ctx}:${section}"
|
||||
if [[ -z "${_HELP_SECTIONS[$skey]+x}" ]]; then
|
||||
_HELP_SECTIONS["$skey"]=$(( _HELP_SECTION_COUNT++ ))
|
||||
fi
|
||||
_HELP_FLAG_SECTION["${ctx}:${flag}"]="$section"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Auto help generation ──────────────────────────────────────────────────────
|
||||
|
||||
# command::help::auto
|
||||
# Generates help from registered metadata.
|
||||
# Called via: hook::on "command:help" command::help::auto
|
||||
function command::help::auto() {
|
||||
local cmd="${1:-}" subcmd="${2:-}"
|
||||
local ctx="${_CURRENT_COMMAND:-__global__}"
|
||||
|
||||
local desc="${_HELP_CMD_DESC[$ctx]:-}"
|
||||
|
||||
# Usage line — build from required flags
|
||||
local usage_parts=()
|
||||
local key
|
||||
for key in "${!_FLAG_REGISTRY[@]}"; do
|
||||
[[ "$key" != "${ctx}:"* ]] && continue
|
||||
local flag="${key#${ctx}:}"
|
||||
local reg="${_FLAG_REGISTRY[$key]}"
|
||||
local type="${reg%%|*}"
|
||||
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")
|
||||
local label
|
||||
label=$(flag::_constraint_get "$parsed" "label")
|
||||
[[ -z "$label" ]] && label="value"
|
||||
|
||||
if [[ "$required" == "true" ]]; then
|
||||
case "$type" in
|
||||
bool) usage_parts+=("$flag") ;;
|
||||
value) usage_parts+=("${flag} <${label}>*") ;;
|
||||
array) usage_parts+=("${flag} <${label}>*") ;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Print usage
|
||||
local cmd_path="${cmd}"
|
||||
[[ -n "$subcmd" ]] && cmd_path="${cmd} ${subcmd}"
|
||||
|
||||
printf "\nUsage: wgctl %s" "$cmd_path"
|
||||
for part in "${usage_parts[@]:-}"; do
|
||||
printf " %s" "$part"
|
||||
done
|
||||
|
||||
# Show subcommands if any defined
|
||||
local -a subcmds=()
|
||||
for key in "${!_COMMAND_DEFS[@]}"; do
|
||||
[[ "$key" == "${cmd}:"* ]] && subcmds+=("${key#${cmd}:}")
|
||||
done
|
||||
[[ ${#subcmds[@]} -gt 0 ]] && printf " [command]"
|
||||
printf " [options]\n"
|
||||
|
||||
[[ -n "$desc" ]] && printf "\n%s\n" "$desc"
|
||||
|
||||
# Print subcommands
|
||||
if [[ ${#subcmds[@]} -gt 0 ]]; then
|
||||
printf "\nCommands:\n"
|
||||
for sc in "${subcmds[@]}"; do
|
||||
local sc_key="${cmd}:${sc}"
|
||||
local sc_desc="${_COMMAND_DEFS[$sc_key]:-}"
|
||||
local sc_type
|
||||
sc_type=$(echo "$sc_desc" | cut -d'|' -f1)
|
||||
local sc_text
|
||||
sc_text=$(echo "$sc_desc" | cut -d'|' -f2)
|
||||
local sc_aliases
|
||||
sc_aliases=$(echo "$sc_desc" | cut -d'|' -f3)
|
||||
local sc_default
|
||||
sc_default=$(echo "$sc_desc" | cut -d'|' -f4)
|
||||
|
||||
local suffix=""
|
||||
[[ "$sc_default" == "true" ]] && suffix=" (default)"
|
||||
[[ -n "$sc_aliases" ]] && suffix+=" aliases: ${sc_aliases}"
|
||||
|
||||
printf " %-12s %s%s\n" "$sc" "$sc_text" "$suffix"
|
||||
done
|
||||
fi
|
||||
|
||||
# Print flags by section
|
||||
# Collect sections for this context, sorted by order
|
||||
local -a section_names=()
|
||||
local -A section_order=()
|
||||
for key in "${!_HELP_SECTIONS[@]}"; do
|
||||
[[ "$key" != "${ctx}:"* ]] && continue
|
||||
local sname="${key#${ctx}:}"
|
||||
section_names+=("$sname")
|
||||
section_order["$sname"]="${_HELP_SECTIONS[$key]}"
|
||||
done
|
||||
|
||||
# Sort sections by registration order
|
||||
local -a sorted_sections=()
|
||||
if [[ ${#section_names[@]} -gt 0 ]]; then
|
||||
while IFS= read -r sname; do
|
||||
sorted_sections+=("$sname")
|
||||
done < <(
|
||||
for sname in "${section_names[@]}"; do
|
||||
echo "${section_order[$sname]} $sname"
|
||||
done | sort -n | cut -d' ' -f2-
|
||||
)
|
||||
fi
|
||||
|
||||
# Flags without a section
|
||||
local -a unsectioned=()
|
||||
for key in "${!_FLAG_REGISTRY[@]}"; do
|
||||
[[ "$key" != "${ctx}:"* ]] && continue
|
||||
local flag="${key#${ctx}:}"
|
||||
[[ -z "${_HELP_FLAG_SECTION[${ctx}:${flag}]:-}" ]] && unsectioned+=("$flag")
|
||||
done
|
||||
|
||||
# Print unsectioned flags first as "Options"
|
||||
if [[ ${#unsectioned[@]} -gt 0 ]]; then
|
||||
printf "\nOptions:\n"
|
||||
for flag in "${unsectioned[@]}"; do
|
||||
help::_print_flag "$flag" "$ctx"
|
||||
done
|
||||
fi
|
||||
|
||||
# Print sectioned flags
|
||||
for sname in "${sorted_sections[@]:-}"; do
|
||||
local printed_header=false
|
||||
for key in "${!_FLAG_REGISTRY[@]}"; do
|
||||
[[ "$key" != "${ctx}:"* ]] && continue
|
||||
local flag="${key#${ctx}:}"
|
||||
local flag_section="${_HELP_FLAG_SECTION[${ctx}:${flag}]:-}"
|
||||
[[ "$flag_section" != "$sname" ]] && continue
|
||||
if ! $printed_header; then
|
||||
printf "\n%s:\n" "$sname"
|
||||
printed_header=true
|
||||
fi
|
||||
help::_print_flag "$flag" "$ctx"
|
||||
done
|
||||
done
|
||||
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
function help::_print_flag() {
|
||||
local flag="$1" ctx="$2"
|
||||
local key="${ctx}:${flag}"
|
||||
local reg="${_FLAG_REGISTRY[$key]:-}"
|
||||
[[ -z "$reg" ]] && return 0
|
||||
|
||||
local type="${reg%%|*}"
|
||||
local rest="${reg#*|}"
|
||||
local desc="${rest%%|*}"
|
||||
local constraints="${rest#*|}"
|
||||
|
||||
local label="" required=false default_val="" choices=""
|
||||
if [[ -n "$constraints" ]]; then
|
||||
local parsed
|
||||
parsed=$(flag::_parse_constraints "$constraints")
|
||||
label=$(flag::_constraint_get "$parsed" "label")
|
||||
local req
|
||||
req=$(flag::_constraint_get "$parsed" "required")
|
||||
[[ "$req" == "true" ]] && required=true
|
||||
default_val=$(flag::_constraint_get "$parsed" "default")
|
||||
choices=$(flag::_constraint_get "$parsed" "choices")
|
||||
fi
|
||||
[[ -z "$label" ]] && label="value"
|
||||
|
||||
local flag_display="$flag"
|
||||
case "$type" in
|
||||
value) flag_display="${flag} <${label}>" ;;
|
||||
array) flag_display="${flag} <${label}>" ;;
|
||||
esac
|
||||
|
||||
local suffix=""
|
||||
$required && suffix=" *"
|
||||
[[ -n "$default_val" ]] && suffix+=" [default: ${default_val}]"
|
||||
[[ -n "$choices" ]] && suffix+=" (${choices//|/|})"
|
||||
|
||||
printf " %-28s %s%s\n" "$flag_display" "$desc" "$suffix"
|
||||
}
|
||||
|
||||
# ── Reset ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
function help::_reset() {
|
||||
local ctx="${_CURRENT_COMMAND:-__global__}"
|
||||
local key
|
||||
for key in "${!_HELP_SECTIONS[@]}"; do
|
||||
[[ "$key" == "${ctx}:"* ]] && unset "_HELP_SECTIONS[$key]"
|
||||
done
|
||||
for key in "${!_HELP_FLAG_SECTION[@]}"; do
|
||||
[[ "$key" == "${ctx}:"* ]] && unset "_HELP_FLAG_SECTION[$key]"
|
||||
done
|
||||
_CURRENT_HELP_SECTION=""
|
||||
}
|
||||
|
|
@ -1 +1,47 @@
|
|||
#!/usr/bin/env bash
|
||||
# core/framework/hook.sh
|
||||
#
|
||||
# Simple callback registry — fire named hooks, register handlers.
|
||||
#
|
||||
# Usage:
|
||||
# hook::on "command:help" my_handler_function
|
||||
# hook::off "command:help" my_handler_function
|
||||
# hook::fire "command:help" arg1 arg2
|
||||
#
|
||||
# If no handler registered, hook::fire returns 0 silently.
|
||||
# hook::fire returns the exit code of the handler.
|
||||
|
||||
# Registry: event_name → handler_function
|
||||
declare -gA _HOOK_REGISTRY=()
|
||||
|
||||
# hook::on <event> <handler>
|
||||
# Register a handler for an event. Replaces existing handler.
|
||||
function hook::on() {
|
||||
local event="${1:-}" handler="${2:-}"
|
||||
[[ -z "$event" || -z "$handler" ]] && return 1
|
||||
_HOOK_REGISTRY["$event"]="$handler"
|
||||
}
|
||||
|
||||
# hook::off <event>
|
||||
# Remove handler for an event.
|
||||
function hook::off() {
|
||||
local event="${1:-}"
|
||||
[[ -z "$event" ]] && return 1
|
||||
unset "_HOOK_REGISTRY[$event]"
|
||||
}
|
||||
|
||||
# hook::fire <event> [args...]
|
||||
# Call registered handler with args. Silent no-op if no handler.
|
||||
function hook::fire() {
|
||||
local event="${1:-}"
|
||||
shift || true
|
||||
local handler="${_HOOK_REGISTRY[$event]:-}"
|
||||
[[ -z "$handler" ]] && return 0
|
||||
"$handler" "$@"
|
||||
}
|
||||
|
||||
# hook::has <event>
|
||||
# Returns 0 if a handler is registered for event.
|
||||
function hook::has() {
|
||||
[[ -n "${_HOOK_REGISTRY[$1]:-}" ]]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue