IPv6アドレスをシェルスクリプトで扱いやすくするための小物

# 前置き

IPv6アドレスを扱う場合、その表記は極力短くなるよう省略が行われます。例えば`2001:08db:0000:0000:0000:0000:0012:3456`というアドレスは`2001:8db::12:3456`になるといった具合です。先頭の0を省略する、程度だったら切り出すのも簡単なのですが、コロン区切りのフィールドの数が省略によって可変になるというのが厄介です。
起動時に実行されるinitスクリプトの中でNICの初期化でIPv6アドレスを扱うとなった場合、その表記の柔軟性によりそのままだとシェルスクリプトでプレフィックスを切り出して加工するなどといった操作が難しいので、省略した部分を復元するようなシェルスクリプトを書きました。

## すること

- IPv6アドレスは128bitあるものを16bit毎に区切ってコロンで区切ることにより8個のフィールドで表記される。省略することによってフィールドが8個未満になっていた場合それを元に戻す。
- それぞれのフィールドの16進数の値は0埋めする。

## しないこと

- 一度冗長表記にしたアドレスを再び省略するということはしない。前段階で加工したIPv6アドレスは他のソフトウェアとやり取りするためだけに使うのであり、スクリプト自身で出力に使うことはなく、また渡された側は適切に表記を省略するはずだからである。
- IPv4射影アドレスは扱わない。

# コード

```bash
#!/bin/sh
# License: CC0

# 仕様
# 第1引数にIPv6アドレスを渡す。
# IPv6アドレスとして正しい形式なら、省略部分を復元して標準出力へ出力
# 不正なら何も出力せず非0で終了する

# 形式チェックで撥ねられたら即座に終了する
set -e

export LANG=C
ip="$1"

# 文字種のチェック。exprだと改行も普通の文字として扱われるので
# grepを使う時に必要な複数行かどうかの事前チェックを省略できて便利。
expr "$ip" : '[[:xdigit:]:]\{2,\}$' >/dev/null

# grepで文字シーケンスのチェック。-vオプションでマッチ条件を反転させ、
# マッチした時に失敗するようにする。
echo $ip |grep -sqEv '[[:xdigit:]]{5}|:::|::.*::' 

# コロンの数を数える
cn=$(echo $ip |grep -o : |wc -l)
test $cn -ge 2 -a $cn -le 7

ip=$(echo $ip |sed '
# ここからsedスクリプト
# 前後がコロンで終わってたら0を足す
	s/^:/0000:/
	s/:$/:0000/
# 一旦デリミタとしてコロンを前後に足す
	s/.*/:&:/
# 桁が4に満たなかったら先頭に0を付ける、というのを繰り返す(tコマンド)
	:add0
	s/:\([^:]\{1,3\}\):/:0\1:/g
	t add0
#前後のデリミタを再び除く
	s/:\(.*\):/\1/' )

if echo $ip |grep -sq :: 
then
# アドレスが::で省略されている場合…
# 内側のコマンド展開で省略されたフィールドの分を出力
# 外側のコマンド展開のsedで省略が行われたところにはめ込む感じ
## ところで、headではなくtailを使っているのは、POSIXにおいてheadの-cオプションがわざわざ落とされているため。
## tailに似た機能があるからというのが言い分らしいが…。
	ip=$(echo $ip |sed s/::/:$(echo -n :::::: |tail -c $((8-cn)) |sed 's/:/0000:/g')/ )
else
# アドレスの省略がない場合はコロンは7個のはずである
        test $cn -eq 7
fi
# 出力!
echo $ip
```

## 出力例

今回はスクリプト名を`ipv6expand`としておきます。

```bash
ipv6expand 2001:db8::1
# 2001:0db8:0000:0000:0000:0000:0000:0001

ipv6expand 2001:db8:0:0::1
# 2001:0db8:0000:0000:0000:0000:0000:0001

ipv6expand ::
# 0000:0000:0000:0000:0000:0000:0000:0000

ipv6expand ::1
# 0000:0000:0000:0000:0000:0000:0000:0001

ipv6expand 1:2:3:4:5:6:7:8
# 0001:0002:0003:0004:0005:0006:0007:0008

ipv6expand 1:2:3:4:5:6:7
# xfail 省略無しでフィールドが8に満たない

ipv6expand 1:2::3::4
# xfail 省略が2回

ipv6expand 1:2:::3
# xfail 省略のミス

ipv6expand 01234:05678:09abc:0def0::
# xfail フィールドが5文字以上なら範囲内でも落とす
```

# 使い方

プレフィックスの切り出しはcut(1)で簡単に。

```bash
# /64 を取り出す
pfx=$(ipv6expand 2001:db8:123:456::beef |cut -d: -f1-4)
#pfx=2001:0db8:0123:0456

# /48 を取り出す
pfx=$(ipv6expand 2001:db8:123:456::beef |cut -d: -f1-3)
# pfx=2001:0db8:0123

# /56 を取り出す
## 単純な文字列として切り出しを行う方法
pfx=$(ipv6expand 2001:db8:123:456::beef |cut -d: -f1-4)
pfx=${pfx%??}00
# pfx=2001:0db8:0123:0400

## 数値展開で切り出しを行う方法
## 数値計算でアドレスを加工するときは入力と出力を16進数として扱わせるようにする必要がある事に注意
pfx=$(ipv6expand 2001:db8:123:456::beef |cut -d: -f1-4)
pfx=${pfx%:*}:$(printf %x $((0x${pfx##*:} & 0xff00)) )
# pfx=2001:0db8:0123:400

# インターフェイス部分を取り出す
ifid=$(ipv6expand 2001:db8:123:456::beef |cut -d: -f5-8)
# ifid=0000:0000:0000:beef
```


## 込み入った使い方

シチュエーション: ホストにIPv6アドレスを割り当てたい3つのI/F、eth1・eth2・wlan0がある。プレフィックスの取得のためDHCPv6クライアントでPDを要求すると、/56のプレフィックスをもらえるので、各I/Fにサブネット部分を0x10から初めて0x10飛びにし、インターフェイス部分を::1にした/64のアドレスを割り当てたい。同時に、ULAも割り当てたい。

```bash
#!/bin/sh
# このスクリプトはDHCPv6クライアントからinvokeされて、
# プレフィックスは環境変数 PREFIX に設定されるとします。
# ULAの /48 の部分は事前に fd00:ab:cdef:: に決めてあるとします。
# システムはLinuxを想定してI/F操作にiproute2を使用。

ifacelist="eth1 eth2 wlan0"

ulapfx=fd00:ab:cdef:
guapfx=$(ipv6expand $PREFIX |cut -d: -f1-4)
guapfx=${guapfx%??}

idx=1
for ifname in $ifacelist
do
    net=$(printf %02x $((idx * 0x10)) )
    ip addr add $guapfx$net::1/64 dev $ifname
    ip addr add $ulapfx$net::1/64 dev $ifname
    idx=$(($idx+1))
done

# PREFIXが"2001:db8:1234:5600::"でinvokeされた場合のユニキャストアドレス
# eth1:
#   2001:db8:1234:5610::1, fd00:ab:cdef:10::1
# eth2:
#   2001:db8:1234:5620::1, fd00:ab:cdef:20::1
# wlan0:
#   2001:db8:1234:5630::1, fd00:ab:cdef:30::1
```