2016年5月30日月曜日

シェルを電卓として使いたいとき

いろいろな方法が考えられる。

適当な言語のインタプリタを使う
真っ先に思い浮かぶ方法。よく使いそうなのというとPythonとかRuby(irb)とかGauche(gosh)とかだろうか。
プログラミング言語の高い表現力を享受できる代わりに(相対的に)巨大な言語がインストールされている必要がある。

計算用のコマンドを使う
bc、dc、exprなどがある。相対的に小さく、軽いが癖のあるものが多い。一番使いやすいのはbcだろうか。

awkを使う
確かにawkは計算ができる。だがシェル上に存在する問題すべてをawkで解決しようとするのはいかがなものか。「金鎚を手にするとあらゆる問題が釘に見える」とはこの謂か。もちろん文字列処理の一環として計算を行うならこれ以上のものはそうそうない。
awk 'BEGIN{print sin(30*3.1416/180)}'

シェルの機能を使う
$(())の中で多少の演算が可能である。これは別にBashやZshの拡張機能ではなくPOSIXで定義されているので(2.6.4 Arithmetic Expansionを参照)基本的にどのシェルでも使える。
echo $((256 + 998))
またこれはシェルスクリプトで変数のカウンタを回すときにも重用する。
count=`expr $count + 1`
ではなく
count=$((count + 1))
でいい。シェル組み込みの機能なので呼び出しのオーバーヘッドがない……はず。もっとも大した差ではないだろうけど。

[2016/5/31追記]
試してみたら大違いだった。
$ cat test.sh
count=0
while [ 1000000 -gt $count ]
do
    count=$((count + 1))
done

$ time sh test.sh 
real 0m8.446s
user 0m8.436s
sys 0m0.004s
に対し
$ cat test.sh
count=0
while [ 1000000 -gt $count ]
do
    count=`expr $count + 1`
done

$ time sh test.sh 
real 9m4.312s
user 0m29.381s
sys 1m19.182s

使用した環境はArch Linux, Bash4.3.42, expr (GNU coreutils) 8.25。ハードはCPU Intel Core i5 3475S, メモリ24GB。ちなみに何度か試したらbash(約8秒)よりzsh(約4秒)やbusybox sh(約3秒)のほうが高速だった。



なんでこんなことをまとめたかというと、仕事で文字列処理がしたくて、でもPCはWindowsだしソフトをインストールできないからWindows用busyboxを使うことにし、Powershellを立ち上げると横で電卓を使うのが面倒になりシェル上から計算もしたいなーと考えた結果である。シェルを手にするとあらゆる問題がシェル上の問題に見えてくるのだ。

Busyboxは便利だ。Powershellで
busybox.exe sh -l
するだけで立派な環境が手に入る。1MBもないバイナリの中にsedもawkもsortもuniqもcutもgrepもviさえも……もうなにも怖くない。

2016年5月27日金曜日

IPv4にマッチする正規表現

ひょんなことからIPv4のIPアドレスを判定する正規表現を考えることになった。考えた流れをここに残しておくことにする。

IPv4というと

192.168.0.1

こんなやつである。「.」で区切られた4つの0〜255までの数値からなるわけだ。

なので第一歩。
0〜255の数値にマッチする正規表現をXXXとすると
(XXX)\.(XXX)\.(XXX)\.(XXX)
である。

今回は別にキャプチャしたいわけでもないので「(XXX)\.」が3回続くのに注目してまとめることができる。
((XXX)\.){3}(XXX)

あとは肝心のXXXを考えるだけだ。

必要なのは場合分け。それぞれの場合についての表現をX1,X2,X3……とするとXXXは(X1|X2|X3|……)と表せる。

まず1桁の場合。0〜9までなので簡単だ。
[0-9]

続いて2桁の場合。10〜99。1〜9に続けて0〜9が並ぶと言い換えることができるので
[1-9][0-9]

3桁の場合は100の位が1か2かで分ける必要がある。
100〜199については簡単だ。1に続けて0〜9が2回並ぶ。
1[0-9][0-9]
もちろん繰り返しを使って1[0-9]{2}としてもよい。

200〜255についてはさらに場合分けが必要だ。
200〜249に関しては1の位として0〜9をとりうる。よって
2[0-4][1-9]

250〜255は見ての通り1の位に6〜9をとってはいけない。すなわち
25[0-5]

さあ。これだけをまとめていこう。
[0-9]|[1-9][0-9]|1[0-9{2}|2[0-4][0-9]|25[0-5]

これをXXXに当てはめて
(([0-9]|[1-9][0-9]|1[0-9{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9{2}|2[0-4][0-9]|25[0-5])

今回の問題は判定する文字列が1行として与えられるものだったので行頭と行末をつけて
^(([0-9]|[1-9][0-9]|1[0-9{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9{2}|2[0-4][0-9]|25[0-5])$

ふう。

※あとで調べていたら1桁の場合と2桁の場合はまとめることができることがわかった。[0-9]と[1-9][0-9]を一緒にして[1-9]?[0-9]となり、結果としては[1-9]?[0-9]|1[0-9{2}|2[0-4][0-9]|25[0-5]を用いて
^(([1-9]?[0-9]|1[0-9{2}|2[0-4][0-9]|25[0-5])\.){3}([1-9]?[0-9]|1[0-9{2}|2[0-4][0-9]|25[0-5])$
と表せる。詰めが甘かった。

こういう場合は[0-9]の代わりに\dを使うと何をしたいのか不明瞭になるな、と考えるなど。