IPアドレスを取得するone-liners

## 概要
WSL2 で ip adder コマンドから IPアドレスを取得したくなることがる。通常は awk など使うが、他の言語だとどうやるのか気になったので試してみた。

それぞれの言語の特徴を垣間見られる点で興味深い。

### 環境

* Ubuntu 18.04 LTS

## 表示内容

ポイント
* IPv4 アドレスは `inet` の行のみにある
* `inet` と `inet6` の行があるので行を抽出する場合は区別が必要

```console
$ ip address show dev eth0
...
    inet xxx.xxx.xxx.xxx/24 brd xxx.xxx.xxx.255 scope global dynamic
       valid_lft forever preferred_lft forever
    inet6 xxxx:xxxx:xxx:xxxx:xxxx:xxxx:xxxx:xxxx/64 scope global dynamic
       valid_lft 86398sec preferred_lft 14398sec
...
```

## 結局どれが一番いいの？

この記事の趣旨ではないけれど、どれが一番いいの？ということなら、awk かなと思います。結果を見ながらコマンドを組み立てられるし、正規表現の知識もいらないためです。

```shell
ip address show dev eth0 | awk '/inet / {print $2}' | awk -F/ '{print $1}'
```

awk のパターンマッチングなどの使い方を勉強したくない、このコマンドが理解しやすいでしょうか。

```shell
ip address show dev eth0 | grep -w inet | awk '{print $2}' | cut -d/ -f 1
```

## One-linersについて

* メリット
  * ファイルを配置しておく必要がない 
* デメリット
  * (コードによるが)可読性が低い 

## 全体的な指針

大まかに次のアプローチになる

* 行と列を意識する
  ip address show dev eth0 → 行を抽出 → 列を抽出 → サブネットを削る
* 行を抽出した後、正規表現でIPアドレスを抜く
  ip address show dev eth0 → 行を抽出 → 正規表現でIPアドレスを抽出
* プログラミング言語のソケット関係のクラスを通じて取得する

## grep版

```shell
ip address show dev eth0 | grep -Eo '([0-9]+(\.[0-9]+){3})' | head -1
```


## awk版

全部 awk でやるには (将来的にメンテナンスするであろう第三者を含めて) awk のことを色々と知らなくてはならないので、妥当でないことはある。

### awkのみ

以下、正規表現を使わない例。

```shell
ip address show dev eth0 | awk '/inet / {print $2}' | awk -F/ '{print $1}'
```

以下、正規表現を使った例。

```shell
ip address show dev eth0 | awk '/inet / {gsub(/\/[0-9]*/, "", $2); print $2}'
```

### awk以外のコマンドを組み合わせる

* `grep -w inet` は単語としての照合を行うので `inet6` の行は除外される。
* netmask を削る場合、 cut や sed が使える

```shell
ip address show dev eth0 | grep -w inet | awk '{print $2}' | cut -d/ -f 1
ip address show dev eth0 | grep -w inet | awk '{print $2}' | sed -e 's@/.*@@'
```

## sed版

正規表現をしっていれば sed も使えますね。
awk知らんし覚えたくもない…という人には良いかもしれません。

```shell
ip address show dev eth0 | sed -ne '/inet /p' | sed -e 's@.*inet \([0-9.]*\)/.*@\1@'
```

## grep版

IPv4アドレスのパターンマッチする箇所は2か所あるので、最初に見つかったものだけ取り出す。

`grep -w inet` は今回は無くても良いが `grep -o` が行と列を区別することなく抜いてくるので `grep -w inet` と行を絞り込んでおくほうが無難。

```shell
ip address show dev eth0 | grep -w inet | grep -Eo '[0-9]+(\.[0-9]+){3}' | head -1
```

## Ruby版

puts の出力以外はメソッドチェーンが効くので見通しは良いほう。ただ、OS自体の必須パッケージになっていないことも多いので、使えるかどうかは環境次第。

### 標準入力から読み取る場合

```shell
ip address show dev eth0 | ruby -e 'puts STDIN.read.scan(/inet ([0-9.]+)/)'
```

以下、色々書き方があるということで参考。
書く人とメンテナンスに関わる人が分かりやすければ、特に短くすることにこだわる必要はないが、テクニックをただ知らないだけなのと、知っていて使わないのとはまた別なので、一応。

**配列型をなるべく残した書き方**
Haskellをやっていると演算の合成を意識した書き方で可読性が高まることがある。RubyでいうOne-linerでは配列から文字列を取り出す部分の `[n]` の演算で作成時にコード誤りを起こしやすい。(どちらかというと、出来上がってからというより変更するときに間違えやすい)

```shell
ip address show dev eth0 | ruby -e 'puts STDIN.read.split("\n").select{|s| /inet\W/ =~ s}.map{|s| s.split(%r@\s+|/@)[2]}[0]'
```

**上記を短くする書き方**

`Array#filter` の代わりに `Enumerable#grep` を使うともう少し短くなる。

```shell
ip address show dev eth0 | ruby -e 'puts STDIN.readlines.grep(/inet\W/)[0].split(%r@\s+|/@)[2]'
```

### コマンド実行の場合

```shell
ruby -e 'puts IO.popen("ip address show dev eth0") {|io| io.read}.scan(/inet ([0-9.]+)/)'
```

```shell
ruby -e 'puts IO.popen("ip address show dev eth0") {|io| io.read}.split("\n").filter{|s| /inet\W/ =~ s}.map{|s| s.split(%r@\s+|\/@)[2]}[0]'
```

### クラスを使う

今回は必ず目当ての IP が見つかるので select でなく find にした。

```shell
ruby -r socket -e "puts Socket.getifaddrs.find{_1.name == 'eth0' and _1.addr.ipv4?}.addr.ip_address"
```

## Python版

* メソッドチェーンが使えず関数呼び出しが必要となることが多い。
* Python 自体は、OSの必須パッケージとなっていることが多く強みがある
  一方で古い環境では Python3 が入っていなかったりするし、python3 のコマンドやパスは環境によって異なることで煩わしく思うこともある
* Pythonはサードパーティーのパッケージを導入するとより簡潔に取得できる (環境変更が容易ならこれも選択肢の一つ)

### 標準入力から読み取る場合

```shell
ip address show dev eth0 | python -c "import re, sys; print(re.findall('[ ]+inet (.*)/[0-9]'
, sys.stdin.read())[0])"
```

### コマンド実行の場合
コマンドから読み取る場合は、バイト文字列となるので Unicode文字列に変換する必要がある。

```shell
python -c "import re, subprocess; print(re.findall('[ ]+inet (.*)/[0-9]', subprocess.run('ip address show dev eth0'.split(), stdout=subprocess.PIPE).stdout.decode('utf-8'))[0])"
```

### 標準ライブラリ中のクラスを使う

connect するのが気持ち悪いですが、組み込みのクラスだけで済ます場合は可読性が高めな選択肢は少なくなる。

```shell
python -c 'import fcntl, socket, struct; s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.connect(("8.8.8.8", 80)); print(s.getsockname()[0])'
```

* [unix - How can I get the IP address from a NIC (network interface controller) in Python? - Stack Overflow](https://stackoverflow.com/questions/24196932/how-can-i-get-the-ip-address-from-a-nic-network-interface-controller-in-python)

## あとがき

難しくないにしても grep / awk / sed などのコマンドの知識をどこまで要求するかというのは気になるところです。最近はアプリ畑から入った人は Python 知っているけどシェルは知らんという人も今はいそうなので。個人的にはシェルは知っているので、こういうインフラ寄りのは基本は awk 中心に組むと思います。

One-linersに関しては、メソッドチェーンの関係でRubyは組みやすい傾向にあります。プログラミングが複雑化しそうな状況では、人や環境が許せば Ruby もいいかなと思います。やはり、文字列処理は書きやすいです。

今回一番組みにくかったのが Python です。Pythonだけができるプログラマだけなら選択肢に入ってきますが、サードパーティーのパッケージは時が経つと非公開になることもあるので、今回は一番最後に使いたい言語です。