feat: new flag::define syntax, flag::set_constraint
- flag::define: variadic constraint args (key:value) instead of bracket string - flag::_parse_constraints_from_args: replaces flag::_parse_and_cache - flag::set_constraint: Option B syntax for post-definition constraints - choices separator: comma (choices:split,full) — no quoting needed - guard against empty _CURRENT_COMMAND in exclusive groups lookup - migrate all commands to new constraint syntax - add helpful error for unknown constraint args
This commit is contained in:
parent
f61bc59446
commit
a559b73e8e
18 changed files with 664 additions and 205 deletions
|
|
@ -5,19 +5,19 @@ function cmd::activity::show::on_load() {
|
||||||
command::mixin json_output [section="Output"]
|
command::mixin json_output [section="Output"]
|
||||||
|
|
||||||
help::section "Filters"
|
help::section "Filters"
|
||||||
flag::define --peer value "Filter by peer name" [label="name", section="Filters"]
|
flag::define --peer value "Filter by peer name" label:name section:Filters
|
||||||
flag::define --type value "Filter by device type" [label="type", section="Filters"]
|
flag::define --type value "Filter by device type" label:type section:Filters
|
||||||
flag::define --service value "Filter by service" [label="service", section="Filters"]
|
flag::define --service value "Filter by service" label:service section:Filters
|
||||||
flag::define --ip value "Filter by destination IP" [label="ip", section="Filters"]
|
flag::define --ip value "Filter by destination IP" label:ip section:Filters
|
||||||
flag::define --hours value "Hours to look back" [default=24, type=int, min=0, section="Filters"]
|
flag::define --hours value "Hours to look back" default:24 type:int min:0 section:Filters
|
||||||
flag::define --exclude-service[] "Exclude service from output" [label="service", section="Filters"]
|
flag::define --exclude-service[] "Exclude service from output" label:service section:Filters
|
||||||
flag::define --include-service[] "Override excluded service" [label="service", section="Filters"]
|
flag::define --include-service[] "Override excluded service" label:service section:Filters
|
||||||
|
|
||||||
help::section "Display"
|
help::section "Display"
|
||||||
flag::define --accept bool "Show only accepted traffic" [section="Display"]
|
flag::define --accept bool "Show only accepted traffic" section:Display
|
||||||
flag::define --drop bool "Show only firewall drops" [section="Display"]
|
flag::define --drop bool "Show only firewall drops" section:Display
|
||||||
flag::define --external bool "Show only external traffic" [section="Display"]
|
flag::define --external bool "Show only external traffic" section:Display
|
||||||
flag::define --ports bool "Show raw IP:port annotations" [section="Display"]
|
flag::define --ports bool "Show raw IP:port annotations" section:Display
|
||||||
|
|
||||||
flag::exclusive --accept --drop
|
flag::exclusive --accept --drop
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,21 @@
|
||||||
|
|
||||||
function cmd::block::show::on_load() {
|
function cmd::block::show::on_load() {
|
||||||
help::section "Target"
|
help::section "Target"
|
||||||
flag::define --name value "Peer name to block" [label="name", section="Target"]
|
flag::define --name value "Peer name to block" label:name section:Target
|
||||||
flag::define --identity value "Block all peers in identity" [label="identity", section="Target"]
|
flag::define --identity value "Block all peers in identity" label:identity section:Target
|
||||||
flag::define --type value "Filter by device type" [label="type", section="Target"]
|
flag::define --type value "Filter by device type" label:type section:Target
|
||||||
|
|
||||||
help::section "Rules"
|
help::section "Rules"
|
||||||
flag::define --ip[] "Block specific IP" [label="ip", section="Rules"]
|
flag::define --ip[] "Block specific IP" label:ip section:Rules
|
||||||
flag::define --subnet[] "Block specific subnet" [label="subnet", section="Rules"]
|
flag::define --subnet[] "Block specific subnet" label:subnet section:Rules
|
||||||
flag::define --port[] "Block specific port (ip:port:proto)" [label="port",section="Rules"]
|
flag::define --port[] "Block specific port (ip:port:proto)" label:port section:Rules
|
||||||
flag::define --service[] "Block by service name" [label="service", section="Rules"]
|
flag::define --service[] "Block by service name" label:service section:Rules
|
||||||
flag::define --block-name value "Label for this block rule" [label="name", section="Rules"]
|
flag::define --block-name value "Label for this block rule" label:name section:Rules
|
||||||
|
|
||||||
help::section "Options"
|
help::section "Options"
|
||||||
flag::define --reason value "Reason for block (recorded in history)" [label="reason", section="Options"]
|
flag::define --reason value "Reason for block (recorded in history)" label:reason section:Options
|
||||||
flag::define --force bool "Skip confirmation" [section="Options"]
|
flag::define --force bool "Skip confirmation" section:Options
|
||||||
flag::define --quiet bool "Suppress output" [section="Options"]
|
flag::define --quiet bool "Suppress output" section:Options
|
||||||
|
|
||||||
flag::exclusive --name --identity
|
flag::exclusive --name --identity
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -184,98 +184,98 @@ function cmd::config::migrate() {
|
||||||
|| log::wg_success "Migration complete"
|
|| log::wg_success "Migration complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
function config::_convert_to_json() {
|
# function config::_convert_to_json() {
|
||||||
local legacy_file="$1" output_file="$2"
|
# local legacy_file="$1" output_file="$2"
|
||||||
|
|
||||||
# Read legacy conf into variables
|
# # Read legacy conf into variables
|
||||||
local wg_interface="wg0" wg_endpoint="" wg_dns="10.0.0.103"
|
# local wg_interface="wg0" wg_endpoint="" wg_dns="10.0.0.103"
|
||||||
local wg_dns_fallback="" wg_port="51820" wg_subnet="10.1.0.0/16"
|
# local wg_dns_fallback="" wg_port="51820" wg_subnet="10.1.0.0/16"
|
||||||
local wg_lan="10.0.0.0/24" wg_hs_check="300" date_format="eu"
|
# local wg_lan="10.0.0.0/24" wg_hs_check="300" date_format="eu"
|
||||||
|
|
||||||
while IFS='=' read -r key value || [[ -n "$key" ]]; do
|
# while IFS='=' read -r key value || [[ -n "$key" ]]; do
|
||||||
[[ "$key" =~ ^[[:space:]]*# ]] && continue
|
# [[ "$key" =~ ^[[:space:]]*# ]] && continue
|
||||||
[[ -z "${key// }" ]] && continue
|
# [[ -z "${key// }" ]] && continue
|
||||||
key="${key// /}"
|
# key="${key// /}"
|
||||||
value="${value// /}"
|
# value="${value// /}"
|
||||||
case "$key" in
|
# case "$key" in
|
||||||
WG_INTERFACE) wg_interface="$value" ;;
|
# WG_INTERFACE) wg_interface="$value" ;;
|
||||||
WG_ENDPOINT) wg_endpoint="$value" ;;
|
# WG_ENDPOINT) wg_endpoint="$value" ;;
|
||||||
WG_DNS) wg_dns="$value" ;;
|
# WG_DNS) wg_dns="$value" ;;
|
||||||
WG_DNS_FALLBACK) wg_dns_fallback="$value" ;;
|
# WG_DNS_FALLBACK) wg_dns_fallback="$value" ;;
|
||||||
WG_PORT) wg_port="$value" ;;
|
# WG_PORT) wg_port="$value" ;;
|
||||||
WG_SUBNET) wg_subnet="$value" ;;
|
# WG_SUBNET) wg_subnet="$value" ;;
|
||||||
WG_LAN) wg_lan="$value" ;;
|
# WG_LAN) wg_lan="$value" ;;
|
||||||
WG_HANDSHAKE_CHECK_TIME_SEC) wg_hs_check="$value" ;;
|
# WG_HANDSHAKE_CHECK_TIME_SEC) wg_hs_check="$value" ;;
|
||||||
DATE_FORMAT) date_format="$value" ;;
|
# DATE_FORMAT) date_format="$value" ;;
|
||||||
esac
|
# esac
|
||||||
done < "$legacy_file"
|
# done < "$legacy_file"
|
||||||
|
|
||||||
# Build fallback DNS array
|
# # Build fallback DNS array
|
||||||
local dns_fallback_json="[]"
|
# local dns_fallback_json="[]"
|
||||||
if [[ -n "$wg_dns_fallback" ]]; then
|
# if [[ -n "$wg_dns_fallback" ]]; then
|
||||||
local fallback_array
|
# local fallback_array
|
||||||
fallback_array=$(echo "$wg_dns_fallback" | tr ',' '\n' | \
|
# fallback_array=$(echo "$wg_dns_fallback" | tr ',' '\n' | \
|
||||||
while IFS= read -r s; do
|
# while IFS= read -r s; do
|
||||||
s="${s// /}"
|
# s="${s// /}"
|
||||||
[[ -n "$s" ]] && printf '"%s",' "$s"
|
# [[ -n "$s" ]] && printf '"%s",' "$s"
|
||||||
done | sed 's/,$//')
|
# done | sed 's/,$//')
|
||||||
dns_fallback_json="[${fallback_array}]"
|
# dns_fallback_json="[${fallback_array}]"
|
||||||
fi
|
# fi
|
||||||
|
|
||||||
mkdir -p "$(dirname "$output_file")"
|
# mkdir -p "$(dirname "$output_file")"
|
||||||
cat > "$output_file" << JSON
|
# cat > "$output_file" << JSON
|
||||||
{
|
# {
|
||||||
"wireguard": {
|
# "wireguard": {
|
||||||
"interface": "${wg_interface}",
|
# "interface": "${wg_interface}",
|
||||||
"endpoint": "${wg_endpoint}",
|
# "endpoint": "${wg_endpoint}",
|
||||||
"port": ${wg_port},
|
# "port": ${wg_port},
|
||||||
"subnet": "${wg_subnet}",
|
# "subnet": "${wg_subnet}",
|
||||||
"lan": "${wg_lan}"
|
# "lan": "${wg_lan}"
|
||||||
},
|
# },
|
||||||
"dns": {
|
# "dns": {
|
||||||
"primary": "${wg_dns}",
|
# "primary": "${wg_dns}",
|
||||||
"fallback": ${dns_fallback_json}
|
# "fallback": ${dns_fallback_json}
|
||||||
},
|
# },
|
||||||
"handshake": {
|
# "handshake": {
|
||||||
"check_interval_sec": ${wg_hs_check}
|
# "check_interval_sec": ${wg_hs_check}
|
||||||
},
|
# },
|
||||||
"activity": {
|
# "activity": {
|
||||||
"total": {"low": 1000000, "medium": 10000000, "high": 100000000},
|
# "total": {"low": 1000000, "medium": 10000000, "high": 100000000},
|
||||||
"current": {"low": 1000000, "medium": 10000000, "high": 100000000}
|
# "current": {"low": 1000000, "medium": 10000000, "high": 100000000}
|
||||||
},
|
# },
|
||||||
"display": {
|
# "display": {
|
||||||
"date_format": "${date_format}"
|
# "date_format": "${date_format}"
|
||||||
}
|
# }
|
||||||
}
|
# }
|
||||||
JSON
|
# JSON
|
||||||
}
|
# }
|
||||||
|
|
||||||
function config::_write_default_json() {
|
# function config::_write_default_json() {
|
||||||
local output_file="$1"
|
# local output_file="$1"
|
||||||
mkdir -p "$(dirname "$output_file")"
|
# mkdir -p "$(dirname "$output_file")"
|
||||||
cat > "$output_file" << 'JSON'
|
# cat > "$output_file" << 'JSON'
|
||||||
{
|
# {
|
||||||
"wireguard": {
|
# "wireguard": {
|
||||||
"interface": "wg0",
|
# "interface": "wg0",
|
||||||
"endpoint": "",
|
# "endpoint": "",
|
||||||
"port": 51820,
|
# "port": 51820,
|
||||||
"subnet": "10.1.0.0/16",
|
# "subnet": "10.1.0.0/16",
|
||||||
"lan": "10.0.0.0/24"
|
# "lan": "10.0.0.0/24"
|
||||||
},
|
# },
|
||||||
"dns": {
|
# "dns": {
|
||||||
"primary": "10.0.0.103",
|
# "primary": "10.0.0.103",
|
||||||
"fallback": []
|
# "fallback": []
|
||||||
},
|
# },
|
||||||
"handshake": {
|
# "handshake": {
|
||||||
"check_interval_sec": 300
|
# "check_interval_sec": 300
|
||||||
},
|
# },
|
||||||
"activity": {
|
# "activity": {
|
||||||
"total": {"low": 1000000, "medium": 10000000, "high": 100000000},
|
# "total": {"low": 1000000, "medium": 10000000, "high": 100000000},
|
||||||
"current": {"low": 1000000, "medium": 10000000, "high": 100000000}
|
# "current": {"low": 1000000, "medium": 10000000, "high": 100000000}
|
||||||
},
|
# },
|
||||||
"display": {
|
# "display": {
|
||||||
"date_format": "eu"
|
# "date_format": "eu"
|
||||||
}
|
# }
|
||||||
}
|
# }
|
||||||
JSON
|
# JSON
|
||||||
}
|
# }
|
||||||
10
commands/config/config.sh
Normal file
10
commands/config/config.sh
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# commands/config/config.sh — router only
|
||||||
|
|
||||||
|
function cmd::config::on_load() {
|
||||||
|
command::helpers "helpers.sh"
|
||||||
|
command::define show "Show client config" [*]
|
||||||
|
command::define migrate "Migrate config to JSON" [m]
|
||||||
|
}
|
||||||
|
|
||||||
|
hook::on "command:help:config" command::help::auto
|
||||||
169
commands/config/helpers.sh
Normal file
169
commands/config/helpers.sh
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# commands/config/helpers.sh
|
||||||
|
|
||||||
|
function cmd::config::_migrate_impl() {
|
||||||
|
local force="$1" dry_run="$2"
|
||||||
|
|
||||||
|
local wgctl_dir; wgctl_dir="$(ctx::wgctl)"
|
||||||
|
local config_dir="${wgctl_dir}/config"
|
||||||
|
local data_dir="${wgctl_dir}/data"
|
||||||
|
local legacy_conf="${wgctl_dir}/wgctl.conf"
|
||||||
|
local json_conf="${config_dir}/wgctl.json"
|
||||||
|
|
||||||
|
if [[ -f "$json_conf" && ! -f "$legacy_conf" ]]; then
|
||||||
|
log::wg_warning "Already migrated to new config structure"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log::section "wgctl Config Migration"
|
||||||
|
printf "\n"
|
||||||
|
printf " This will:\n"
|
||||||
|
printf " 1. Create %s/config/ and %s/data/\n" "$wgctl_dir" "$wgctl_dir"
|
||||||
|
printf " 2. Convert wgctl.conf → wgctl.json\n"
|
||||||
|
printf " 3. Move data files to data/\n\n"
|
||||||
|
|
||||||
|
if [[ "$force" != "true" && "$dry_run" != "true" ]]; then
|
||||||
|
read -r -p " Proceed? [y/N] " confirm
|
||||||
|
case "$confirm" in [yY]*) ;; *) log::info "Aborted"; return 0 ;; esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
$dry_run || mkdir -p "$config_dir" "$data_dir"
|
||||||
|
$dry_run && printf " Would create: %s/config/\n" "$wgctl_dir"
|
||||||
|
$dry_run && printf " Would create: %s/data/\n" "$wgctl_dir"
|
||||||
|
|
||||||
|
if [[ -f "$legacy_conf" ]]; then
|
||||||
|
$dry_run || config::_convert_to_json "$legacy_conf" "$json_conf"
|
||||||
|
printf " %s wgctl.conf → config/wgctl.json\n" \
|
||||||
|
"$($dry_run && echo '[dry-run]' || echo '✓')"
|
||||||
|
else
|
||||||
|
log::wg_warning "No wgctl.conf found — creating default wgctl.json"
|
||||||
|
$dry_run || config::_write_default_json "$json_conf"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local -a data_files=(hosts.json services.json subnets.json policies.json)
|
||||||
|
local -a data_dirs=(rules identities groups blocks meta peer-history)
|
||||||
|
|
||||||
|
for f in "${data_files[@]}"; do
|
||||||
|
if [[ -f "${wgctl_dir}/${f}" ]]; then
|
||||||
|
$dry_run || mv "${wgctl_dir}/${f}" "${data_dir}/${f}"
|
||||||
|
printf " %s %s → data/%s\n" \
|
||||||
|
"$($dry_run && echo '[dry-run]' || echo '✓')" "$f" "$f"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
for d in "${data_dirs[@]}"; do
|
||||||
|
if [[ -d "${wgctl_dir}/${d}" ]]; then
|
||||||
|
$dry_run || mv "${wgctl_dir}/${d}" "${data_dir}/${d}"
|
||||||
|
printf " %s %s/ → data/%s/\n" \
|
||||||
|
"$($dry_run && echo '[dry-run]' || echo '✓')" "$d" "$d"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$dry_run" != "true" && -f "$legacy_conf" ]]; then
|
||||||
|
mv "$legacy_conf" "${legacy_conf}.bak"
|
||||||
|
printf " ✓ wgctl.conf → wgctl.conf.bak (backup)\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "\n"
|
||||||
|
[[ "$dry_run" == "true" ]] \
|
||||||
|
&& log::wg_warning "Dry run — no changes made" \
|
||||||
|
|| log::wg_success "Migration complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function config::_convert_to_json() {
|
||||||
|
local legacy_file="$1" output_file="$2"
|
||||||
|
|
||||||
|
# Read legacy conf into variables
|
||||||
|
local wg_interface="wg0" wg_endpoint="" wg_dns="10.0.0.103"
|
||||||
|
local wg_dns_fallback="" wg_port="51820" wg_subnet="10.1.0.0/16"
|
||||||
|
local wg_lan="10.0.0.0/24" wg_hs_check="300" date_format="eu"
|
||||||
|
|
||||||
|
while IFS='=' read -r key value || [[ -n "$key" ]]; do
|
||||||
|
[[ "$key" =~ ^[[:space:]]*# ]] && continue
|
||||||
|
[[ -z "${key// }" ]] && continue
|
||||||
|
key="${key// /}"
|
||||||
|
value="${value// /}"
|
||||||
|
case "$key" in
|
||||||
|
WG_INTERFACE) wg_interface="$value" ;;
|
||||||
|
WG_ENDPOINT) wg_endpoint="$value" ;;
|
||||||
|
WG_DNS) wg_dns="$value" ;;
|
||||||
|
WG_DNS_FALLBACK) wg_dns_fallback="$value" ;;
|
||||||
|
WG_PORT) wg_port="$value" ;;
|
||||||
|
WG_SUBNET) wg_subnet="$value" ;;
|
||||||
|
WG_LAN) wg_lan="$value" ;;
|
||||||
|
WG_HANDSHAKE_CHECK_TIME_SEC) wg_hs_check="$value" ;;
|
||||||
|
DATE_FORMAT) date_format="$value" ;;
|
||||||
|
esac
|
||||||
|
done < "$legacy_file"
|
||||||
|
|
||||||
|
# Build fallback DNS array
|
||||||
|
local dns_fallback_json="[]"
|
||||||
|
if [[ -n "$wg_dns_fallback" ]]; then
|
||||||
|
local fallback_array
|
||||||
|
fallback_array=$(echo "$wg_dns_fallback" | tr ',' '\n' | \
|
||||||
|
while IFS= read -r s; do
|
||||||
|
s="${s// /}"
|
||||||
|
[[ -n "$s" ]] && printf '"%s",' "$s"
|
||||||
|
done | sed 's/,$//')
|
||||||
|
dns_fallback_json="[${fallback_array}]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$output_file")"
|
||||||
|
cat > "$output_file" << JSON
|
||||||
|
{
|
||||||
|
"wireguard": {
|
||||||
|
"interface": "${wg_interface}",
|
||||||
|
"endpoint": "${wg_endpoint}",
|
||||||
|
"port": ${wg_port},
|
||||||
|
"subnet": "${wg_subnet}",
|
||||||
|
"lan": "${wg_lan}"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"primary": "${wg_dns}",
|
||||||
|
"fallback": ${dns_fallback_json}
|
||||||
|
},
|
||||||
|
"handshake": {
|
||||||
|
"check_interval_sec": ${wg_hs_check}
|
||||||
|
},
|
||||||
|
"activity": {
|
||||||
|
"total": {"low": 1000000, "medium": 10000000, "high": 100000000},
|
||||||
|
"current": {"low": 1000000, "medium": 10000000, "high": 100000000}
|
||||||
|
},
|
||||||
|
"display": {
|
||||||
|
"date_format": "${date_format}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
}
|
||||||
|
|
||||||
|
function config::_write_default_json() {
|
||||||
|
local output_file="$1"
|
||||||
|
mkdir -p "$(dirname "$output_file")"
|
||||||
|
cat > "$output_file" << 'JSON'
|
||||||
|
{
|
||||||
|
"wireguard": {
|
||||||
|
"interface": "wg0",
|
||||||
|
"endpoint": "",
|
||||||
|
"port": 51820,
|
||||||
|
"subnet": "10.1.0.0/16",
|
||||||
|
"lan": "10.0.0.0/24"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"primary": "10.0.0.103",
|
||||||
|
"fallback": []
|
||||||
|
},
|
||||||
|
"handshake": {
|
||||||
|
"check_interval_sec": 300
|
||||||
|
},
|
||||||
|
"activity": {
|
||||||
|
"total": {"low": 1000000, "medium": 10000000, "high": 100000000},
|
||||||
|
"current": {"low": 1000000, "medium": 10000000, "high": 100000000}
|
||||||
|
},
|
||||||
|
"display": {
|
||||||
|
"date_format": "eu"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JSON
|
||||||
|
}
|
||||||
17
commands/config/migrate.sh
Normal file
17
commands/config/migrate.sh
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# commands/config/migrate.sh
|
||||||
|
|
||||||
|
function cmd::config::migrate::on_load() {
|
||||||
|
flag::define --force bool "Skip confirmation" section:Options
|
||||||
|
flag::define --dry-run bool "Show what would be done" section:Options
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::config::migrate::run() {
|
||||||
|
flag::parse "$@" || return 1
|
||||||
|
|
||||||
|
local force=false dry_run=false
|
||||||
|
flag::bool --force && force=true
|
||||||
|
flag::bool --dry-run && dry_run=true
|
||||||
|
|
||||||
|
cmd::config::_migrate_impl "$force" "$dry_run"
|
||||||
|
}
|
||||||
24
commands/config/show.sh
Normal file
24
commands/config/show.sh
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# commands/config/show.sh
|
||||||
|
|
||||||
|
function cmd::config::show::on_load() {
|
||||||
|
help::section "Filters"
|
||||||
|
flag::define --name value "Peer name" label:name required:true section:Filters
|
||||||
|
flag::define --type value "Filter by type" label:type section:Filters
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::config::show::run() {
|
||||||
|
flag::parse "$@" || return 1
|
||||||
|
|
||||||
|
local name; name=$(flag::value --name)
|
||||||
|
local type; type=$(flag::value --type)
|
||||||
|
|
||||||
|
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
|
||||||
|
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||||
|
|
||||||
|
local conf
|
||||||
|
conf="$(ctx::clients)/${name}.conf"
|
||||||
|
|
||||||
|
log::section "Client Config: ${name}"
|
||||||
|
cat "$conf"
|
||||||
|
}
|
||||||
|
|
@ -5,19 +5,19 @@ function cmd::list::show::on_load() {
|
||||||
command::mixin json_output [section="Output"]
|
command::mixin json_output [section="Output"]
|
||||||
|
|
||||||
help::section "Filters"
|
help::section "Filters"
|
||||||
flag::define --name value "Show single peer" [label="name", section="Filters"]
|
flag::define --name value "Show single peer" label:name section:Filters
|
||||||
flag::define --type value "Filter by device type" [label="type", section="Filters"]
|
flag::define --type value "Filter by device type" label:type section:Filters
|
||||||
flag::define --rule value "Filter by rule" [label="rule", section="Filters"]
|
flag::define --rule value "Filter by rule" label:rule section:Filters
|
||||||
flag::define --group value "Filter by group" [label="group", section="Filters"]
|
flag::define --group value "Filter by group" label:group section:Filters
|
||||||
flag::define --identity value "Filter by identity" [label="identity", section="Filters"]
|
flag::define --identity value "Filter by identity" label:identity section:Filters
|
||||||
flag::define --online bool "Show online peers only" [section="Filters"]
|
flag::define --online bool "Show online peers only" section:Filters
|
||||||
flag::define --offline bool "Show offline peers only" [section="Filters"]
|
flag::define --offline bool "Show offline peers only" section:Filters
|
||||||
flag::define --restricted bool "Show restricted peers" [section="Filters"]
|
flag::define --restricted bool "Show restricted peers" section:Filters
|
||||||
flag::define --blocked bool "Show blocked peers" [section="Filters"]
|
flag::define --blocked bool "Show blocked peers" section:Filters
|
||||||
flag::define --allowed bool "Show allowed peers" [section="Filters"]
|
flag::define --allowed bool "Show allowed peers" section:Filters
|
||||||
|
|
||||||
help::section "Output"
|
help::section "Output"
|
||||||
flag::define --detailed bool "Show detailed view" [section="Output"]
|
flag::define --detailed bool "Show detailed view" section:Output
|
||||||
|
|
||||||
flag::exclusive --online --offline --blocked --restricted --allowed
|
flag::exclusive --online --offline --blocked --restricted --allowed
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
# commands/logs/remove.sh
|
# commands/logs/remove.sh
|
||||||
|
|
||||||
function cmd::logs::remove::on_load() {
|
function cmd::logs::remove::on_load() {
|
||||||
flag::define --name value "Filter by peer name" [label="name"]
|
flag::define --name value "Filter by peer name" label:name
|
||||||
flag::define --since value "Remove entries since" [label="time"]
|
flag::define --since value "Remove entries since" label:time
|
||||||
flag::define --fw bool "Remove firewall logs only"
|
flag::define --fw bool "Remove firewall logs only"
|
||||||
flag::define --wg bool "Remove WireGuard logs only"
|
flag::define --wg bool "Remove WireGuard logs only"
|
||||||
flag::define --force bool "Skip confirmation"
|
flag::define --force bool "Skip confirmation"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
# commands/logs/rotate.sh
|
# commands/logs/rotate.sh
|
||||||
|
|
||||||
function cmd::logs::rotate::on_load() {
|
function cmd::logs::rotate::on_load() {
|
||||||
flag::define --days value "Remove entries older than N days" [default=30, type=int, min=1, label="days"]
|
flag::define --days value "Remove entries older than N days" default:30 type:int min:1 label:days
|
||||||
flag::define --force bool "Skip confirmation"
|
flag::define --force bool "Skip confirmation"
|
||||||
flag::define --dry-run bool "Show what would be removed"
|
flag::define --dry-run bool "Show what would be removed"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,23 +8,23 @@ function cmd::logs::show::on_load() {
|
||||||
command::mixin json_output [section="Output"]
|
command::mixin json_output [section="Output"]
|
||||||
|
|
||||||
help::section "Filters"
|
help::section "Filters"
|
||||||
flag::define --name value "Filter by peer name" [label="name", section="Filters"]
|
flag::define --name value "Filter by peer name" label:name section:Filters
|
||||||
flag::define --type value "Filter by device type" [label="type", section="Filters"]
|
flag::define --type value "Filter by device type" label:type section:Filters
|
||||||
flag::define --since value "Show events since (2h, 7d)" [label="time", section="Filters"]
|
flag::define --since value "Show events since (2h, 7d)" label:time section:Filters
|
||||||
flag::define --service value "Filter by service/IP" [label="service", section="Filters"]
|
flag::define --service value "Filter by service/IP" label:service section:Filters
|
||||||
flag::define --event value "Filter wg events" [label="type", section="Filters"]
|
flag::define --event value "Filter wg events" label:type section:Filters
|
||||||
flag::define --fw bool "Show firewall drops only" [section="Filters"]
|
flag::define --fw bool "Show firewall drops only" section:Filters
|
||||||
flag::define --wg bool "Show WireGuard events only" [section="Filters"]
|
flag::define --wg bool "Show WireGuard events only" section:Filters
|
||||||
flag::define --merged bool "Show all events interleaved" [section="Filters"]
|
flag::define --merged bool "Show all events interleaved" section:Filters
|
||||||
|
|
||||||
help::section "Output"
|
help::section "Output"
|
||||||
flag::define --limit value "Max results per source" [default=50, type=int, min=1, section="Output"]
|
flag::define --limit value "Max results per source" default:50 type:int min:1 section:Output
|
||||||
flag::define --ascending bool "Sort ascending" [section="Output"]
|
flag::define --ascending bool "Sort ascending" section:Output
|
||||||
flag::define --descending bool "Sort descending" [section="Output"]
|
flag::define --descending bool "Sort descending" section:Output
|
||||||
flag::define --resolved bool "Resolve endpoints" [section="Output"]
|
flag::define --resolved bool "Resolve endpoints" section:Output
|
||||||
flag::define --detailed bool "Show per-event detail" [section="Output"]
|
flag::define --detailed bool "Show per-event detail" section:Output
|
||||||
flag::define --raw bool "Skip service resolution" [section="Output"]
|
flag::define --raw bool "Skip service resolution" section:Output
|
||||||
flag::define --follow bool "Follow live" [section="Output"]
|
flag::define --follow bool "Follow live" section:Output
|
||||||
|
|
||||||
flag::exclusive --fw --wg
|
flag::exclusive --fw --wg
|
||||||
flag::exclusive --ascending --descending
|
flag::exclusive --ascending --descending
|
||||||
|
|
|
||||||
9
commands/peer/peer.sh
Normal file
9
commands/peer/peer.sh
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# commands/peer/peer.sh — router only
|
||||||
|
|
||||||
|
function cmd::peer::on_load() {
|
||||||
|
command::define update-dns "Update DNS servers in peer configs" [dns]
|
||||||
|
command::define update-tunnel "Update tunnel mode for peer configs" [tunnel]
|
||||||
|
}
|
||||||
|
|
||||||
|
hook::on "command:help:peer" command::help::auto
|
||||||
62
commands/peer/update-dns.sh
Normal file
62
commands/peer/update-dns.sh
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# commands/peer/update-dns.sh
|
||||||
|
|
||||||
|
function cmd::peer::update_dns::on_load() {
|
||||||
|
help::section "Target"
|
||||||
|
flag::define --name value "Peer name" label:name section:Target
|
||||||
|
flag::define --type value "Filter by type" label:type section:Target
|
||||||
|
flag::define --all bool "Update all peers" section:Target
|
||||||
|
|
||||||
|
help::section "DNS"
|
||||||
|
flag::define --dns value "Primary DNS server" label:ip section:DNS
|
||||||
|
flag::define --fallback-dns value "Fallback DNS servers" label:ips section:DNS
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::peer::update_dns::run() {
|
||||||
|
flag::parse "$@" || return 1
|
||||||
|
|
||||||
|
local name; name=$(flag::value --name)
|
||||||
|
local type; type=$(flag::value --type)
|
||||||
|
local dns; dns=$(flag::value --dns)
|
||||||
|
local fallback_dns; fallback_dns=$(flag::value --fallback-dns)
|
||||||
|
local all=false
|
||||||
|
flag::bool --all && all=true
|
||||||
|
|
||||||
|
[[ -z "$name" && "$all" == "false" ]] && \
|
||||||
|
log::error "Specify --name or --all" && return 1
|
||||||
|
|
||||||
|
local primary="${dns:-$(config::dns)}"
|
||||||
|
local fallback="${fallback_dns:-$(config::dns_fallback)}"
|
||||||
|
local dns_string
|
||||||
|
if [[ -n "$fallback" ]]; then
|
||||||
|
dns_string="${primary}, ${fallback}"
|
||||||
|
else
|
||||||
|
dns_string="$primary"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local -a peers=()
|
||||||
|
if $all; then
|
||||||
|
while IFS= read -r conf; do
|
||||||
|
peers+=("$(basename "$conf" .conf)")
|
||||||
|
done < <(find "$(ctx::clients)" -name "*.conf" 2>/dev/null)
|
||||||
|
else
|
||||||
|
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||||
|
peers=("$name")
|
||||||
|
fi
|
||||||
|
|
||||||
|
local updated=0
|
||||||
|
for peer_name in "${peers[@]}"; do
|
||||||
|
local conf
|
||||||
|
conf="$(ctx::clients)/${peer_name}.conf"
|
||||||
|
[[ ! -f "$conf" ]] && continue
|
||||||
|
if grep -q "^DNS" "$conf"; then
|
||||||
|
sed -i "s|^DNS = .*|DNS = ${dns_string}|" "$conf"
|
||||||
|
else
|
||||||
|
sed -i "/^Address/a DNS = ${dns_string}" "$conf"
|
||||||
|
fi
|
||||||
|
(( updated++ )) || true
|
||||||
|
log::debug "Updated DNS for: ${peer_name}"
|
||||||
|
done
|
||||||
|
|
||||||
|
log::wg_success "Updated DNS to '${dns_string}' for ${updated} peer(s)"
|
||||||
|
}
|
||||||
57
commands/peer/update-tunnel.sh
Normal file
57
commands/peer/update-tunnel.sh
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# commands/peer/update-tunnel.sh
|
||||||
|
|
||||||
|
function cmd::peer::update_tunnel::on_load() {
|
||||||
|
help::section "Target"
|
||||||
|
flag::define --name value "desc" label:name section:Filters
|
||||||
|
flag::define --type value "Filter by type" label:type section:Target
|
||||||
|
flag::define --all bool "Update all peers" section:Target
|
||||||
|
|
||||||
|
help::section "Options"
|
||||||
|
flag::define --mode value "Tunnel mode" label:mode required choices:split,full section:Options
|
||||||
|
flag::define --force bool "Skip confirmation for --all" section:Options
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::peer::update_tunnel::run() {
|
||||||
|
flag::parse "$@" || return 1
|
||||||
|
|
||||||
|
local name; name=$(flag::value --name)
|
||||||
|
local type; type=$(flag::value --type)
|
||||||
|
local mode; mode=$(flag::value --mode)
|
||||||
|
local all=false force=false
|
||||||
|
flag::bool --all && all=true
|
||||||
|
flag::bool --force && force=true
|
||||||
|
|
||||||
|
[[ -z "$name" && "$all" == "false" ]] && \
|
||||||
|
log::error "Specify --name or --all" && return 1
|
||||||
|
|
||||||
|
local allowed_ips
|
||||||
|
allowed_ips=$(config::allowed_ips_for "$mode")
|
||||||
|
|
||||||
|
local -a peers=()
|
||||||
|
if $all; then
|
||||||
|
if ! $force; then
|
||||||
|
read -r -p "Update tunnel mode to '${mode}' for ALL peers? [y/N] " confirm
|
||||||
|
case "$confirm" in [yY]*) ;; *) log::info "Aborted"; return 0 ;; esac
|
||||||
|
fi
|
||||||
|
while IFS= read -r conf; do
|
||||||
|
peers+=("$(basename "$conf" .conf)")
|
||||||
|
done < <(find "$(ctx::clients)" -name "*.conf" 2>/dev/null)
|
||||||
|
else
|
||||||
|
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||||
|
peers=("$name")
|
||||||
|
fi
|
||||||
|
|
||||||
|
local updated=0
|
||||||
|
for peer_name in "${peers[@]}"; do
|
||||||
|
local conf
|
||||||
|
conf="$(ctx::clients)/${peer_name}.conf"
|
||||||
|
[[ ! -f "$conf" ]] && continue
|
||||||
|
sed -i "s|^AllowedIPs = .*|AllowedIPs = ${allowed_ips}|" "$conf"
|
||||||
|
(( updated++ )) || true
|
||||||
|
log::debug "Updated tunnel for: ${peer_name}"
|
||||||
|
done
|
||||||
|
|
||||||
|
log::wg_success "Updated tunnel to '${mode}' (${allowed_ips}) for ${updated} peer(s)"
|
||||||
|
log::wg "Peers must reconnect to apply the new tunnel mode"
|
||||||
|
}
|
||||||
|
|
@ -380,9 +380,9 @@ function cmd::test::section_identity() {
|
||||||
function cmd::test::section_activity() {
|
function cmd::test::section_activity() {
|
||||||
test::section "Activity"
|
test::section "Activity"
|
||||||
cmd::test::run_cmd "activity" "Activity" activity
|
cmd::test::run_cmd "activity" "Activity" activity
|
||||||
cmd::test::run_cmd "activity --json" '"peers":' activity --json
|
cmd::test::run_cmd "activity --json" '"command":"activity"' activity --json
|
||||||
cmd::test::run_cmd "activity --json services" '"services":' activity --json
|
cmd::test::run_cmd "activity --json has data" '"data":' activity --json
|
||||||
cmd::test::run_cmd "activity --json rx" '"rx":' activity --json
|
cmd::test::run_cmd "activity --json has rx" '"rx":' activity --json
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::test::section_policy() {
|
function cmd::test::section_policy() {
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,21 @@
|
||||||
|
|
||||||
function cmd::unblock::show::on_load() {
|
function cmd::unblock::show::on_load() {
|
||||||
help::section "Target"
|
help::section "Target"
|
||||||
flag::define --name value "Peer name to unblock" [label="name", section="Target"]
|
flag::define --name value "Peer name to unblock" label:name section:Target
|
||||||
flag::define --identity value "Unblock all peers in identity" [label="identity", section="Target"]
|
flag::define --identity value "Unblock all peers in identity" label:identity section:Target
|
||||||
flag::define --type value "Filter by device type" [label="type", section="Target"]
|
flag::define --type value "Filter by device type" label:type section:Target
|
||||||
|
|
||||||
help::section "Rules"
|
help::section "Rules"
|
||||||
flag::define --ip[] "Unblock specific IP" [label="ip", section="Rules"]
|
flag::define --ip[] "Unblock specific IP" label:ip section:Rules
|
||||||
flag::define --subnet[] "Unblock specific subnet" [label="subnet", section="Rules"]
|
flag::define --subnet[] "Unblock specific subnet" label:subnet section:Rules
|
||||||
flag::define --port[] "Unblock specific port (ip:port:proto)" [label="port", section="Rules"]
|
flag::define --port[] "Unblock specific port (ip:port:proto)" label:port section:Rules
|
||||||
flag::define --service[] "Unblock by service name" [label="service", section="Rules"]
|
flag::define --service[] "Unblock by service name" label:service section:Rules
|
||||||
flag::define --all bool "Unblock all rules" [section="Rules"]
|
flag::define --all bool "Unblock all rules" section:Rules
|
||||||
|
|
||||||
help::section "Options"
|
help::section "Options"
|
||||||
flag::define --reason value "Reason for unblock (recorded in history)" [label="reason", section="Options"]
|
flag::define --reason value "Reason for unblock (recorded in history)" label:reason section:Options
|
||||||
flag::define --force bool "Skip confirmation" [section="Options"]
|
flag::define --force bool "Skip confirmation" section:Options
|
||||||
flag::define --quiet bool "Suppress output" [section="Options"]
|
flag::define --quiet bool "Suppress output" section:Options
|
||||||
|
|
||||||
flag::exclusive --name --identity
|
flag::exclusive --name --identity
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,7 @@ function command::route() {
|
||||||
# Lazy-loads a subcommand's file and calls its on_load
|
# Lazy-loads a subcommand's file and calls its on_load
|
||||||
function command::load_subcmd() {
|
function command::load_subcmd() {
|
||||||
local cmd="$1" subcmd="$2"
|
local cmd="$1" subcmd="$2"
|
||||||
|
# log::debug "load_subcmd: cmd=$cmd subcmd=$subcmd"
|
||||||
[[ -z "$cmd" || -z "$subcmd" ]] && return 1
|
[[ -z "$cmd" || -z "$subcmd" ]] && return 1
|
||||||
|
|
||||||
# Determine file path — commands/<cmd>/<subcmd>.sh
|
# Determine file path — commands/<cmd>/<subcmd>.sh
|
||||||
|
|
@ -169,13 +170,20 @@ function command::load_subcmd() {
|
||||||
cmd_dir="${WGCTL_DIR}/commands/${cmd}"
|
cmd_dir="${WGCTL_DIR}/commands/${cmd}"
|
||||||
|
|
||||||
local subcmd_file="${cmd_dir}/${subcmd}.sh"
|
local subcmd_file="${cmd_dir}/${subcmd}.sh"
|
||||||
|
# log::debug "looking for: $subcmd_file exists=$([ -f "$subcmd_file" ] && echo yes || echo no)"
|
||||||
[[ ! -f "$subcmd_file" ]] && return 1
|
[[ ! -f "$subcmd_file" ]] && return 1
|
||||||
|
|
||||||
|
local fn_subcmd="${subcmd//-/_}"
|
||||||
|
local on_load_fn="cmd::${cmd}::${fn_subcmd}::on_load"
|
||||||
|
|
||||||
_CURRENT_LOADING_CMD="${cmd}::${subcmd}"
|
_CURRENT_LOADING_CMD="${cmd}::${subcmd}"
|
||||||
_CURRENT_COMMAND="${cmd}::${subcmd}"
|
_CURRENT_COMMAND="${cmd}::${subcmd}"
|
||||||
source "$subcmd_file"
|
source "$subcmd_file"
|
||||||
|
|
||||||
local on_load_fn="cmd::${cmd}::${subcmd}::on_load"
|
# log::debug "sourced: $subcmd_file exit=$?"
|
||||||
|
# log::debug "on_load_fn=$on_load_fn exists=$(declare -f "$on_load_fn" &>/dev/null && echo yes || echo no)"
|
||||||
|
|
||||||
|
# log::debug "load_subcmd: calling on_load, _CURRENT_COMMAND=$_CURRENT_COMMAND _CURRENT_LOADING_CMD=$_CURRENT_LOADING_CMD"
|
||||||
if declare -f "$on_load_fn" &>/dev/null; then
|
if declare -f "$on_load_fn" &>/dev/null; then
|
||||||
"$on_load_fn"
|
"$on_load_fn"
|
||||||
fi
|
fi
|
||||||
|
|
@ -235,7 +243,8 @@ function command::run_routed() {
|
||||||
command::_preprocess_flags final_args
|
command::_preprocess_flags final_args
|
||||||
|
|
||||||
# Run
|
# Run
|
||||||
local run_fn="cmd::${cmd}::${subcmd}::run"
|
local fn_subcmd="${subcmd//-/_}"
|
||||||
|
local run_fn="cmd::${cmd}::${fn_subcmd}::run"
|
||||||
if declare -f "$run_fn" &>/dev/null; then
|
if declare -f "$run_fn" &>/dev/null; then
|
||||||
if [[ ${#final_args[@]} -gt 0 ]]; then
|
if [[ ${#final_args[@]} -gt 0 ]]; then
|
||||||
"$run_fn" "${final_args[@]}"
|
"$run_fn" "${final_args[@]}"
|
||||||
|
|
@ -325,23 +334,22 @@ function command::run() {
|
||||||
local subcmd="$_ROUTED_SUBCMD"
|
local subcmd="$_ROUTED_SUBCMD"
|
||||||
local -a routed_args=("${_ROUTED_ARGS[@]:-}")
|
local -a routed_args=("${_ROUTED_ARGS[@]:-}")
|
||||||
|
|
||||||
# Lazy load subcommand file
|
if [[ -z "$subcmd" ]]; then
|
||||||
local subcmd_file="$(ctx::commands)/${cmd}/${subcmd}.sh"
|
hook::fire "command:help:${cmd}" "$cmd" ""
|
||||||
if [[ -f "$subcmd_file" ]]; then
|
return 0
|
||||||
_CURRENT_LOADING_CMD="${cmd}::${subcmd}"
|
|
||||||
_CURRENT_COMMAND="${cmd}::${subcmd}"
|
|
||||||
source "$subcmd_file"
|
|
||||||
core::call_if_exists "cmd::${cmd}::${subcmd}::on_load"
|
|
||||||
_CURRENT_LOADING_CMD=""
|
|
||||||
|
|
||||||
for arg in "$@"; do
|
|
||||||
[[ "$arg" == "--help" || "$arg" == "-h" ]] && {
|
|
||||||
hook::fire "command:help:${cmd}" "$cmd" "$_ROUTED_SUBCMD"
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
done
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# log::debug "about to load_subcmd: cmd=$cmd subcmd=$subcmd"
|
||||||
|
# Lazy load subcommand file
|
||||||
|
command::load_subcmd "$cmd" "$subcmd"
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
[[ "$arg" == "--help" || "$arg" == "-h" ]] && {
|
||||||
|
hook::fire "command:help:${cmd}" "$cmd" "$_ROUTED_SUBCMD"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
command::run_routed "$cmd" "$subcmd" "${routed_args[@]:-}"
|
command::run_routed "$cmd" "$subcmd" "${routed_args[@]:-}"
|
||||||
return $?
|
return $?
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ function flag::_extract_constraint() {
|
||||||
function flag::_parse_and_cache() {
|
function flag::_parse_and_cache() {
|
||||||
local key="$1" constraints="$2"
|
local key="$1" constraints="$2"
|
||||||
[[ -z "$constraints" ]] && return 0
|
[[ -z "$constraints" ]] && return 0
|
||||||
|
# log::debug "parse_and_cache: key=$key"
|
||||||
local v
|
local v
|
||||||
v=$(flag::_extract_constraint "$constraints" "default")
|
v=$(flag::_extract_constraint "$constraints" "default")
|
||||||
[[ -n "$v" ]] && _FLAG_C_DEFAULT["$key"]="$v"
|
[[ -n "$v" ]] && _FLAG_C_DEFAULT["$key"]="$v"
|
||||||
|
|
@ -58,6 +59,7 @@ function flag::_parse_and_cache() {
|
||||||
v=$(flag::_extract_constraint "$constraints" "max")
|
v=$(flag::_extract_constraint "$constraints" "max")
|
||||||
[[ -n "$v" ]] && _FLAG_C_MAX["$key"]="$v"
|
[[ -n "$v" ]] && _FLAG_C_MAX["$key"]="$v"
|
||||||
v=$(flag::_extract_constraint "$constraints" "choices")
|
v=$(flag::_extract_constraint "$constraints" "choices")
|
||||||
|
# log::debug "parse_and_cache: choices='$v'"
|
||||||
[[ -n "$v" ]] && _FLAG_C_CHOICES["$key"]="$v"
|
[[ -n "$v" ]] && _FLAG_C_CHOICES["$key"]="$v"
|
||||||
v=$(flag::_extract_constraint "$constraints" "required")
|
v=$(flag::_extract_constraint "$constraints" "required")
|
||||||
[[ "$v" == "true" ]] && _FLAG_C_REQUIRED["$key"]="true"
|
[[ "$v" == "true" ]] && _FLAG_C_REQUIRED["$key"]="true"
|
||||||
|
|
@ -67,6 +69,38 @@ function flag::_parse_and_cache() {
|
||||||
[[ -n "$v" ]] && _FLAG_C_SECTION["$key"]="$v"
|
[[ -n "$v" ]] && _FLAG_C_SECTION["$key"]="$v"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Constraints ────────────────────────────────────────
|
||||||
|
|
||||||
|
# flag::set_constraint <flag> <key> [value]
|
||||||
|
# Set a constraint on an already-defined flag (Option B syntax)
|
||||||
|
# Examples:
|
||||||
|
# flag::set_constraint --mode choices "split|full"
|
||||||
|
# flag::set_constraint --mode required
|
||||||
|
# flag::set_constraint --mode section Options
|
||||||
|
function flag::set_constraint() {
|
||||||
|
local flag="$1" k="$2" v="${3:-}"
|
||||||
|
local ctx="${_CURRENT_COMMAND:-__global__}"
|
||||||
|
local key="${ctx}:${flag}"
|
||||||
|
|
||||||
|
[[ -z "${_FLAG_REGISTRY[$key]+x}" ]] && \
|
||||||
|
log::error "flag::set_constraint: flag not defined: ${flag}" && return 1
|
||||||
|
|
||||||
|
case "$k" in
|
||||||
|
label) _FLAG_C_LABEL["$key"]="$v" ;;
|
||||||
|
default) _FLAG_C_DEFAULT["$key"]="$v" ;;
|
||||||
|
type) _FLAG_C_TYPE["$key"]="$v" ;;
|
||||||
|
min) _FLAG_C_MIN["$key"]="$v" ;;
|
||||||
|
max) _FLAG_C_MAX["$key"]="$v" ;;
|
||||||
|
choices) _FLAG_C_CHOICES["$key"]="$v" ;;
|
||||||
|
section)
|
||||||
|
_FLAG_C_SECTION["$key"]="$v"
|
||||||
|
declare -f help::_assign_flag_from_cache &>/dev/null && \
|
||||||
|
help::_assign_flag_from_cache "$flag" "$v"
|
||||||
|
;;
|
||||||
|
required) _FLAG_C_REQUIRED["$key"]="true" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
# ── Context ───────────────────────────────────────────────────────────────
|
# ── Context ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function flag::_context() { printf '%s' "${_CURRENT_COMMAND:-__global__}"; }
|
function flag::_context() { printf '%s' "${_CURRENT_COMMAND:-__global__}"; }
|
||||||
|
|
@ -74,41 +108,70 @@ function flag::_key() { printf '%s:%s' "${_CURRENT_COMMAND:-__global__}" "$1
|
||||||
|
|
||||||
# ── Registration ──────────────────────────────────────────────────────────
|
# ── Registration ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# flag::define <flag> [bool|value] "description" [constraints...]
|
||||||
|
# flag::define <flag[]> "description" [constraints...]
|
||||||
|
#
|
||||||
|
# Constraints (positional, after description):
|
||||||
|
# label:name Display label in help
|
||||||
|
# default:50 Default value
|
||||||
|
# type:int Value type (int, string)
|
||||||
|
# min:1 Minimum value (int)
|
||||||
|
# max:100 Maximum value (int)
|
||||||
|
# choices:a|b|c Allowed values (pipe-separated)
|
||||||
|
# section:Filters Help section
|
||||||
|
# required Mark as required (no value needed)
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# flag::define --fw bool "Firewall only" section:Filters
|
||||||
|
# flag::define --limit value "Max results" default:50 type:int min:1 section:Output
|
||||||
|
# flag::define --mode value "Tunnel mode" required choices:split|full section:Options
|
||||||
|
# flag::define --svc[] "Exclude service" label:service section:Filters
|
||||||
function flag::define() {
|
function flag::define() {
|
||||||
local raw_flag="$1" is_array=false
|
local raw_flag="$1"
|
||||||
|
local is_array=false
|
||||||
|
|
||||||
if [[ "$raw_flag" == *"[]" ]]; then
|
if [[ "$raw_flag" == *"[]" ]]; then
|
||||||
is_array=true; raw_flag="${raw_flag%[]}"
|
is_array=true
|
||||||
|
raw_flag="${raw_flag%[]}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local type description constraints=""
|
local type description
|
||||||
|
local -a constraints=()
|
||||||
|
|
||||||
if $is_array; then
|
if $is_array; then
|
||||||
type="array"; description="${2:-}"; constraints="${3:-}"
|
type="array"
|
||||||
|
description="${2:-}"
|
||||||
|
shift 2
|
||||||
|
constraints=("$@")
|
||||||
else
|
else
|
||||||
type="${2:-bool}"; description="${3:-}"; constraints="${4:-}"
|
type="${2:-bool}"
|
||||||
|
description="${3:-}"
|
||||||
|
shift 3
|
||||||
|
constraints=("$@")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local ctx="${_CURRENT_COMMAND:-__global__}"
|
local ctx="${_CURRENT_COMMAND:-__global__}"
|
||||||
local key="${ctx}:${raw_flag}"
|
local key="${ctx}:${raw_flag}"
|
||||||
|
|
||||||
_FLAG_REGISTRY["$key"]="${type}|${description}"
|
_FLAG_REGISTRY["$key"]="${type}|${description}"
|
||||||
|
|
||||||
# Per-command index — fast lookup in flag::parse
|
|
||||||
_FLAG_INDEX["$ctx"]+=" ${raw_flag}"
|
_FLAG_INDEX["$ctx"]+=" ${raw_flag}"
|
||||||
|
|
||||||
# Pre-parse constraints
|
# Parse constraints
|
||||||
flag::_parse_and_cache "$key" "$constraints"
|
flag::_parse_constraints_from_args "$key" "${constraints[@]:-}"
|
||||||
|
|
||||||
# Bool default
|
# Bool default
|
||||||
if [[ "$type" == "bool" && -z "${_FLAG_C_DEFAULT[$key]:-}" ]]; then
|
if [[ "$type" == "bool" && -z "${_FLAG_C_DEFAULT[$key]:-}" ]]; then
|
||||||
_FLAG_C_DEFAULT["$key"]="false"
|
_FLAG_C_DEFAULT["$key"]="false"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Help section assignment (no subshell — uses cached value)
|
# Help section — constraint takes precedence, then active section
|
||||||
if declare -f help::_assign_flag_from_cache &>/dev/null; then
|
local section="${_FLAG_C_SECTION[$key]:-${_CURRENT_HELP_SECTION:-}}"
|
||||||
help::_assign_flag_from_cache "$raw_flag" "${_FLAG_C_SECTION[$key]:-}"
|
if [[ -n "$section" ]]; then
|
||||||
|
_FLAG_C_SECTION["$key"]="$section"
|
||||||
|
declare -f help::_assign_flag_from_cache &>/dev/null && \
|
||||||
|
help::_assign_flag_from_cache "$raw_flag" "$section"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_FLAG_COMPLETION["$raw_flag"]=1
|
_FLAG_COMPLETION["$raw_flag"]=1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,6 +181,7 @@ function flag::register() { _FLAG_COMPLETION["${1:-}"]=1; }
|
||||||
|
|
||||||
function flag::parse() {
|
function flag::parse() {
|
||||||
local ctx="${_CURRENT_COMMAND:-__global__}"
|
local ctx="${_CURRENT_COMMAND:-__global__}"
|
||||||
|
# log::debug "ctx=$ctx index='${_FLAG_INDEX[$ctx]:-EMPTY}'" >&2
|
||||||
|
|
||||||
# Reset runtime
|
# Reset runtime
|
||||||
_FLAG_VALUES=(); _FLAG_ARRAYS=(); _FLAG_SET=(); _FLAG_ARGS=()
|
_FLAG_VALUES=(); _FLAG_ARRAYS=(); _FLAG_SET=(); _FLAG_ARGS=()
|
||||||
|
|
@ -189,17 +253,20 @@ function flag::parse() {
|
||||||
log::error "Flag ${arg} maximum is ${max}, got: ${val}" && return 1
|
log::error "Flag ${arg} maximum is ${max}, got: ${val}" && return 1
|
||||||
fi
|
fi
|
||||||
local choices="${_FLAG_C_CHOICES[$key]:-}"
|
local choices="${_FLAG_C_CHOICES[$key]:-}"
|
||||||
if [[ -n "$choices" ]]; then
|
# log::debug "choices check: flag=$arg val=$val choices='${_FLAG_C_CHOICES[$key]:-EMPTY}'"
|
||||||
local valid=false choice
|
if [[ -n "$choices" ]]; then
|
||||||
local IFS='|'
|
local valid=false
|
||||||
for choice in $choices; do
|
local choice
|
||||||
[[ "$val" == "$choice" ]] && valid=true && break
|
local saved_ifs="$IFS"
|
||||||
done
|
IFS=',' read -ra choice_list <<< "$choices"
|
||||||
unset IFS
|
IFS="$saved_ifs"
|
||||||
if ! $valid; then
|
for choice in "${choice_list[@]}"; do
|
||||||
log::error "Flag ${arg} must be one of: ${choices//|/, }, got: ${val}"
|
[[ "$val" == "$choice" ]] && valid=true && break
|
||||||
return 1
|
done
|
||||||
fi
|
if ! $valid; then
|
||||||
|
log::error "Flag ${arg} must be one of: ${choices//|/, }, got: ${val}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
_FLAG_VALUES["$arg"]="$val"
|
_FLAG_VALUES["$arg"]="$val"
|
||||||
_FLAG_SET["$arg"]="1"
|
_FLAG_SET["$arg"]="1"
|
||||||
|
|
@ -230,7 +297,10 @@ function flag::parse() {
|
||||||
done
|
done
|
||||||
|
|
||||||
# Validate exclusive groups
|
# Validate exclusive groups
|
||||||
local groups="${_FLAG_EXCLUSIVE_GROUPS[${_CURRENT_COMMAND%%::*}]:-}"
|
local cmd="${_CURRENT_COMMAND%%::*}"
|
||||||
|
local groups=""
|
||||||
|
[[ -n "$cmd" ]] && groups="${_FLAG_EXCLUSIVE_GROUPS[$cmd]:-}"
|
||||||
|
|
||||||
if [[ -n "$groups" ]]; then
|
if [[ -n "$groups" ]]; then
|
||||||
local group
|
local group
|
||||||
while IFS= read -r group; do
|
while IFS= read -r group; do
|
||||||
|
|
@ -246,10 +316,43 @@ function flag::parse() {
|
||||||
fi
|
fi
|
||||||
done < <(printf '%s' "$groups" | tr '|' '\n')
|
done < <(printf '%s' "$groups" | tr '|' '\n')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# flag::_parse_constraints_from_args <key> <constraint_args...>
|
||||||
|
# Parse variadic constraint args: label:x required choices:a|b section:x
|
||||||
|
function flag::_parse_constraints_from_args() {
|
||||||
|
local key="$1"
|
||||||
|
shift
|
||||||
|
local arg k v
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
# Handle bare keywords (no colon) — e.g. "required"
|
||||||
|
if [[ "$arg" != *:* ]]; then
|
||||||
|
case "$arg" in
|
||||||
|
required) _FLAG_C_REQUIRED["$key"]="true" ;;
|
||||||
|
optional) ;; # default, no-op
|
||||||
|
esac
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
k="${arg%%:*}"
|
||||||
|
v="${arg#*:}"
|
||||||
|
|
||||||
|
case "$k" in
|
||||||
|
label) _FLAG_C_LABEL["$key"]="$v" ;;
|
||||||
|
default) _FLAG_C_DEFAULT["$key"]="$v" ;;
|
||||||
|
type) _FLAG_C_TYPE["$key"]="$v" ;;
|
||||||
|
min) _FLAG_C_MIN["$key"]="$v" ;;
|
||||||
|
max) _FLAG_C_MAX["$key"]="$v" ;;
|
||||||
|
choices) _FLAG_C_CHOICES["$key"]="$v" ;;
|
||||||
|
section) _FLAG_C_SECTION["$key"]="$v" ;;
|
||||||
|
required) _FLAG_C_REQUIRED["$key"]="$v" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ── Accessors ─────────────────────────────────────────────────────────────
|
# ── Accessors ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function flag::bool() { [[ "${_FLAG_VALUES[$1]:-false}" == "true" ]]; }
|
function flag::bool() { [[ "${_FLAG_VALUES[$1]:-false}" == "true" ]]; }
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue