From 9c11152682ca4859ec2fd288dade9d728e67d936 Mon Sep 17 00:00:00 2001 From: Nuno Duque Nunes Date: Fri, 29 May 2026 04:31:07 +0000 Subject: [PATCH] feat: command defaults, aliases, exclusive flag groups - wgctl.json: commands section with defaults and aliases - command_mixins.sh: flag::exclusive, command::_resolve_conflicts - command::run: two-pass defaults+user with conflict resolution - load_command: _CURRENT_LOADING_CMD for flag::exclusive context - list: flag::exclusive --online --offline --blocked --restricted --allowed - logs: flag::exclusive --ascending --descending - logs: fix --fw --wg together treated as neither (show both) - dispatch: config alias resolution before load_command - wgctl ls/peers/act: aliases via wgctl.json --- commands/activity.command.sh | 6 ++- commands/list.command.sh | 6 ++- commands/logs.command.sh | 7 +++ core/command.sh | 53 ++++++++++++++++++++-- core/command_mixins.sh | 86 +++++++++++++++++++++++++++++++++++- core/json_helper.py | 17 +++++++ modules/config.module.sh | 11 +++++ wgctl | 7 ++- 8 files changed, 184 insertions(+), 9 deletions(-) diff --git a/commands/activity.command.sh b/commands/activity.command.sh index 554ea0a..2ff0c1d 100644 --- a/commands/activity.command.sh +++ b/commands/activity.command.sh @@ -6,6 +6,8 @@ # ============================================ function cmd::activity::on_load() { + command::mixin json_output + load_module net flag::register --peer @@ -16,8 +18,8 @@ function cmd::activity::on_load() { flag::register --accept flag::register --drop flag::register --external - - command::mixin json_output + + flag::exclusive --accept --drop } # ============================================ diff --git a/commands/list.command.sh b/commands/list.command.sh index eedade9..dac46f7 100644 --- a/commands/list.command.sh +++ b/commands/list.command.sh @@ -5,6 +5,8 @@ # ============================================ function cmd::list::on_load() { + command::mixin json_output + load_module identity load_module ui @@ -19,7 +21,9 @@ function cmd::list::on_load() { flag::register --allowed flag::register --detailed flag::register --name - command::mixin json_output + + # Mutually exclusive filter groups + flag::exclusive --online --offline --blocked --restricted --allowed } # ============================================ diff --git a/commands/logs.command.sh b/commands/logs.command.sh index ae6129a..d967d6d 100644 --- a/commands/logs.command.sh +++ b/commands/logs.command.sh @@ -23,6 +23,8 @@ function cmd::logs::on_load() { flag::register --ascending flag::register --descending flag::register --resolved + + flag::exclusive --ascending --descending } function cmd::logs::help() { @@ -161,6 +163,11 @@ function cmd::logs::show() { [[ -z "$filter_ip" ]] && log::error "Could not find IP for: $name" && return 1 fi + if $fw_only && $wg_only; then + fw_only=false + wg_only=false + fi + if $follow; then cmd::logs::follow "$filter_ip" "$name" "$type" "$fw_only" "$wg_only" return diff --git a/core/command.sh b/core/command.sh index 6957d53..4d3c182 100644 --- a/core/command.sh +++ b/core/command.sh @@ -8,6 +8,7 @@ declare -A _LOADED_COMMANDS=() readonly _COMMAND_NAMESPACE="cmd" readonly _COMMAND_AUTO_LOAD_HOOK="on_load" +_CURRENT_LOADING_CMD="" # ============================================ # Helpers @@ -36,15 +37,59 @@ function command::exists() { command::has_function "$1" run; } # Runner # ============================================ +# function command::run() { +# local cmd="$1" +# shift + +# command::_reset_mixin_state # reset values only, keep _ACTIVE_MIXINS + +# local -a args=("$@") +# command::_preprocess_flags args + +# local fn +# fn=$(command::fn "$cmd" run) +# core::call_function "$fn" ${args[@]+"${args[@]}"} +# } + function command::run() { local cmd="$1" shift + + command::_reset_mixin_state + + # Build default args from config + local -a default_args=() + local defaults="${_COMMAND_DEFAULTS[$cmd]:-}" + if [[ -n "$defaults" ]]; then + read -ra default_args <<< "$defaults" + fi + + local -a user_args=("$@") + [[ $# -gt 0 ]] && user_args=("$@") + + # Resolve exclusive group conflicts — user args override defaults + local groups="${_FLAG_EXCLUSIVE_GROUPS[$cmd]:-}" + if [[ -n "$groups" && ${#default_args[@]} -gt 0 && ${#user_args[@]} -gt 0 ]]; then + command::_resolve_conflicts default_args user_args "$groups" + fi - command::_reset_mixin_state # reset values only, keep _ACTIVE_MIXINS + local -a cleaned_defaults=() + for _d in "${default_args[@]:-}"; do + [[ -n "$_d" ]] && cleaned_defaults+=("$_d") + done + default_args=("${cleaned_defaults[@]:-}") - local -a args=("$@") + local -a args=() + for _d in "${default_args[@]:-}"; do + [[ -n "$_d" ]] && args+=("$_d") + done + for _u in "${user_args[@]:-}"; do + [[ -n "$_u" ]] && args+=("$_u") + done + + # Preprocess mixin flags (--json, --no-color etc) command::_preprocess_flags args - + echo "DEBUG cmd=$cmd groups='$groups' defs='${default_args[*]}' user='${user_args[*]:-}'" >&2 local fn fn=$(command::fn "$cmd" run) core::call_function "$fn" ${args[@]+"${args[@]}"} @@ -77,7 +122,9 @@ function load_command() { source "$path" _LOADED_COMMANDS["$name"]=1 + _CURRENT_LOADING_CMD="$name" core::call_if_exists "$(command::fn "$name" on_load)" + _CURRENT_LOADING_CMD="" return 0 } diff --git a/core/command_mixins.sh b/core/command_mixins.sh index fdb5582..2e441e3 100644 --- a/core/command_mixins.sh +++ b/core/command_mixins.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # core/command_mixins.sh -# Mixin infrastructure — loads mixin files and provides command::mixin +# Mixin infrastructure — loads mixin files and provides command::mixin / flag::exclusive # ============================================ # Active mixin tracking (per-process) @@ -108,4 +108,88 @@ function command::_preprocess_flags() { else _args_ref=() fi +} + +# command::_resolve_conflicts +# 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 ... +# Called from on_load — registers mutually exclusive flags for current command +function flag::exclusive() { + local cmd="${_CURRENT_LOADING_CMD:-}" + [[ -z "$cmd" ]] && return 0 + + # 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 } \ No newline at end of file diff --git a/core/json_helper.py b/core/json_helper.py index c3fd34c..7381c49 100644 --- a/core/json_helper.py +++ b/core/json_helper.py @@ -1607,6 +1607,23 @@ def config_load(file): emit('ACTIVITY_CURRENT_LOW_BYTES', acur.get('low')) emit('ACTIVITY_CURRENT_MED_BYTES', acur.get('medium')) emit('ACTIVITY_CURRENT_HIGH_BYTES', acur.get('high')) + + # Command defaults and aliases + # Output format: + # CMD_DEFAULT:activity=--exclude-service pihole:dns-udp --limit 50 + # CMD_ALIAS:act=activity + # CMD_ALIAS:a=activity + cmds = d.get('commands', {}) + for cmd_name, cmd_cfg in cmds.items(): + if not isinstance(cmd_cfg, dict): + continue + defaults = cmd_cfg.get('defaults', []) + if defaults: + print(f"CMD_DEFAULT:{cmd_name}={' '.join(str(x) for x in defaults)}") + aliases = cmd_cfg.get('aliases', []) + for alias in aliases: + print(f"CMD_ALIAS:{alias}={cmd_name}") + except Exception as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) diff --git a/modules/config.module.sh b/modules/config.module.sh index 00b097b..c85240e 100644 --- a/modules/config.module.sh +++ b/modules/config.module.sh @@ -22,6 +22,9 @@ declare -g _ACTIVITY_CURRENT_LOW_BYTES="${ACTIVITY_CURRENT_LOW_BYTES:-1000000}" declare -g _ACTIVITY_CURRENT_MED_BYTES="${ACTIVITY_CURRENT_MED_BYTES:-10000000}" declare -g _ACTIVITY_CURRENT_HIGH_BYTES="${ACTIVITY_CURRENT_HIGH_BYTES:-100000000}" +declare -gA _COMMAND_DEFAULTS=() +declare -gA _COMMAND_ALIASES=() + function config::_init_defaults() { _WG_INTERFACE="wg0" _WG_DNS="10.0.0.103" @@ -89,6 +92,14 @@ function config::_load_json() { ACTIVITY_CURRENT_LOW_BYTES) _ACTIVITY_CURRENT_LOW_BYTES="$value" ;; ACTIVITY_CURRENT_MED_BYTES) _ACTIVITY_CURRENT_MED_BYTES="$value" ;; ACTIVITY_CURRENT_HIGH_BYTES) _ACTIVITY_CURRENT_HIGH_BYTES="$value" ;; + CMD_DEFAULT:*) + local cmd_name="${key#CMD_DEFAULT:}" + _COMMAND_DEFAULTS["$cmd_name"]="$value" + ;; + CMD_ALIAS:*) + local alias_name="${key#CMD_ALIAS:}" + _COMMAND_ALIASES["$alias_name"]="$value" + ;; esac done < <(json::config_load "$file" 2>/dev/null) } diff --git a/wgctl b/wgctl index e80f81c..21eb188 100755 --- a/wgctl +++ b/wgctl @@ -43,7 +43,6 @@ declare -A CMD_ALIASES=( [del]=remove [delete]=remove [mv]=rename - [ls]=list [show]=list [monitor]=watch [ban]=block @@ -53,7 +52,6 @@ declare -A CMD_ALIASES=( [down]=service [reload]=service [stat]=service - [log]=service [start]=service [stop]=service [restart]=service @@ -78,6 +76,11 @@ function wgctl::dispatch() { local cmd cmd="$(wgctl::resolve_alias "$raw_cmd")" + # Resolve config-defined aliases (from wgctl.json commands section) + if [[ -n "${_COMMAND_ALIASES[$cmd]:-}" ]]; then + cmd="${_COMMAND_ALIASES[$cmd]}" + fi + case "$cmd" in help) wgctl::help; return ;; shell) : ;;