zshで悲願のファイルロック

## Advent Calendarって25日目あるんだっけ？

ってことで、エクストラ的な位置づけでひとつ……。
というのもアタクシ、シェルスクリプト野郎ではあるものの、zshはこのAdvent Calendar見てはじめまして、最近zshの本を買って読みだしたペーペーでございます。

### update 2013/12/25 19:45

頂いたコメントで目標達成できました。感謝！そしてさすがzsh、やはり最強だった……

## 本日のテーマ「flock(2)みたいなロックがやりたい」

シェルスクリプトのファンとして残念なのは、`flock(2)`のようなファイルロックコマンドが無いこと。誰にも邪魔されずにファイルを読み書きしたいこととか、多重起動を防止したいこととかよくありますから。えぇ、OSによってはそれっぽいのは確かにあります。Linuxには[`flock(1)`コマンド](http://linuxjm.sourceforge.jp/html/util-linux/man1/flock.1.html)があって実際に内部でflock(2)を使っていますが、 **「違う違う、そうじゃ、そうじゃなーい」** と思うのです。

だって、flock(1)コマンドって自分が実行されてる間しかflock(2)してくれないじゃないですか。そうじゃなくて、 flock(2)掛けたまま次のコマンドに進んでくれないかなーと。そしてflock(2)掛けたまま次へ進みつつも、コマンド呼出元の親シェルが終わったら自動的にロック解除してくれる、と。これで親シェルが解除忘れたり、突然死しても安心です。つまり、

1. **flock(2)掛けたまま次のコマンド**に進ませてくれる

1. ロックは **親シェル死んだら自動解除**

1. ロックの順番待ちもできれば、なおありがたい

なのが欲しかったんです。1だけなら、`mkdir`や`ln`コマンドを使い、予め決めた名前のファイルを作れるか試すという古典的手段がありますがそれだと3ができないし、何より親の突然死に対する補償がない！使用者が``kill -9``とかしてきたら太刀打ちできないじゃないか！
というわけで、そういうflock(2)っぽいロックコマンドがシェルスクリプト向けに用意されていないことが悔しかったのです。

## とりあえず、素のsh向けに作った

悔しさをバネに、作りました。ぜぇぜぇ……。
それがこちら、[exflock@Gist](https://gist.github.com/richmikan/8080844#file-exflock)コマンド。排他ロック専用なのでコマンド名の頭にexを付けています。

### このシェルスクリプトのアイデア

Linuxの[flock(1)](http://linuxjm.sourceforge.jp/html/util-linux/man1/flock.1.html)や、FreeBSDの[lockf(1)](http://www.jp.freebsd.org/cgi/mroff.cgi?subdir=man&lc=1&cmd=&man=lockf&dir=jpman-5.4.0%2Fman&sect=0)など(以降flock(1)類と呼びます)、いずれもこれらのコマンド自身が実行されている間しかflock(2)を掛けてくれません。その代わり、「ロックを掛けている間に引数で指定した別のシェルスクリプトを呼んであげます」というのがflock(1)類の仕事です。

なのでまず、flock(1)類を`&`を付けてバックグラウンド実行させ、次のコマンドへ進めるようにします。そして、お目当てのファイルにflock(2)を掛けてる最中のflock(1)類に実行させるサブコマンドに、次の仕事を与えます。

1. 自分のプロセスIDを、親プロセス(今回作ったコマンド自身じゃなくてそれを呼びだしているプロセス)に教える。⇒親プロセスはunlockしたい時にそのプロセスにSIGTERM等を送ればよい。

2. 親プロセス(今回作ったコマンド自身じゃなくてそれを呼びだしているプロセス)の生死を監視し、死んでいたら自分も死ぬ。

3. タイムアウト秒数を設定して、その時間が経過したらやはり死ぬ。

しかし、たったこれだけのことをするのに実質140行ものシェルスクリプトになってしまいました。じっくり読んでいただけるならわかりますが、トリッキーなことや、細かな問題回避作業をいろいろせざるを得ませんでした。

## zshでどれだけスマートに書けるかな？

というわけで、目下勉強中のzshでリファクタリングしてみます。さーて、やるぞー！

### 引数解釈部分で早速改良

引数が数字のみで構成されているかどうかのチェックを行ってる箇所がありますが、ここで早速外部コマンドを削れるじゃないですか。強力な文字列加工が自力でできるのがzshのひとついいところなんですよね。

```sh:exflock(line.114-115)
  echo "_$1" | grep '^_[0-9]\+$' >/dev/null 2>&1
  [ $? -eq 0 ] || print_usage_and_exit
```

```zsh:exflock.zsh(line.114)
  [ -n "${1//[^0-9]/}" ] || print_usage_and_exit
```

### 親プロセスIDなんてPPID見りゃわかる

素のsh向けでは、``ps``や``awk``コマンドに頼っていた親プロセスID検出も、zshなら特殊シェル変数``PPID``見りゃ済みます。なので、この行はゴッソリ削れますね。

```sh:exflock(line.134-136)
  # --- investigate the caller process ID ----------------------------
  pid=$$
  pid=$(ps -Ao pid,ppid | awk '$1=='$pid'{print $2}')
```

### あれ!? jobsコマンドのサブシェル化はできないの?

あと、素のshでスマートでないと思っているのがこの部分。

```sh:exflock(line.140-143)
  temp_datafile=$(mktemp -t "${0##*/}.XXXXXXXX")
  [ $? -eq 0 ] || { echo "${0##*/}: Can't make a tempfile" 1>&2; exit_trap 2; }
  jobs -l > "$temp_datafile"
  sleepjob=$(cat "$temp_datafile" | tr -C '0-9\n' ' ' | awk '{print $1,$2}')
```

素のshはjobsコマンドをサブシェル(つまりパイプや小括弧の中)で動かすとジョブ情報をうまく取得できません。まぁjobsコマンドの性質を顧みると言われてみればそりゃそうなのですが……。だから、ジョブ情報をシェル変数に入れたいとなったら、一旦テンポラリーファイルに書き出してからcatで拾うなどしなければなりません。なんかカッコ悪いよなぁ……。

でも、例えばbashで試してみるとその点に気を利かせてくれているようで、こういう書き方ができちゃいます。

```bash:bashでリファクタリング
  sleepjob=$(jobs -l | tr -C '0-9\n' ' ' | awk '{print $1,$2}')
```

「うん、スマートスマート！当然zshでもできるよね」と思って書き直したら……、なんと動きません。ちなみに試したzshのバージョンは5.0.2です。

## 他には? あれ、おしまいなの!?

えぇと、zsh歴の浅いアタクシにはここまででした。[exflock.zsh@Gist](https://gist.github.com/richmikan/8080844#file-exflock-zsh)

結局たった6行のダイエット……。例えば、"trap" + "trap関数" などの記述も、TRAPsigname関数一本に置き換えられないかなーと思ったんですが、複数シグナルの面倒を見るには利用できないみたいで……なかなかダイエットできない。

でもzshを駆使したらもっとスマートに書けるんじゃないかと思うんですよね。

**よし来年は、zshマスターになるぞ！！！**

おあとがよろしいようで……。
zsh Advent Calendar 2013、皆さんの投稿、面白かったです。
では、また来年よろしくお願いします。