シェルとファイルデスクリプタのお話

Shell Script Advent Calendar 2016が始まりました。1日目は[私](https://github.com/ueokande)です。好きなシェルはBashです。

はじめに
--------

**ファイルデスクリプタ**を知ることで、よりスマートにシェルを記述でき、シェル芸の幅も広がります。この記事はシェルとファイルデスクリプタの理解を深めることを目的としており、対象は次のような人です。

- パイプ・リダイレクトをうまく使えない人
- `2>&1` の置き場所にいつも迷う人、そもそも`2>&1`が何って人？
- シェルの作業で中間ファイルを作成している人

ファイルデスクリプタとは
------------------------

**ファイルデスクリプタ (FD)** とは、プロセスが入出力するファイルやデバイスを扱うためのインターフェイスです。これはUNIX系OSの**デバイスとファイルは透過的に扱える**ことと関係し、任意のファイルもデバイスも同じAPIで操作できます。

FDは扱うデバイス・ファイルごとに番号が振られ、プログラムはその番号でファイル・デバイスを識別します。標準の入出力デバイスにFD0、FD1、FD2が用意されており、それぞれ標準入力 (stdin)、標準出力 (stdout)、標準エラー出力 (stderr) です。プロセスが現在使用しているFDは`/dev/fd/`以下からアクセスでき、中身は実体となるデバイスやファイルへのシンボリックリンクです。

試しに `stat` を使って確認してみましょう。`stat`に`-c%N`をオプションにつけると、リンク先を見ることができます。このとき表示されるのは、`stat` プロセスが使用するFDです。

```sh
$ stat -c%N /dev/fd/*
'/dev/fd/0' -> '/dev/pts/1'    # stdin
'/dev/fd/1' -> '/dev/pts/1'    # stdout
'/dev/fd/2' -> '/dev/pts/1'    # stderr
```
`stat`が使用するFD0、FD1、FD2が、`/dev/pts/1`を指しています。これは`tty`コマンドの結果と一致します。つまりstdin, stdout, stderrは使用している端末であることを意味し、`stat`の出力 (stdout) は端末に書き込まれます。

リダイレクト
------------

先程の例はFDは端末を指していました。FDが指す先を変更する機能が多くのシェルにも備わっており、おそらく読者はすでに知っています。その一つがリダイレクトです。次の例はFD0を`<`、FD1を `1>` で、FD2を `2>` で指定します。

```sh
$ stat -c%N /dev/fd/* </dev/zero 1>/tmp/output 2>/dev/null
```

おっとこのままでは結果までリダイレクトされて、画面に出力されません。stdoutで出力したファイルの内容を確認してみましょう。

```sh
$ cat output 
'/dev/fd/0' -> '/dev/zero'      # stdin
'/dev/fd/1' -> '/tmp/output'    # stdout
'/dev/fd/2' -> '/dev/null'      # stderr
```

プログラムはFDの実体がデバイスかファイルかを気にせず、FD0、FD1、FD2へ読み書きしています。`stat`はFD1への書き込みにputs(3)やputchar(3)を使いますが、FD1が何なのかは気にしません。FDという透過的なインターフェイスにより、プログラムは同じAPIを使ってFDにアクセスします。そして外部からFDの指す先を、画面やファイルに切り替えてるだけです。

FDの複製・移動
--------------

次は、FDの実体を別のFDにも割り当てる、FDの複製と移動についてです。次の例はstdout/stderrの出力を共に`/dev/null`に送り出す定石です。

```sh
$ date 1>/dev/null 2>&1
```

シェル初心者は、`1>/dev/null`と`2>&1`の順番で迷います。しかしそれぞれの意味することを紐解くことで、つまづきません。

まず前提として、リダイレクトは**左から評価**されます。まず`1>/dev/null`は、FD1の指す先を`/dev/null`にします。そして`2>&1`が意味するのは、FD1の指す先（この場合`/dev/null`）をFD2にも割り当てます。これはFD1をFD2に**複製**していることになります。

もうひとつ、FDの**移動**についても説明します。FDの移動は複製と違い、移動元のFDが消えます。次の例はstdoutとstderrの出力先を入れ替える例です。swapの実装に一時変数が必要なように、FDの入れ替えも一時的なFDを使います。

```sh
$ date 3>&1 1>&2 2>&3-
```

1つ目の `3>&1` で、FD1を新たなFD3に複製します。2つ目の `1>&2` でFD2をFD1に複製します。3つ目の `2>&3-` でFD3をFD2に**移動**します。`date`からは移動元のFD3は見えず、扱えるFDはFD0、FD1、FD2のみとなります。

FDの複製や移動はもっぱら、シェルの組込みコマンド`exec`と併用されることが多いです。`exec`コマンドを使うと、現在のシェルのリダイレクト先を変更できます。移動元を指定せずFDの移動をすると、移動先のFDを閉じます。

```sh
$ exec 3>>/tmp/output   # /tmp/outputをFD3に割り当て
$ echo "Hello" >&3      # FD3に出力
$ exec 3>&-             # FD3を閉じる
```

意図しない出力を防ぐためにも、使い終わったFDは閉じましょう。

パイプ
------

さてここまででは単一のプロセスでの話でした。次にシェルで欠せないパイプです。パイプ `|` を使うと、FDはパイプと呼ばれる特殊なFIFOデバイスを指します。

以下の例は`cal`のstdoutが`wc`のstdinに渡されます。その仕組みは、`cal`プロセスのFD1にパイプが割り当てられ、`wc`プロセスのFD0に同じパイプが割り当てられます。リダイレクト同様、プログラムはFDがファイルかパイプかを気にせず、`cal`はFD1に書き込みを、`wc`はFD0の文字数をカウントするのみです。

```sh
$ cal | wc
```

パイプのFDとその実体を確認してみましょう。次の例は、`stat`でFD1のリンク先を確認しています。`cat`はご存知の通り、FD0をFD1に流すコマンドで、`stat`の出力を端末に表示します。

```sh
$ stat -c%N /dev/fd/1 | cat
'/dev/fd/1' -> 'pipe:[152817]'
```

同様にパイプされたプロセスのFD0も表示してみます。

```sh
$ : | stat -c%N /dev/fd/0 
'/dev/fd/0' -> 'pipe:[152818]'
```

`|` で作られるパイプは**匿名パイプ**と呼ばれます。任意の名前をつけられる**名前付きパイプ**は `mkfifo` で作成できます。

```sh
$ mkfifo /tmp/tmppipe
$ date >/tmp/tmppipe &
$ cat /tmp/tmppipe 
Tue Dec 01 12:34:56 JST 2016
```

プロセス置換
------------

**プロセス置換**は匿名パイプよりも柔軟かつ強力な機能です。プロセス置換ではパイプをFDとして取得できます（**匿名名前付きパイプ**と呼ばれたりします）。そのためパイプをパス名として参照でき、プロセスの出力を別のプロセスにコマンドライン引数として渡せます。

次の例は2つのコマンドの結果を比較するときに用いられるイディオムです。プロセス置換はリダイレクトと構文が似てますが、全くの別物です。`diff`はご存知の通り、引数に与えられたファイルを比較するコマンドです。

```sh
$ diff  <(ls /usr) <(ls /usr/local)
```

プロセス置換を用いることで、複数のプロセスの出力をひとつのコマンドに渡せます。`diff`にはそれぞれのプロセスの出力がパス名として渡されますが、`diff`はファイル同様与えられたファイルを読み込んで差分を計算するだけです。

どんなファイル名が渡されているかは、いつものように `stat` で確認してみましょう。

```sh
$ stat -c%N <(ls /usr) <(ls /usr/local)
'/dev/fd/63' -> 'pipe:[187612]'
'/dev/fd/62' -> 'pipe:[187614]'
```

statにはFDのパスが渡され、その実態はパイプです。匿名パイプと同様に、パイプからはプロセスの出力が取得できます。

また `stat -c%A` でファイルのアクセス権も見てみましょう。興味深いことに `>(...)` はwrite-onlyに、`<(...)` はread-onlyになっています。

```sh
$ stat -c%A >(:) <(:)
l-wx------
lr-x------
```
応用例
------

ここまでは説明ばかりで少々退屈だったかも知れません。最後にいくつかの応用例を紹介します。

### プロセス置換とリダイレクトの組み合わせ

プロセス置換はパイプをパスとして扱えるので、プロセス置換へのリダイレクトも可能です。次の例は、あるコマンドのstdoutとstderrを整形した後に、ファイルに追加書き込みします。

```sh
$ date \
    1> >(fmt >>out.log) \
    2> >(fmt >>err.log)
```

`1>` によりFD1はプロセス置換へ出力され、受け取る側のプロセスは`fmt`コマンド（所定の幅にフォーマット）を経て`out.log`に追加書き込みします。FD2も同様に、`2>`でリダイレクトされて、`fmt`した後に `err.log` に追加書き込みされます。

### teeコマンドとの組み合わせ

stdinからの入力をstdoutと（複数の）ファイルに分岐させる`tee`コマンドも、プロセス置換と相性が良いです。`tee`はFD0から読み込んだデータを、引数に与えられたパスに書き込みます。

次の例では、あるサーバを起動するコマンド`start-server`コマンドの出力を`tee`に渡します。`tee`は`start-server`の出力をパイプで受け取ります。そしてプロセス置換でそれぞれのコマンドに出力し、すべての出力は`all.log`に出力します。それぞれのプロセス置換の中は、GETやPOSTにマッチする行を`grep`して、`get.log`や`post.log`に出力します。

```sh
$ start-server | tee >(grep 'GET' >get.log) >(grep 'POST' >post.log) >all.log
```
終わりに
--------

FDの仕組みは単純ですが、すごく合理的な仕組みです。FDを意識することで、いつもよりprettyにリダイレクトできたり、中間ファイル作成の手間を省けたりできます。

2日目は @hamano_t さんです、よろしくお願いします！