IPV6地址压缩纯Shell脚本

纯Shell实现IPv6地址格式化(RFC标准压缩格式),无需依赖iproute2,支持验证地址有效性、压缩零段、处理带前缀地址,输出全小写字母。

SHELL脚本

#!/bin/bash
set -euo pipefail

# 脚本名称
SCRIPT_NAME=$(basename "$0")

# 帮助信息函数
show_help() {
    cat << EOF
用法: $SCRIPT_NAME [选项] <IPv6地址>

功能: 纯Shell实现IPv6地址格式化(RFC标准压缩格式),无需依赖iproute2
      支持验证地址有效性、压缩零段、处理带前缀地址,输出全小写字母

选项:
    -h, --help      显示此帮助信息并退出
    -p, --prefix    保留IPv6地址的前缀(如2001:db8::1/64),默认仅格式化地址部分

示例:
    $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
}

# 验证IPv6地址字符合法性(基础校验)
validate_ipv6_chars() {
    local addr="$1"
    # IPv6合法字符:0-9, a-f, A-F, :, /(前缀)
    if [[ ! "$addr" =~ ^[0-9a-fA-F:/]+$ ]]; then
        echo "错误: IPv6地址包含非法字符(仅允许0-9/a-f/A-F/:/)" >&2
        exit 1
    fi
}

# 展开IPv6的零压缩(将::替换为对应数量的0段)
expand_ipv6() {
    local pure_addr="$1"
    local expanded=""

    # 检查::出现的次数(合法IPv6只能有一个::)
    local colon_count=$(grep -o "::" <<< "$pure_addr" | wc -l)
    if [[ $colon_count -gt 1 ]]; then
        echo "错误: 无效的IPv6地址(::只能出现一次)" >&2
        exit 1
    fi

    # 处理全零压缩(::)
    if [[ "$pure_addr" == "::" ]]; then
        expanded="0:0:0:0:0:0:0:0"
        echo "$expanded"
        return
    fi

    # 替换::为临时标记,计算需要补充的0段数量
    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[@]}")

    # 计算需要补充的0段数(总段数需为8)
    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
        # 计算需要填充的0段数
        local fill_zeros=$((8 - (seg_count - 1)))
        if [[ $fill_zeros -lt 0 ]]; then
            echo "错误: IPv6地址段数过多(超过8段)" >&2
            exit 1
        fi
        # 拼接前半段 + 填充0 + 拼接后半段
        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
        # 无::的情况,直接使用原段(需正好8段)
        new_segments=("${segments[@]}")
        if [[ ${#new_segments[@]} -ne 8 ]]; then
            echo "错误: IPv6地址段数错误(无::时需正好8段)" >&2
            exit 1
        fi
    fi

    # 验证每段长度(IPv6每段最多4位16进制数)
    for seg in "${new_segments[@]}"; do
        if [[ ${#seg} -gt 4 ]]; then
            echo "错误: IPv6地址段 '$seg' 过长(最多4位)" >&2
            exit 1
        fi
        # 验证是合法16进制数
        if ! [[ "$seg" =~ ^[0-9a-fA-F]{1,4}$ ]]; then
            echo "错误: IPv6地址段 '$seg' 包含非法字符" >&2
            exit 1
        fi
    done

    # 拼接为展开后的完整地址
    expanded=$(IFS=:; echo "${new_segments[*]}")
    echo "$expanded"
}

# 压缩IPv6地址(去除前导零,压缩最长连续零段为::)
compress_ipv6() {
    local expanded_addr="$1"
    local segments=()
    # 正确拆分展开后的地址段
    IFS=: read -ra segments <<< "$expanded_addr"
    local compressed_segments=()

    # 第一步:去除每段的前导零(保留单零),并转为小写
    for seg in "${segments[@]}"; do
        # 空段或全零段保留为0,否则去除前导零
        if [[ -z "$seg" || "$seg" == "0000" || "$seg" == "000" || "$seg" == "00" ]]; then
            compressed_segments+=("0")
        else
            # 先将seg转为大写(确保16进制解析正确),再用bc转换后强制转小写
            local seg_upper=$(echo "$seg" | tr 'a-f' 'A-F')
            # 用bc工具处理16进制,输出小写
            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#*/}"
        # 验证前缀合法性(0-128的整数)
        if ! [[ "$prefix" =~ ^[0-9]+$ ]] || [[ "$prefix" -lt 0 ]] || [[ "$prefix" -gt 128 ]]; then
            echo "错误: 无效的IPv6前缀 '$prefix'(需为0-128的整数)" >&2
            exit 1
        fi
    else
        pure_addr="$addr"
    fi

    # 第二步:基础字符验证
    validate_ipv6_chars "$pure_addr"

    # 第三步:展开零压缩为8段完整地址
    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 "错误: 多余的参数 '$1'" >&2
                show_help >&2
                exit 1
            fi
            ;;
    esac
done

# 检查是否输入了IPv6地址
if [[ -z "$IPV6_ADDR" ]]; then
    echo "错误: 必须指定IPv6地址" >&2
    show_help >&2
    exit 1
fi

# 执行格式化并输出结果
format_ipv6 "$IPV6_ADDR" "$KEEP_PREFIX"

使用样例

[gbase@vm151 ~]$ ./ipv6_format.sh 2001:1:0:0:5:0:0:8
2001:1::5:0:0:8
[gbase@vm151 ~]$ ./ipv6_format.sh -p 2001:1:0:0:5:0:0:8/64
2001:1::5:0:0:8/64