wgctl/core/framework/help.sh
Nuno Duque Nunes 8ed491313d 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
2026-05-30 03:44:08 +00:00

276 lines
No EOL
8.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"
}
# 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=""
}