DEV Community

Pure Shell Script to Compress IPv6 Addresses

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.

Comments

No comments yet. Start the discussion.