このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

シェルのリダイレクトを「こわいものなし」というくらい完全に理解しよう

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

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

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

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

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

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

内容:

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

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

パイプ記号「|」や逐次実行の記号「;」を含んだ長いコマンドラインも、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番のファイルディスクリプタ」のことです。詳細はともかく、次のことは知っておいてください。

  1. 0番のファイルディスクリプタは標準入力
  2. 1番のファイルディスクリプタは標準出力
  3. 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)
  • 番号1>&番号2 は、fd番号1 = fd番号2

という翻訳規則に基づいて、冒頭に挙げた2つのコマンドライン some-command >file 2>&1some-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は、左から右に解釈されます。>file1>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が先に実行されるので、fd2の値はfd1と同じout(/dev/tty)となりますが、これは結局、何もしないのと一緒ですね。その後のfd1 = out(file)でfd1の値は変更されます。これは、some-command >file と何も変わりません。

むずかしそうな例をやってみましょう。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

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

応答や補足:

*1:シェルの内部処理を別な表現にしているだけ、と突っこまれそうですが、少なくともシステムコールdup2とかは出してません。

*2:bashのmanページによると、simple commandと呼ぶのが適切みたいです。