wgctl/core/framework/help.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

238 lines
No EOL
7.5 KiB
Bash

#!/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"
}
# ── 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 required; required="${_FLAG_C_REQUIRED[$key]:-}"
local label; label="${_FLAG_C_LABEL[$key]:-value}"
[[ -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}"
local default_subcmd="${_COMMAND_DEFAULT[$cmd]:-}"
# Only show subcmd in usage if it's not the default
[[ -n "$subcmd" && "$subcmd" != "$default_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_text sc_aliases sc_default
sc_text=$(echo "$sc_desc" | cut -d'|' -f1)
sc_aliases=$(echo "$sc_desc" | cut -d'|' -f2)
sc_default=$(echo "$sc_desc" | cut -d'|' -f3)
# Clean up empty aliases
[[ "$sc_aliases" == "false" || "$sc_aliases" == "true" ]] && sc_aliases=""
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 desc="${reg#*|}"
# Use pre-cached constraints instead of parsing
local label="${_FLAG_C_LABEL[$key]:-value}"
local required="${_FLAG_C_REQUIRED[$key]:-}"
local default_val="${_FLAG_C_DEFAULT[$key]:-}"
local choices="${_FLAG_C_CHOICES[$key]:-}"
local flag_display="$flag"
case "$type" in
value) flag_display="${flag} <${label}>" ;;
array) flag_display="${flag} <${label}>" ;;
esac
local suffix=""
[[ "$required" == "true" ]] && suffix=" *"
[[ -n "$default_val" && "$type" != "bool" ]] && 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=""
}