#!/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}" 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 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="" }