Pure Shell implementation of IPv6 address formatting (RFC standard compression format), without relying on iproute2, supports validating address validity, compressing zero segments, handling addresses with prefixes, and outputs all lowercase letters.
Directory Navigation
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