DEV Community

Michael
Michael

Posted on • Originally published at gbase8.cn

Pure Shell Script to Expand IPv6 Addresses

When deploying a gbase database cluster, you often need to handle IPv6 addresses in configuration files. Many tools require the full 8‑segment, 4‑digit hexadecimal format, while real‑world addresses often come in compressed notation (e.g., 2001:db8::1). This article presents a pure Bash script that expands any compressed IPv6 address to the complete, canonical form, with built‑in validation and prefix handling.

Features

  • Expands any compressed IPv6 address (including :: zero‑compression) into 8 segments of 4 lowercase hex digits
  • Handles addresses with prefixes (e.g., 2001:db8::1/64), with an option to preserve or strip the prefix
  • Built‑in validation: illegal characters, :: occurrence count, segment length, and hex validity
  • Pure Shell implementation; relies only on 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>

Expands a compressed IPv6 address to the full 8‑segment, 4‑digit format.
Supports address validation 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 expanded

Examples:
    $SCRIPT_NAME 2001:db8::1
    $SCRIPT_NAME -p ::1/128
    $SCRIPT_NAME fe80::1234:5678:9abc:def0
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_core() {
    local pure_addr="$1"
    local expanded_segments=()

    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
        echo "0000:0000:0000:0000:0000:0000:0000:0000"
        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+=("")
        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 [[ -z "$seg" || "$seg" == "0" ]]; then
            expanded_segments+=("0000")
            continue
        fi

        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

        local seg_upper=$(echo "$seg" | tr 'a-f' 'A-F')
        local seg_10=$(echo "ibase=16; $seg_upper" | bc)
        local seg_4digit=$(printf "%04x" "$seg_10" | tr 'A-F' 'a-f')
        expanded_segments+=("$seg_4digit")
    done

    local expanded_addr=$(IFS=:; echo "${expanded_segments[*]}")
    echo "$expanded_addr"
}

expand_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_core "$pure_addr")

    if [[ "$keep_prefix" -eq 1 && -n "$prefix" ]]; then
        echo "${expanded_addr}/${prefix}"
    else
        echo "$expanded_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

expand_ipv6 "$IPV6_ADDR" "$KEEP_PREFIX"
Enter fullscreen mode Exit fullscreen mode

Running the Script

$ ./ipv6_expand.sh -p 2001:1::5:0:0:8/64
2001:0001:0000:0000:0005:0000:0000:0008/64
$ ./ipv6_expand.sh 2001:1::5:0:0:8
2001:0001:0000:0000:0005:0000:0000:0008
Enter fullscreen mode Exit fullscreen mode

When configuring inter‑node communication in a gbase database cluster that involves IPv6, this script helps you normalize addresses — ensuring they are consistently written in configuration files and simplifying address comparisons during automated deployments.

Top comments (0)