「Java BlockingQueueで遊ぶ：パイプラインごっこ」でパイプラインの話をしたので、本来の、つまりUnixのパイプやリダイレクトを少し調べてみました。

たまに話題となる some-command >file 2>&1 と some-command 2>&1 >file の挙動の違いについて、「シェルは コマンドライン リダイレクトの指定を右から左に解釈実行する」なんて説明が見つかりました。んなバカな！ パージングは左から右にするものですよ。パーズツリーを逆順にたどることはできるけど、そんなことする必然性はなんにもないよ。

次の記事を読むと、「右から左」なんて事情じゃないことが分かるでしょう。

さてここでは、複雑なリダイレクト処理も完全に理解できる処方箋を示しましょう。例えば、次のコマンドラインが何をするか分かるようになるってことです :-)

some-command 3>&1 >/dev/null 2>&3 3>&- | less

シェルが行っている内部処理を知ればいいのですが、ここでは別のアプローチを取ります*1。

内容：

コマンド実行を記述するミニ言語 パイプラインのなかのコマンド実行単位 リダイレクトは代入文 複雑な例もスッキリわかる

●コマンド実行を記述するミニ言語

パイプ記号「|」や逐次実行の記号「;」を含んだ長いコマンドラインも、１つのコマンドとその引数、それとリダイレクト指定からなる“成分”に分解できます。こういった成分をなんと呼ぶか僕は知らないので、仮に「コマンド実行単位」とでも呼びましょう*2。

例えば、 grep ^a < infile > outfile は全体で1つのコマンド実行単位です。これを次のように書いてみましょう。



# デフォルトの初期化

in fd0 = in(/dev/tty)

out fd1 = out(/dev/tty)

out fd2 = out(/dev/tty) # リダイレクト指定

fd0 = in(infile) # < infile に対応

fd1 = out(outfile) # > outfile に対応 grep ^a

なんじゃこりゃ？ たった今、僕が考えたエエカゲンな記法です。この記法を使ってリダイレクトの秘密をあばこう、という魂胆です。

fdはファイルディスクリプタのつもりで、fd0は「0番のファイルディスクリプタ」のことです。詳細はともかく、次のことは知っておいてください。

0番のファイルディスクリプタは標準入力 1番のファイルディスクリプタは標準出力 2番のファイルディスクリプタは標準エラー出力

他に、fd3からfd9まで使えます（シェルの実装によりますけど）。

in fd0 は、「0番のファイルディスクリプタfd0を入力用に使う」という宣言です。 in(/dev/tty) は、ファイル/dev/ttyを入力用に開いたモノを示します。そしてイコールは普通の代入演算子です。 out fd1 = out(/dev/tty) なら、「出力用に開いたファイル/dev/ttyを、1番のファイルディスクリプタ（出力用）fd1に割り当てる」と読めます。/dev/ttyは特殊なファイルで、in(/dev/tty)はキーボードからの入力、out(/dev/tty)は画面への出力を意味します。Windowsなら、/dev/ttyの代わりにconという名前を使います。

リダイレクト指定がなければ、デフォルトの初期化がそのまま使われます。今回の例では、 < infile > outfile に対応する代入文によりデフォルトの初期化が上書き（変更）されています。 grep ^a が実行されるときには、fd0がin(infile）に、fd1がout(outfile)に設定されています（fd2はそのまま）。実際のシェルは、無駄な初期化はしないと思いますが、デフォルトを書いておいたほうが分かりやすいかと。

このような“代入文”を並べてファイルディスクリプタ達を設定してから、最後の行に実行するコマンドと引数を書きます -- これが、コマンド実行を記述するミニ言語の構文です。

●パイプラインのなかのコマンド実行単位

次に、 cat infile | grep ^a | less を考えてみましょう。このパイプラインのなかにある grep ^a はコマンド実行単位ですが、次のように記述されます。



in fd0 = in(l-pipe)

out fd1 = out(r-pipe)

out fd2 = out(/dev/tty) grep ^a

ここで、l-pipe、r-pipeはファイル名ではなくて、コマンドの左側（left）のパイプと右側（right）のパイプを意味するとします。 grep ^a だけ見ていてもファイルディスクリプタの状況は分かりませんが、パイプラインのなかにいるという状況を含めて記述しています。

同じパイプライン内のlessの実行は次のようですね。



in fd0 = in(l-pipe)

out fd1 = out(/dev/tty)

out fd2 = out(/dev/tty) less

grepのr-pipeとlessのl-pipeは同じパイプをさしているので、データがgrepからlessへと流れます。

●リダイレクトは代入文

前節の例ではリダイレクトが出てこなかったのですが、今度は cat infile | grep [ 2>&1 | less を考えましょう。 grep [ はエラーで grep: Invalid regular expression というエラーメッセージが標準エラー出力、つまりfd2に出力されます。

コマンド実行単位 grep [ 2>&1 は、次のように記述できます。



# デフォルトの初期化

in fd0 = in(l-pipe)

out fd1 = out(r-pipe)

out fd2 = out(/dev/tty) # リダイレクト指定

fd2 = fd1 grep [

リダイレクト指定 2>&1 は、 fd2 = fd1 という代入文と解釈されます。fd1の値はout(r-pipe)だったので、代入文によりfd2の値もout(r-pipe)となります。その結果、grepによるfd2への書き込み（エラーメッセージ）はパイプに送り込まれ、結果的にlessに流れます。

なお、このパイプラインを実行しても、受け手がlessだと分かりにくいので、 cat infile | grep [ 2>&1 | cat > outfile として、ファイルoutfileの中身を見るといいかもしれません。

●複雑な例もスッキリわかる

番号 >file は、fd 番号 = out(file)

>file は、fd = out(file) 番号1>&番号2 は、fd番号1 = fd番号2

という翻訳規則に基づいて、冒頭に挙げた2つのコマンドライン some-command >file 2>&1 と some-command 2>&1 >file を分析してみましょう。

some-command >file 2>&1 は、



# デフォルトの初期化

in fd0 = in(/dev/tty)

out fd1 = out(/dev/tty)

out fd2 = out(/dev/tty) # リダイレクト指定

fd1 = out(file)

fd2 = fd1 some-command

リダイレクト指定 >file 2>&1 は、左から右に解釈されます。 >file は 1>file の略記で、 fd1 = out(file) と翻訳されます。デフォルトの初期化が上書きされて、fd1の値はout(file)となります。その後で、 fd2 = fd1 が実行されるので、fd2の値もout(file)になります。その結果として、fd1の出力もfd2の出力も同じファイルに入ります。

一方、 some-command 2>&1 >file は、



# デフォルトの初期化

in fd0 = in(/dev/tty)

out fd1 = out(/dev/tty)

out fd2 = out(/dev/tty) # リダイレクト指定

fd2 = fd1

fd1 = out(file) some-command

fd2 = fd1

fd1 = out(file)

some-command >file

が先に実行されるので、fd2の値はfd1と同じout(/dev/tty)となりますが、これは結局、何もしないのと一緒ですね。その後のでfd1の値は変更されます。これは、と何も変わりません。

むずかしそうな例をやってみましょう。 some-command 3>&1 >/dev/null 2>&3 3>&- | less の最初の実行単位の場合は、



# デフォルトの初期化

in fd0 = in(/dev/tty)

out fd1 = out(r-pipe)

out fd2 = out(/dev/tty) # リダイレクト指定

fd3 = fd1

fd1 = out(/dev/null)

fd2 = fd3

fd3 = none some-command

n>&-

ここで、noneは「何も表さない値」で、noneがファイル ディスクリプタ に代入されると、そのファイル ディスクリプタ は閉じられ使えなくなります。一時的に使ったファイル ディスクリプタ の後始末にnoneを使います（後始末しなくても、たいてい大丈夫ですが）。 コマンドライン 構文では、番号nに対してとしてファイル ディスクリプタ nを閉じます。

さて、代入文を追いかけると、



fd3 = fd1 # fd1の値をfd3に保存 fd3 = out(r-pipe)

fd1 = out(/dev/null) # fd1の値をout(/dev/null)で上書き

fd2 = fd3 # fd2の値はfd3の値であるout(r-pipe)となる

fd3 = none # fd3を後始末 # fd1 is out(/dev/null)

# fd2 is out(r-pipe)

# fd3 is none

よって、some-commandの標準出力（fd1）は/dev/nullに捨てられ、標準エラー出力（fd2）がパイプに送り込まれます。パイプの先にlessがいるので、some-commandのエラーメッセージだけをlessで閲覧できるわけです。

応答や補足：