When configuring a gbase database cluster, you often need to convert IPv6 addresses into the standard RFC compressed format — turning 2001:db8:0:0:0:0:0:1 into 2001:db8::1, for example. This article provides a pure Bash script that performs IPv6 address compression without any external tools like iproute2. It validates the address, finds the longest run of zero segments, and outputs a fully lower‑cased compressed form, optionally preserving a prefix.
Features
- Compresses any valid IPv6 address to the canonical RFC format by replacing the longest consecutive zero blocks with
:: - Handles addresses with prefixes (e.g.,
/64); can keep or drop the prefix - Built‑in checks for illegal characters, segment length, and
::occurrence count - Pure Shell implementation requiring only standard tools (
bc,printf)
The Script
#!/bin/bash
set -euo pipefail
SCRIPT_NAME=$(basename "$0")
show_help() {
cat << EOF
Usage: $SCRIPT_NAME [options] <IPv6 address>
Compresses an IPv6 address to the standard RFC format.
Supports address validation, zero‑block compression, and prefix handling.
Options:
-h, --help Show this help message and exit
-p, --prefix Preserve the prefix (e.g., 2001:db8::1/64); by default only the address part is compressed
Examples:
$SCRIPT_NAME 2001:2:3:4:5:6:7:8
$SCRIPT_NAME -p 2001:db8:0:0:0:0:0:1/64
$SCRIPT_NAME ::1
EOF
}
validate_ipv6_chars() {
local addr="$1"
if [[ ! "$addr" =~ ^[0-9a-fA-F:/]+$ ]]; then
echo "Error: IPv6 address contains illegal characters (only 0-9/a-f/A-F/:/ allowed)" >&2
exit 1
fi
}
expand_ipv6() {
local pure_addr="$1"
local expanded=""
local colon_count=$(grep -o "::" <<< "$pure_addr" | wc -l)
if [[ $colon_count -gt 1 ]]; then
echo "Error: Invalid IPv6 address (:: can appear only once)" >&2
exit 1
fi
if [[ "$pure_addr" == "::" ]]; then
expanded="0:0:0:0:0:0:0:0"
echo "$expanded"
return
fi
local temp_addr="${pure_addr//::/:__ZERO__:}"
local segments=()
IFS=: read -ra segments <<< "$temp_addr"
local filtered_segments=()
for seg in "${segments[@]}"; do
if [[ -n "$seg" ]]; then
filtered_segments+=("$seg")
fi
done
segments=("${filtered_segments[@]}")
local zero_pos=-1
local seg_count=${#segments[@]}
for i in "${!segments[@]}"; do
if [[ "${segments[$i]}" == "__ZERO__" ]]; then
zero_pos=$i
break
fi
done
local new_segments=()
if [[ $zero_pos -ne -1 ]]; then
local fill_zeros=$((8 - (seg_count - 1)))
if [[ $fill_zeros -lt 0 ]]; then
echo "Error: IPv6 address has too many segments (more than 8)" >&2
exit 1
fi
for ((i=0; i<zero_pos; i++)); do
new_segments+=("${segments[$i]}")
done
for ((i=0; i<fill_zeros; i++)); do
new_segments+=("0")
done
for ((i=zero_pos+1; i<seg_count; i++)); do
new_segments+=("${segments[$i]}")
done
else
new_segments=("${segments[@]}")
if [[ ${#new_segments[@]} -ne 8 ]]; then
echo "Error: IPv6 address segment count wrong (must be exactly 8 when no ::)" >&2
exit 1
fi
fi
for seg in "${new_segments[@]}"; do
if [[ ${#seg} -gt 4 ]]; then
echo "Error: IPv6 segment '$seg' too long (max 4 digits)" >&2
exit 1
fi
if ! [[ "$seg" =~ ^[0-9a-fA-F]{1,4}$ ]]; then
echo "Error: IPv6 segment '$seg' contains illegal characters" >&2
exit 1
fi
done
expanded=$(IFS=:; echo "${new_segments[*]}")
echo "$expanded"
}
compress_ipv6() {
local expanded_addr="$1"
local segments=()
IFS=: read -ra segments <<< "$expanded_addr"
local compressed_segments=()
for seg in "${segments[@]}"; do
if [[ -z "$seg" || "$seg" == "0000" || "$seg" == "000" || "$seg" == "00" ]]; then
compressed_segments+=("0")
else
local seg_upper=$(echo "$seg" | tr 'a-f' 'A-F')
local trimmed=$(echo "obase=16; ibase=16; $seg_upper" | bc | tr 'A-F' 'a-f')
compressed_segments+=("$trimmed")
fi
done
local max_zero_len=0
local max_zero_start=-1
local current_zero_len=0
local current_zero_start=-1
for i in "${!compressed_segments[@]}"; do
if [[ "${compressed_segments[$i]}" == "0" ]]; then
if [[ $current_zero_start -eq -1 ]]; then
current_zero_start=$i
fi
((current_zero_len++))
if [[ $current_zero_len -gt $max_zero_len ]]; then
max_zero_len=$current_zero_len
max_zero_start=$current_zero_start
fi
else
current_zero_len=0
current_zero_start=-1
fi
done
local final_segments=()
local skip=0
for i in "${!compressed_segments[@]}"; do
if [[ $skip -gt 0 ]]; then
((skip--))
continue
fi
if [[ $i -eq $max_zero_start && $max_zero_len -ge 2 ]]; then
final_segments+=("")
skip=$((max_zero_len - 1))
if [[ $((i + max_zero_len)) -eq 8 ]]; then
final_segments+=("")
fi
else
final_segments+=("${compressed_segments[$i]}")
fi
done
local compressed=$(IFS=:; echo "${final_segments[*]}")
compressed="${compressed//:::/::}"
if [[ "$compressed" == ":"* ]]; then
compressed=":$compressed"
fi
if [[ "$compressed" == "::0" ]]; then
compressed="::"
fi
compressed=$(echo "$compressed" | tr 'A-F' 'a-f')
echo "$compressed"
}
format_ipv6() {
local addr="$1"
local keep_prefix="$2"
local pure_addr
local prefix=""
if [[ "$addr" =~ / ]]; then
pure_addr="${addr%/*}"
prefix="${addr#*/}"
if ! [[ "$prefix" =~ ^[0-9]+$ ]] || [[ "$prefix" -lt 0 ]] || [[ "$prefix" -gt 128 ]]; then
echo "Error: Invalid IPv6 prefix '$prefix' (must be 0‑128)" >&2
exit 1
fi
else
pure_addr="$addr"
fi
validate_ipv6_chars "$pure_addr"
local expanded_addr=$(expand_ipv6 "$pure_addr")
local formatted_addr=$(compress_ipv6 "$expanded_addr")
if [[ "$keep_prefix" -eq 1 && -n "$prefix" ]]; then
echo "${formatted_addr}/${prefix}"
else
echo "$formatted_addr"
fi
}
KEEP_PREFIX=0
IPV6_ADDR=""
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
show_help
exit 0
;;
-p|--prefix)
KEEP_PREFIX=1
shift
;;
*)
if [[ -z "$IPV6_ADDR" ]]; then
IPV6_ADDR="$1"
shift
else
echo "Error: Extra argument '$1'" >&2
show_help >&2
exit 1
fi
;;
esac
done
if [[ -z "$IPV6_ADDR" ]]; then
echo "Error: IPv6 address is required" >&2
show_help >&2
exit 1
fi
format_ipv6 "$IPV6_ADDR" "$KEEP_PREFIX"
Running the Script
$ ./ipv6_format.sh 2001:1:0:0:5:0:0:8
2001:1::5:0:0:8
$ ./ipv6_format.sh -p 2001:1:0:0:5:0:0:8/64
2001:1::5:0:0:8/64
In a gbase database cluster, this script helps you standardize IPv6 addresses into their shortest canonical form — making configuration files cleaner and simplifying automated deployment and address comparison tasks.
Top comments (0)