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