IPv6 has a monumental amount of flexibility. That flexibility flow over into the address format, which can be - in some cases - a bit frustrating. Since IPv6 address can be presented in several ways, and this can be frustrating for those writing code to suport IPv6 or engineers trying to create templates. It can also be painful during troubleshooting. In an effort to craft something that is similar to my mac address shell tool , I wanted something similar for IPv6 addresses.
Remembering that my programming skills are not teriffic, and all self-taught (ok, I leanred some Apple Basic in the 80s, some C in college, and used Perl in the “olden days”) This proved the rule that developing for all permutations of a valid IPv6 address can be somewhat daunting.
After a lot of attempts this is what works:
└─[$] v6fmt 3fff:0209:0001:0000:0000:0000:0000:0001 [13:33:44]
Expanded: 3fff:0209:0001:0000:0000:0000:0000:0001
Compressed: 3fff:209:1::1
Uppercase: 3FFF:209:1::1
URL format: [3fff:209:1::1]
Dotted: 3.f.f.f.0.2.0.9.0.0.0.1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1
Binary: 0011111111111111:0000001000001001:0000000000000001:0000000000000000:0000000000000000:0000000000000000:0000000000000000:0000000000000001
Reverse DNS: 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.9.0.2.0.f.f.f.3.ip6.arpa
Type: Global Unicast
Compression permutations:
3fff:209:1:0:0:0:0:1
3fff:209:1::0:0:1
3fff:209:1::0:1
3fff:209:1::1
3fff:209:1:0::0:1
3fff:209:1:0::1
3fff:209:1:0:0::1
Even this seemingly simple bit of text wrangling was painful. there are probably better ways to do this, and likely more simple tools, but I wanted this to be part of my operating shell, so here we are. Add this into your .zshrc and it should work. I am unsure if it works with bash, but I suspect because of the parameter expansion flags, globbing, and array work that is unique to zsh, it likely will not.
This is also available in as a web service on ipv6.army .
v6fmt() {
local ip="$1"
[[ -z "$ip" ]] && { print -r -- "Usage: v6fmt <ipv6-address>[/prefix-length]" >&2; return 1 }
# Normalize: lowercase, strip whitespace
ip="${ip:l}"
ip="${ip//[[:space:]]}"
# Strip and save optional prefix length
local prefix_len=""
if [[ "$ip" == */* ]]; then
prefix_len="${ip#*/}"
ip="${ip%/*}"
fi
# Strip URL brackets and zone ID
ip="${ip#\[}"
ip="${ip%\]}"
ip="${ip%%\%*}"
# Handle embedded IPv4 (e.g., ::ffff:192.0.2.1)
if [[ "$ip" == *:*.* ]]; then
local v4="${ip##*:}"
local v6pfx="${ip%:*}"
if [[ "$v4" == *.*.*.* ]]; then
local -a octs=(${(s:.:)v4})
local h1 h2
printf -v h1 '%02x%02x' "${octs[1]}" "${octs[2]}"
printf -v h2 '%02x%02x' "${octs[3]}" "${octs[4]}"
ip="${v6pfx}:${h1}:${h2}"
fi
fi
# Basic validation
if [[ ! "$ip" =~ ^[0-9a-f:]+$ ]]; then
print -r -- "Error: Invalid IPv6 address" >&2
return 1
fi
# Expand :: notation
local prefix suffix zeros_needed full_ip i g
if [[ "$ip" == *::* ]]; then
prefix="${ip%%::*}"
suffix="${ip#*::}"
local pre_count=0 suf_count=0
for g in ${(s.:.)prefix}; [[ -n "$g" ]] && ((pre_count++))
for g in ${(s.:.)suffix}; [[ -n "$g" ]] && ((suf_count++))
zeros_needed=$((8 - pre_count - suf_count))
full_ip="$prefix"
for ((i = 0; i < zeros_needed; i++)); do
full_ip="${full_ip}:0000"
done
[[ -n "$suffix" ]] && full_ip="${full_ip}:${suffix}"
full_ip="${full_ip#:}"
else
full_ip="$ip"
fi
# Split into 8 groups
local groups=(${(s.:.)full_ip})
while (( $#groups < 8 )); do groups+=('0000'); done
(( $#groups > 8 )) && groups=("${(@)groups[1,8]}")
# Pad each group to 4 hex digits
local full_groups=() padded
for g in $groups; do
g="${g#0}"
[[ -z "$g" ]] && g=0
printf -v padded '%04x' "0x${g}"
full_groups+=("$padded")
done
# Build expanded string
local expanded="${full_groups[1]}"
for ((i = 2; i <= 8; i++)); do
expanded="${expanded}:${full_groups[i]}"
done
# Strip leading zeros per group
local short_groups=()
for g in $full_groups; do
while [[ "$g" == 0?* && "$g" != "0" ]]; do g="${g#0}"; done
short_groups+=("$g")
done
# Build no-compression form
local nocompress="${short_groups[1]}"
for ((i = 2; i <= 8; i++)); do
nocompress="${nocompress}:${short_groups[i]}"
done
# Find longest run of consecutive zero groups (RFC 5952)
local best_start=-1 best_len=0 cur_start=-1 cur_len=0
for ((i = 1; i <= 8; i++)); do
if [[ "${short_groups[i]}" == "0" ]]; then
[[ "$cur_start" == -1 ]] && cur_start=$((i - 1))
((cur_len++))
else
if (( cur_len > best_len )); then
best_len=$cur_len
best_start=$cur_start
fi
cur_start=-1
cur_len=0
fi
done
if (( cur_len > best_len )); then
best_len=$cur_len
best_start=$cur_start
fi
# Build canonical (compressed) form per RFC 5952
local canonical
if (( best_len > 1 )); then
canonical=""
for ((i = 1; i <= 8; i++)); do
if (( i == best_start + 1 )); then
canonical="${canonical}::"
((i = best_start + best_len))
continue
fi
[[ -n "$canonical" && "${canonical[-1]}" != ':' ]] && canonical="${canonical}:"
canonical="${canonical}${short_groups[i]}"
done
else
canonical="$nocompress"
fi
# === Derived formats ===
# Suffix for prefix length display
local sfx=""
[[ -n "$prefix_len" ]] && sfx="/${prefix_len}"
# Hex string (32 nibbles, no separators)
local hex_str="${expanded//:/}"
# Dotted notation (each nibble separated by dots)
local dotted=""
for ((i = 1; i <= ${#hex_str}; i++)); do
(( i > 1 )) && dotted="${dotted}."
dotted="${dotted}${hex_str[i]}"
done
# Binary representation (16-bit groups separated by :)
local -A h2b=(
0 0000 1 0001 2 0010 3 0011
4 0100 5 0101 6 0110 7 0111
8 1000 9 1001 a 1010 b 1011
c 1100 d 1101 e 1110 f 1111
)
local binary=""
for ((i = 1; i <= ${#hex_str}; i++)); do
binary="${binary}${h2b[${hex_str[i]}]}"
(( i % 4 == 0 && i < ${#hex_str} )) && binary="${binary}:"
done
# Reverse DNS (ip6.arpa)
local rdns=""
for ((i = ${#hex_str}; i >= 1; i--)); do
(( i < ${#hex_str} )) && rdns="${rdns}."
rdns="${rdns}${hex_str[i]}"
done
rdns="${rdns}.ip6.arpa"
# Address type classification
local addr_type
if [[ "$expanded" == "0000:0000:0000:0000:0000:0000:0000:0001" ]]; then
addr_type="Loopback"
elif [[ "$expanded" == "0000:0000:0000:0000:0000:0000:0000:0000" ]]; then
addr_type="Unspecified"
elif [[ "$expanded" == 0000:0000:0000:0000:0000:ffff:* ]]; then
addr_type="IPv4-Mapped"
elif [[ "$expanded" == 0064:ff9b:0000:0000:0000:0000:* ]]; then
addr_type="NAT64 Well-Known Prefix (RFC 6052)"
elif [[ "$expanded" == 0064:ff9b:0001:* ]]; then
addr_type="NAT64 Local-Use Prefix (RFC 8215)"
elif [[ "$expanded" == 0100:0000:0000:0000:* ]]; then
addr_type="Discard-Only (RFC 6666)"
elif [[ "$expanded" == 2001:0db8:* ]]; then
addr_type="Documentation (RFC 3849)"
elif [[ "$expanded" == 2001:0000:* ]]; then
addr_type="Teredo (RFC 4380)"
elif [[ "$expanded" == 2002:* ]]; then
addr_type="6to4 (RFC 3056)"
elif [[ "${expanded[1,2]}" == "fe" ]]; then
case "${expanded[3]}" in
[89ab]) addr_type="Link-Local" ;;
[cdef]) addr_type="Site-Local (Deprecated)" ;;
*) addr_type="Unknown" ;;
esac
elif [[ "${expanded[1,2]}" == "fc" || "${expanded[1,2]}" == "fd" ]]; then
addr_type="Unique Local Address (ULA)"
elif [[ "${expanded[1,2]}" == "ff" ]]; then
case "${expanded[4]}" in
1) addr_type="Multicast (Interface-Local)" ;;
2) addr_type="Multicast (Link-Local)" ;;
4) addr_type="Multicast (Admin-Local)" ;;
5) addr_type="Multicast (Site-Local)" ;;
8) addr_type="Multicast (Organization-Local)" ;;
e) addr_type="Multicast (Global)" ;;
*) addr_type="Multicast" ;;
esac
elif [[ "${expanded[1]}" == [23] ]]; then
addr_type="Global Unicast"
else
addr_type="Reserved"
fi
# === Output ===
printf '%-14s %s\n' "Expanded:" "${expanded}${sfx}"
printf '%-14s %s\n' "Compressed:" "${canonical}${sfx}"
printf '%-14s %s\n' "Uppercase:" "${canonical:u}${sfx}"
printf '%-14s %s\n' "URL format:" "[${canonical}]"
printf '%-14s %s\n' "Dotted:" "$dotted"
printf '%-14s %s\n' "Binary:" "$binary"
printf '%-14s %s\n' "Reverse DNS:" "$rdns"
printf '%-14s %s\n' "Type:" "$addr_type"
# Compression permutations
print ""
print "Compression permutations:"
print " $nocompress"
local start end sub_start sub_len out
for ((start = 0; start < 8; )); do
[[ "${short_groups[start+1]}" != "0" ]] && { ((start++)); continue }
end=$start
while (( end < 8 )) && [[ "${short_groups[end+1]}" == "0" ]]; do
((end++))
done
for ((sub_start = start; sub_start <= end - 2; sub_start++)); do
for ((sub_len = 2; sub_len <= end - sub_start; sub_len++)); do
out=""
for ((i = 1; i <= sub_start; i++)); do
[[ -n "$out" ]] && out="${out}:"
out="${out}${short_groups[i]}"
done
out="${out}::"
for ((i = sub_start + sub_len + 1; i <= 8; i++)); do
[[ -n "$out" && "${out[-1]}" != ':' ]] && out="${out}:"
out="${out}${short_groups[i]}"
done
print " $out"
done
done
((start = end))
done
}