hosts.command.sh
· 9.5 KiB · Bash
Ham
#!/usr/bin/env bash
# hosts.command.sh — manage host/IP display name mappings
# ============================================
# Lifecycle
# ============================================
function cmd::hosts::on_load() {
flag::register --ip
flag::register --subnet
flag::register --port
flag::register --name
flag::register --desc
flag::register --tag
flag::register --tags
flag::register --force
command::mixin json_output
}
# ============================================
# Help
# ============================================
function cmd::hosts::help() {
cat <<EOF
Usage: wgctl hosts <subcommand> [options]
Manage host display names for IP resolution in logs, watch, and activity.
Maps IPs, subnets, and ports to human-readable names.
Subcommands:
list List all host entries
show --ip <ip> Show host entry details
show --subnet <cidr> Show subnet entry details
show --port <port> Show port entry details
add --ip <ip> --name <name> Add a host entry
add --subnet <cidr> --name <name>
Add a subnet entry
add --port <port> --name <name>
Add a port entry
rm --ip <ip> Remove a host entry
rm --subnet <cidr> Remove a subnet entry
rm --port <port> Remove a port entry
Options for add:
--ip <ip> IP address to map
--subnet <cidr> Subnet CIDR to map (e.g. 10.0.0.0/24)
--port <port> Port number to map (e.g. 443)
--name <name> Display name (e.g. vodafone-wan)
--desc <description> Optional description
--tag <tag> Tag (repeatable)
--tags <tag1,tag2> Tags (comma-separated)
Options for rm:
--force Skip confirmation
Examples:
wgctl hosts list
wgctl hosts add --ip 148.69.46.73 --name vodafone-wan --desc "Vodafone WAN"
wgctl hosts add --ip 94.63.0.129 --name nuno-home --tags home,isp
wgctl hosts add --subnet 10.0.0.0/24 --name lan --desc "Local LAN"
wgctl hosts add --port 443 --name https
wgctl hosts show --ip 148.69.46.73
wgctl hosts rm --ip 148.69.46.73
EOF
}
# ============================================
# Run
# ============================================
function cmd::hosts::run() {
local subcmd="${1:-list}"
shift || true
if command::json; then
cmd::hosts::_output_json
return 0
fi
case "$subcmd" in
list) cmd::hosts::list "$@" ;;
show) cmd::hosts::show "$@" ;;
add) cmd::hosts::add "$@" ;;
rm|remove|del) cmd::hosts::rm "$@" ;;
help) cmd::hosts::help ;;
*)
log::error "Unknown subcommand: '${subcmd}'"
cmd::hosts::help
return 1 ;;
esac
}
# ============================================
# List
# ============================================
function cmd::hosts::list() {
local filter_tag=""
while [[ $# -gt 0 ]]; do
case "$1" in
--tag) filter_tag="$2"; shift 2 ;;
--help) cmd::hosts::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
local hosts_file
hosts_file="$(ctx::hosts)"
if [[ ! -f "$hosts_file" ]]; then
log::wg_warning "No hosts configured. Use 'wgctl hosts add' to add one."
return 0
fi
local data
data=$(json::hosts_list "$hosts_file" 2>/dev/null)
[[ -z "$data" ]] && log::wg_warning "No hosts configured." && return 0
# Apply tag filter to data first
local filtered_data=""
while IFS='|' read -r type key name desc tags; do
[[ -z "$type" ]] && continue
[[ -n "$filter_tag" && "$tags" != *"$filter_tag"* ]] && continue
filtered_data+="${type}|${key}|${name}|${desc}|${tags}"$'\n'
done <<< "$data"
[[ -z "$filtered_data" ]] && log::wg_warning "No hosts found." && return 0
# Measure column widths from filtered data
local w_key=15 w_name=16 w_desc=10
while IFS='|' read -r type key name desc tags; do
[[ -z "$type" ]] && continue
(( ${#key} > w_key )) && w_key=${#key}
(( ${#name} > w_name )) && w_name=${#name}
local desc_len=${#desc}
[[ -z "$desc" ]] && desc_len=1 # "—" = 1 visible char
(( desc_len > w_desc )) && w_desc=$desc_len
done <<< "$filtered_data"
(( w_key += 2 ))
(( w_name += 2 ))
(( w_desc += 2 ))
log::section "Host Mappings"
echo ""
if display::is_table "hosts_list"; then
cmd::hosts::_render_table "$data"
return 0
fi
local last_type="" found=false
while IFS='|' read -r type key name desc tags; do
[[ -z "$type" ]] && continue
found=true
# Section header when type changes
if [[ "$type" != "$last_type" ]]; then
[[ -n "$last_type" ]] && echo ""
ui::hosts::section_header "$type"
last_type="$type"
fi
ui::hosts::list_row "$type" "$key" "$name" "$desc" "$tags" \
"$w_key" "$w_name" "$w_desc"
done <<< "$filtered_data"
$found || log::wg_warning "No hosts configured."
echo ""
}
function cmd::hosts::_render_table() {
local data="${1:-}"
[[ -z "$data" ]] && return 0
ui::hosts::list_header_table
while IFS='|' read -r type key name desc tags; do
[[ -z "$type" ]] && continue
ui::hosts::list_row_table "$type" "$key" "$name" "$desc" "$tags"
done <<< "$data"
}
# ============================================
# Show
# ============================================
function cmd::hosts::show() {
local ip="" subnet="" port=""
while [[ $# -gt 0 ]]; do
case "$1" in
--ip) ip="$2"; shift 2 ;;
--subnet) subnet="$2"; shift 2 ;;
--port) port="$2"; shift 2 ;;
--help) cmd::hosts::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
local key entry_type
if [[ -n "$ip" ]]; then key="$ip"; entry_type="host"; fi
if [[ -n "$subnet" ]]; then key="$subnet"; entry_type="subnet"; fi
if [[ -n "$port" ]]; then key="$port"; entry_type="port"; fi
[[ -z "$key" ]] && log::error "Specify --ip, --subnet, or --port" && return 1
hosts::require_exists "$entry_type" "$key" || return 1
log::section "${entry_type^}: ${key}"
printf "\n"
while IFS='|' read -r field val; do
case "$field" in
name) ui::row "Name" "${val:-—}" ;;
desc) ui::row "Description" "${val:-—}" ;;
tags) ui::row "Tags" "${val:-—}" ;;
esac
done < <(json::hosts_show "$(ctx::hosts)" "$key" "$entry_type")
printf "\n"
}
# ============================================
# Add
# ============================================
function cmd::hosts::add() {
local ip="" subnet="" port="" name="" desc="" tags=()
while [[ $# -gt 0 ]]; do
case "$1" in
--ip) ip="$2"; shift 2 ;;
--subnet) subnet="$2"; shift 2 ;;
--port) port="$2"; shift 2 ;;
--name) name="$2"; shift 2 ;;
--desc) desc="$2"; shift 2 ;;
--tag) tags+=("$2"); shift 2 ;;
--tags) IFS=',' read -ra t <<< "$2"; tags+=("${t[@]}"); shift 2 ;;
--help) cmd::hosts::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
local key entry_type
if [[ -n "$ip" ]]; then key="$ip"; entry_type="host"; fi
if [[ -n "$subnet" ]]; then key="$subnet"; entry_type="subnet"; fi
if [[ -n "$port" ]]; then key="$port"; entry_type="port"; fi
[[ -z "$key" ]] && log::error "Specify --ip, --subnet, or --port" && return 1
local tags_str
tags_str=$(IFS=','; echo "${tags[*]}")
json::hosts_add "$(ctx::hosts)" "$entry_type" "$key" "$name" "$desc" "$tags_str"
log::wg_success "Added ${entry_type}: ${key} → ${name}"
}
# ============================================
# Remove
# ============================================
function cmd::hosts::rm() {
local ip="" subnet="" port="" force=false
while [[ $# -gt 0 ]]; do
case "$1" in
--ip) ip="$2"; shift 2 ;;
--subnet) subnet="$2"; shift 2 ;;
--port) port="$2"; shift 2 ;;
--force) force=true; shift ;;
--help) cmd::hosts::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
local key entry_type
if [[ -n "$ip" ]]; then key="$ip"; entry_type="host"; fi
if [[ -n "$subnet" ]]; then key="$subnet"; entry_type="subnet"; fi
if [[ -n "$port" ]]; then key="$port"; entry_type="port"; fi
[[ -z "$key" ]] && log::error "Specify --ip, --subnet, or --port" && return 1
hosts::require_exists "$entry_type" "$key" || return 1
if ! $force; then
read -r -p "Remove ${entry_type} '${key}'? [y/N] " confirm
case "$confirm" in
[yY]*) ;;
*) log::info "Aborted"; return 0 ;;
esac
fi
json::hosts_remove "$(ctx::hosts)" "$entry_type" "$key"
log::wg_success "Removed ${entry_type}: ${key}"
}
function cmd::hosts::_output_json() {
local data
data=$(json::hosts_list "$(ctx::hosts)" 2>/dev/null)
local -a hosts=()
while IFS='|' read -r type ip name desc tags; do
[[ -z "$type" ]] && continue
local tags_json="[]"
if [[ -n "$tags" ]]; then
local tags_array
tags_array=$(echo "$tags" | tr ',' '\n' | \
while IFS= read -r t; do [[ -n "$t" ]] && printf '"%s",' "$t"; done | sed 's/,$//')
tags_json="[${tags_array}]"
fi
hosts+=("$(printf '{"type":"%s","ip":"%s","name":"%s","desc":"%s","tags":%s}' \
"$type" "$ip" "$name" "$desc" "$tags_json")")
done <<< "$data"
local count=${#hosts[@]}
local array
array=$(printf '%s\n' "${hosts[@]:-}" | paste -sd ',' -)
printf '{"hosts":[%s]}' "${array:-}" | json::envelope "hosts list" "$count"
}
| 1 | #!/usr/bin/env bash |
| 2 | # hosts.command.sh — manage host/IP display name mappings |
| 3 | |
| 4 | # ============================================ |
| 5 | # Lifecycle |
| 6 | # ============================================ |
| 7 | |
| 8 | function cmd::hosts::on_load() { |
| 9 | flag::register --ip |
| 10 | flag::register --subnet |
| 11 | flag::register --port |
| 12 | flag::register --name |
| 13 | flag::register --desc |
| 14 | flag::register --tag |
| 15 | flag::register --tags |
| 16 | flag::register --force |
| 17 | |
| 18 | command::mixin json_output |
| 19 | } |
| 20 | |
| 21 | # ============================================ |
| 22 | # Help |
| 23 | # ============================================ |
| 24 | |
| 25 | function cmd::hosts::help() { |
| 26 | cat <<EOF |
| 27 | Usage: wgctl hosts <subcommand> [options] |
| 28 | |
| 29 | Manage host display names for IP resolution in logs, watch, and activity. |
| 30 | Maps IPs, subnets, and ports to human-readable names. |
| 31 | |
| 32 | Subcommands: |
| 33 | list List all host entries |
| 34 | show --ip <ip> Show host entry details |
| 35 | show --subnet <cidr> Show subnet entry details |
| 36 | show --port <port> Show port entry details |
| 37 | add --ip <ip> --name <name> Add a host entry |
| 38 | add --subnet <cidr> --name <name> |
| 39 | Add a subnet entry |
| 40 | add --port <port> --name <name> |
| 41 | Add a port entry |
| 42 | rm --ip <ip> Remove a host entry |
| 43 | rm --subnet <cidr> Remove a subnet entry |
| 44 | rm --port <port> Remove a port entry |
| 45 | |
| 46 | Options for add: |
| 47 | --ip <ip> IP address to map |
| 48 | --subnet <cidr> Subnet CIDR to map (e.g. 10.0.0.0/24) |
| 49 | --port <port> Port number to map (e.g. 443) |
| 50 | --name <name> Display name (e.g. vodafone-wan) |
| 51 | --desc <description> Optional description |
| 52 | --tag <tag> Tag (repeatable) |
| 53 | --tags <tag1,tag2> Tags (comma-separated) |
| 54 | |
| 55 | Options for rm: |
| 56 | --force Skip confirmation |
| 57 | |
| 58 | Examples: |
| 59 | wgctl hosts list |
| 60 | wgctl hosts add --ip 148.69.46.73 --name vodafone-wan --desc "Vodafone WAN" |
| 61 | wgctl hosts add --ip 94.63.0.129 --name nuno-home --tags home,isp |
| 62 | wgctl hosts add --subnet 10.0.0.0/24 --name lan --desc "Local LAN" |
| 63 | wgctl hosts add --port 443 --name https |
| 64 | wgctl hosts show --ip 148.69.46.73 |
| 65 | wgctl hosts rm --ip 148.69.46.73 |
| 66 | EOF |
| 67 | } |
| 68 | |
| 69 | # ============================================ |
| 70 | # Run |
| 71 | # ============================================ |
| 72 | |
| 73 | function cmd::hosts::run() { |
| 74 | local subcmd="${1:-list}" |
| 75 | shift || true |
| 76 | |
| 77 | if command::json; then |
| 78 | cmd::hosts::_output_json |
| 79 | return 0 |
| 80 | fi |
| 81 | |
| 82 | case "$subcmd" in |
| 83 | list) cmd::hosts::list "$@" ;; |
| 84 | show) cmd::hosts::show "$@" ;; |
| 85 | add) cmd::hosts::add "$@" ;; |
| 86 | rm|remove|del) cmd::hosts::rm "$@" ;; |
| 87 | help) cmd::hosts::help ;; |
| 88 | *) |
| 89 | log::error "Unknown subcommand: '${subcmd}'" |
| 90 | cmd::hosts::help |
| 91 | return 1 ;; |
| 92 | esac |
| 93 | } |
| 94 | |
| 95 | # ============================================ |
| 96 | # List |
| 97 | # ============================================ |
| 98 | |
| 99 | function cmd::hosts::list() { |
| 100 | local filter_tag="" |
| 101 | |
| 102 | while [[ $# -gt 0 ]]; do |
| 103 | case "$1" in |
| 104 | --tag) filter_tag="$2"; shift 2 ;; |
| 105 | --help) cmd::hosts::help; return ;; |
| 106 | *) log::error "Unknown flag: $1"; return 1 ;; |
| 107 | esac |
| 108 | done |
| 109 | |
| 110 | local hosts_file |
| 111 | hosts_file="$(ctx::hosts)" |
| 112 | |
| 113 | if [[ ! -f "$hosts_file" ]]; then |
| 114 | log::wg_warning "No hosts configured. Use 'wgctl hosts add' to add one." |
| 115 | return 0 |
| 116 | fi |
| 117 | |
| 118 | local data |
| 119 | data=$(json::hosts_list "$hosts_file" 2>/dev/null) |
| 120 | [[ -z "$data" ]] && log::wg_warning "No hosts configured." && return 0 |
| 121 | |
| 122 | # Apply tag filter to data first |
| 123 | local filtered_data="" |
| 124 | while IFS='|' read -r type key name desc tags; do |
| 125 | [[ -z "$type" ]] && continue |
| 126 | [[ -n "$filter_tag" && "$tags" != *"$filter_tag"* ]] && continue |
| 127 | filtered_data+="${type}|${key}|${name}|${desc}|${tags}"$'\n' |
| 128 | done <<< "$data" |
| 129 | |
| 130 | [[ -z "$filtered_data" ]] && log::wg_warning "No hosts found." && return 0 |
| 131 | |
| 132 | # Measure column widths from filtered data |
| 133 | local w_key=15 w_name=16 w_desc=10 |
| 134 | while IFS='|' read -r type key name desc tags; do |
| 135 | [[ -z "$type" ]] && continue |
| 136 | (( ${#key} > w_key )) && w_key=${#key} |
| 137 | (( ${#name} > w_name )) && w_name=${#name} |
| 138 | local desc_len=${#desc} |
| 139 | [[ -z "$desc" ]] && desc_len=1 # "—" = 1 visible char |
| 140 | (( desc_len > w_desc )) && w_desc=$desc_len |
| 141 | done <<< "$filtered_data" |
| 142 | (( w_key += 2 )) |
| 143 | (( w_name += 2 )) |
| 144 | (( w_desc += 2 )) |
| 145 | |
| 146 | log::section "Host Mappings" |
| 147 | echo "" |
| 148 | |
| 149 | if display::is_table "hosts_list"; then |
| 150 | cmd::hosts::_render_table "$data" |
| 151 | return 0 |
| 152 | fi |
| 153 | |
| 154 | local last_type="" found=false |
| 155 | while IFS='|' read -r type key name desc tags; do |
| 156 | [[ -z "$type" ]] && continue |
| 157 | found=true |
| 158 | |
| 159 | # Section header when type changes |
| 160 | if [[ "$type" != "$last_type" ]]; then |
| 161 | [[ -n "$last_type" ]] && echo "" |
| 162 | ui::hosts::section_header "$type" |
| 163 | last_type="$type" |
| 164 | fi |
| 165 | |
| 166 | ui::hosts::list_row "$type" "$key" "$name" "$desc" "$tags" \ |
| 167 | "$w_key" "$w_name" "$w_desc" |
| 168 | |
| 169 | done <<< "$filtered_data" |
| 170 | |
| 171 | $found || log::wg_warning "No hosts configured." |
| 172 | echo "" |
| 173 | } |
| 174 | |
| 175 | function cmd::hosts::_render_table() { |
| 176 | local data="${1:-}" |
| 177 | [[ -z "$data" ]] && return 0 |
| 178 | |
| 179 | ui::hosts::list_header_table |
| 180 | while IFS='|' read -r type key name desc tags; do |
| 181 | [[ -z "$type" ]] && continue |
| 182 | ui::hosts::list_row_table "$type" "$key" "$name" "$desc" "$tags" |
| 183 | done <<< "$data" |
| 184 | } |
| 185 | |
| 186 | # ============================================ |
| 187 | # Show |
| 188 | # ============================================ |
| 189 | |
| 190 | function cmd::hosts::show() { |
| 191 | local ip="" subnet="" port="" |
| 192 | |
| 193 | while [[ $# -gt 0 ]]; do |
| 194 | case "$1" in |
| 195 | --ip) ip="$2"; shift 2 ;; |
| 196 | --subnet) subnet="$2"; shift 2 ;; |
| 197 | --port) port="$2"; shift 2 ;; |
| 198 | --help) cmd::hosts::help; return ;; |
| 199 | *) log::error "Unknown flag: $1"; return 1 ;; |
| 200 | esac |
| 201 | done |
| 202 | |
| 203 | local key entry_type |
| 204 | if [[ -n "$ip" ]]; then key="$ip"; entry_type="host"; fi |
| 205 | if [[ -n "$subnet" ]]; then key="$subnet"; entry_type="subnet"; fi |
| 206 | if [[ -n "$port" ]]; then key="$port"; entry_type="port"; fi |
| 207 | |
| 208 | [[ -z "$key" ]] && log::error "Specify --ip, --subnet, or --port" && return 1 |
| 209 | |
| 210 | hosts::require_exists "$entry_type" "$key" || return 1 |
| 211 | |
| 212 | log::section "${entry_type^}: ${key}" |
| 213 | printf "\n" |
| 214 | |
| 215 | while IFS='|' read -r field val; do |
| 216 | case "$field" in |
| 217 | name) ui::row "Name" "${val:-—}" ;; |
| 218 | desc) ui::row "Description" "${val:-—}" ;; |
| 219 | tags) ui::row "Tags" "${val:-—}" ;; |
| 220 | esac |
| 221 | done < <(json::hosts_show "$(ctx::hosts)" "$key" "$entry_type") |
| 222 | |
| 223 | printf "\n" |
| 224 | } |
| 225 | |
| 226 | # ============================================ |
| 227 | # Add |
| 228 | # ============================================ |
| 229 | |
| 230 | function cmd::hosts::add() { |
| 231 | local ip="" subnet="" port="" name="" desc="" tags=() |
| 232 | |
| 233 | while [[ $# -gt 0 ]]; do |
| 234 | case "$1" in |
| 235 | --ip) ip="$2"; shift 2 ;; |
| 236 | --subnet) subnet="$2"; shift 2 ;; |
| 237 | --port) port="$2"; shift 2 ;; |
| 238 | --name) name="$2"; shift 2 ;; |
| 239 | --desc) desc="$2"; shift 2 ;; |
| 240 | --tag) tags+=("$2"); shift 2 ;; |
| 241 | --tags) IFS=',' read -ra t <<< "$2"; tags+=("${t[@]}"); shift 2 ;; |
| 242 | --help) cmd::hosts::help; return ;; |
| 243 | *) log::error "Unknown flag: $1"; return 1 ;; |
| 244 | esac |
| 245 | done |
| 246 | |
| 247 | [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1 |
| 248 | |
| 249 | local key entry_type |
| 250 | if [[ -n "$ip" ]]; then key="$ip"; entry_type="host"; fi |
| 251 | if [[ -n "$subnet" ]]; then key="$subnet"; entry_type="subnet"; fi |
| 252 | if [[ -n "$port" ]]; then key="$port"; entry_type="port"; fi |
| 253 | |
| 254 | [[ -z "$key" ]] && log::error "Specify --ip, --subnet, or --port" && return 1 |
| 255 | |
| 256 | local tags_str |
| 257 | tags_str=$(IFS=','; echo "${tags[*]}") |
| 258 | |
| 259 | json::hosts_add "$(ctx::hosts)" "$entry_type" "$key" "$name" "$desc" "$tags_str" |
| 260 | log::wg_success "Added ${entry_type}: ${key} → ${name}" |
| 261 | } |
| 262 | |
| 263 | # ============================================ |
| 264 | # Remove |
| 265 | # ============================================ |
| 266 | |
| 267 | function cmd::hosts::rm() { |
| 268 | local ip="" subnet="" port="" force=false |
| 269 | |
| 270 | while [[ $# -gt 0 ]]; do |
| 271 | case "$1" in |
| 272 | --ip) ip="$2"; shift 2 ;; |
| 273 | --subnet) subnet="$2"; shift 2 ;; |
| 274 | --port) port="$2"; shift 2 ;; |
| 275 | --force) force=true; shift ;; |
| 276 | --help) cmd::hosts::help; return ;; |
| 277 | *) log::error "Unknown flag: $1"; return 1 ;; |
| 278 | esac |
| 279 | done |
| 280 | |
| 281 | local key entry_type |
| 282 | if [[ -n "$ip" ]]; then key="$ip"; entry_type="host"; fi |
| 283 | if [[ -n "$subnet" ]]; then key="$subnet"; entry_type="subnet"; fi |
| 284 | if [[ -n "$port" ]]; then key="$port"; entry_type="port"; fi |
| 285 | |
| 286 | [[ -z "$key" ]] && log::error "Specify --ip, --subnet, or --port" && return 1 |
| 287 | |
| 288 | hosts::require_exists "$entry_type" "$key" || return 1 |
| 289 | |
| 290 | if ! $force; then |
| 291 | read -r -p "Remove ${entry_type} '${key}'? [y/N] " confirm |
| 292 | case "$confirm" in |
| 293 | [yY]*) ;; |
| 294 | *) log::info "Aborted"; return 0 ;; |
| 295 | esac |
| 296 | fi |
| 297 | |
| 298 | json::hosts_remove "$(ctx::hosts)" "$entry_type" "$key" |
| 299 | log::wg_success "Removed ${entry_type}: ${key}" |
| 300 | } |
| 301 | |
| 302 | function cmd::hosts::_output_json() { |
| 303 | local data |
| 304 | data=$(json::hosts_list "$(ctx::hosts)" 2>/dev/null) |
| 305 | |
| 306 | local -a hosts=() |
| 307 | while IFS='|' read -r type ip name desc tags; do |
| 308 | [[ -z "$type" ]] && continue |
| 309 | |
| 310 | local tags_json="[]" |
| 311 | if [[ -n "$tags" ]]; then |
| 312 | local tags_array |
| 313 | tags_array=$(echo "$tags" | tr ',' '\n' | \ |
| 314 | while IFS= read -r t; do [[ -n "$t" ]] && printf '"%s",' "$t"; done | sed 's/,$//') |
| 315 | tags_json="[${tags_array}]" |
| 316 | fi |
| 317 | |
| 318 | hosts+=("$(printf '{"type":"%s","ip":"%s","name":"%s","desc":"%s","tags":%s}' \ |
| 319 | "$type" "$ip" "$name" "$desc" "$tags_json")") |
| 320 | done <<< "$data" |
| 321 | |
| 322 | local count=${#hosts[@]} |
| 323 | local array |
| 324 | array=$(printf '%s\n' "${hosts[@]:-}" | paste -sd ',' -) |
| 325 | printf '{"hosts":[%s]}' "${array:-}" | json::envelope "hosts list" "$count" |
| 326 | } |