wgctl/core/framework/command_mixins.sh
Nuno Duque Nunes de22dbeec7 feat: list migration to command framework
- commands/list/list.sh: router + command::define show [*]
- commands/list/show.sh: flag::define + flag::parse, all helpers
- flag::exclusive: register under top-level command name (strip ::subcmd)
- flag::parse: validate exclusive groups, error on conflicting user flags
- --help: intercept in command::run before routing
- help::auto: don't show default subcommand in usage line
2026-05-30 04:14:21 +00:00

195 lines
No EOL
5.5 KiB
Bash

#!/usr/bin/env bash
# core/command_mixins.sh
# Mixin infrastructure — loads mixin files and provides command::mixin / flag::exclusive
# ============================================
# Active mixin tracking (per-process)
# ============================================
declare -a _ACTIVE_MIXINS=()
# ============================================
# Auto-load all mixin files
# ============================================
function command::_load_mixins() {
local mixin_file
local -a mixin_paths=(
"${_FRAMEWORK_DIR}/mixins/"*.mixin.sh
)
for mixin_file in "${mixin_paths[@]:-}"; do
[[ -f "$mixin_file" ]] && source "$mixin_file"
done
}
# ============================================
# command::mixin <name>
# Called from cmd::<name>::on_load to opt into a mixin
# ============================================
function command::mixin() {
local name="${1:-}"
[[ -z "$name" ]] && log::error "command::mixin: missing name" && return 1
local register_fn="command::mixin::${name}::register"
if declare -f "$register_fn" >/dev/null 2>&1; then
# Track for reset/process — avoid duplicates
local m already=false
for m in "${_ACTIVE_MIXINS[@]:-}"; do
[[ "$m" == "$name" ]] && already=true && break
done
$already || _ACTIVE_MIXINS+=("$name")
"$register_fn"
else
log::error "Unknown mixin: ${name} (no command::mixin::${name}::register found)"
return 1
fi
}
# ============================================
# command::_reset_mixins
# Called by command::run before each invocation
# ============================================
function command::_reset_mixin_state() {
# Reset values but keep _ACTIVE_MIXINS populated
local m
for m in "${_ACTIVE_MIXINS[@]:-}"; do
local reset_fn="command::mixin::${m}::reset"
declare -f "$reset_fn" >/dev/null 2>&1 && "$reset_fn"
done
}
function command::_reset_mixins() {
_ACTIVE_MIXINS=()
local reset_fn mixin_file
# Reset all known mixins regardless of active state
for mixin_file in \
"${WGCTL_DIR}/core/mixins/"*.mixin.sh \
"${WGCTL_DIR}/commands/mixins/"*.mixin.sh; do
[[ -f "$mixin_file" ]] || continue
local mixin_name
mixin_name=$(basename "$mixin_file" .mixin.sh)
reset_fn="command::mixin::${mixin_name}::reset"
declare -f "$reset_fn" >/dev/null 2>&1 && "$reset_fn"
done
}
# ============================================
# command::_preprocess_flags <args_nameref>
# Called by command::run — strips mixin flags from args
# ============================================
function command::_preprocess_flags() {
local -n _args_ref="$1"
local -a _filtered=()
if [[ ${#_args_ref[@]} -eq 0 ]]; then
return 0 # nothing to process
fi
for _arg in "${_args_ref[@]}"; do
local _consumed=false
for _mixin in "${_ACTIVE_MIXINS[@]:-}"; do
local _process_fn="command::mixin::${_mixin}::process"
if declare -f "$_process_fn" >/dev/null 2>&1; then
if "$_process_fn" "$_arg"; then
_consumed=true
break
fi
fi
done
$_consumed || _filtered+=("$_arg")
done
if [[ ${#_filtered[@]} -gt 0 ]]; then
_args_ref=("${_filtered[@]}")
else
_args_ref=()
fi
}
# command::_resolve_conflicts <defaults_nameref> <user_nameref> <groups_string>
# Removes conflicting defaults when user provides a member of an exclusive group
function command::_resolve_conflicts() {
local -n _def_ref="$1"
local -n _usr_ref="$2"
local groups="$3"
[[ -z "$groups" ]] && return 0
[[ ${#_def_ref[@]} -eq 0 ]] && return 0
# Work on a copy — progressively filter across all groups
local -a working=("${_def_ref[@]}")
local group
while IFS= read -r group; do
[[ -z "$group" ]] && continue
local -a members=()
IFS=',' read -ra members <<< "$group"
# Find which member (if any) the user passed from this group
local user_member=""
local member user_arg
for member in "${members[@]}"; do
for user_arg in "${_usr_ref[@]:-}"; do
if [[ "$user_arg" == "$member" ]]; then
user_member="$member"
break 2
fi
done
done
# No user member in this group — don't touch defaults
[[ -z "$user_member" ]] && continue
# User passed a member — remove all OTHER members from defaults
# (keep the same flag if it was already in defaults)
local -a new_working=()
local def_arg
for def_arg in "${working[@]:-}"; do
local is_other_member=false
for member in "${members[@]}"; do
# It's another member if it's in the group AND not the same as user's choice
if [[ "$def_arg" == "$member" && "$def_arg" != "$user_member" ]]; then
is_other_member=true
break
fi
done
$is_other_member || new_working+=("$def_arg")
done
working=("${new_working[@]:-}")
done < <(echo "$groups" | tr '|' '\n')
# Write back
if [[ ${#working[@]} -gt 0 ]]; then
_def_ref=("${working[@]}")
else
_def_ref=()
fi
}
# ============================================
# Flag Exclusive
# ============================================
declare -gA _FLAG_EXCLUSIVE_GROUPS=()
# flag::exclusive <flag1> <flag2> ...
# Called from on_load — registers mutually exclusive flags for current command
function flag::exclusive() {
local cmd="${_CURRENT_LOADING_CMD:-}"
[[ -z "$cmd" ]] && return 0
cmd="${cmd%%::*}"
# Join flags with comma as one group
local group
group=$(IFS=','; echo "$*")
if [[ -n "${_FLAG_EXCLUSIVE_GROUPS[$cmd]:-}" ]]; then
_FLAG_EXCLUSIVE_GROUPS["$cmd"]+="${group}|"
else
_FLAG_EXCLUSIVE_GROUPS["$cmd"]="${group}|"
fi
}