- 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
238 lines
No EOL
7.5 KiB
Bash
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=""
|
|
} |