キマイラ・サイトは http://www.chimaira.org/です。
トラックバック/コメントは日付を気にせずにどうぞ。
連絡は hiyama{at}chimaira{dot}org へ。
蒸し返し歓迎!
ところで、アーカイブってけっこう便利ですよ。タクソノミーも作成中。
2007-12-21 (金)
シェルのリダイレクトを「こわいものなし」というくらい完全に理解しよう
雑記/備忘 | |
「Java BlockingQueueで遊ぶ:パイプラインごっこ」でパイプラインの話をしたので、本来の、つまりUnixのパイプやリダイレクトを少し調べてみました。
たまに話題となる some-command >file 2>&1 と some-command 2>&1 >fileの挙動の違いについて、「シェルはコマンドラインリダイレクトの指定を右から左に解釈実行する」なんて説明が見つかりました。んなバカな! パージングは左から右にするものですよ。パーズツリーを逆順にたどることはできるけど、そんなことする必然性はなんにもないよ。
次の記事を読むと、「右から左」なんて事情じゃないことが分かるでしょう。
さてここでは、複雑なリダイレクト処理も完全に理解できる処方箋を示しましょう。例えば、次のコマンドラインが何をするか分かるようになるってことです :-)
-
some-command 3>&1 >/dev/null 2>&3 3>&- | less
シェルが行っている内部処理を知ればいいのですが、ここでは別のアプローチを取ります*1。
内容:
- コマンド実行を記述するミニ言語
- パイプラインのなかのコマンド実行単位
- リダイレクトは代入文
- 複雑な例もスッキリわかる
●コマンド実行を記述するミニ言語
パイプ記号「|」や逐次実行の記号「;」を含んだ長いコマンドラインも、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番のファイルディスクリプタ」のことです。詳細はともかく、次のことは知っておいてください。
他に、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>&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が先に実行されるので、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で閲覧できるわけです。
応答や補足:
- Webアプリを作ろう - 12/21 scrap
- シェルのリダイレクト。
- パイプとかリダイレクトとかがちょっと理解した。
- The web’s most interesting stories on Fri 21st Dec 2007
- 檜山正幸のキマイラ飼育記 - シェルのリダイレクトの補遺
- WEB開発日記 - シェルのリダイレクトを「こわいものなし」というく...
- 日本 GNU AWK ユーザー会 0.2 - 未消化物
- tail -f /var/log/こうちかずお.log - shellのredirect(シェルのリ...
- パラダイムシフトふぁくとりー資材置き場 - ParadigmShift Factory’...
- foosinの日記 - [Linux]
- 愚者のニュース - 2007-12-24 IT
- すぱいだー日記。 - リダイレクト
- madknightの日記 - 在庫ブックマーク
- 檜山正幸のキマイラ飼育記 - すんません、絵を描いてもらっちょりま...
- 人・時・空の間 - Shell のリダイレクト
- toruotの日記 - シェルのパイプとリダイレクトを図にしてみた
- Life is Hacked -spooky の頭ん中- - ファイルディスクリプタと...
- github.comでphp-users.jpを管理するまでの作業ログ
- 総合的な学習のお時間 - 2008年このWebページが凄かった25選(くら...
- ソースコード備忘録 - シェル パイプとリダイレクト
- 檜山正幸のキマイラ飼育記 - これは便利、tarを使ってファイルをコ...
- 1077 http://www.hatena.ne.jp/
- 1030 http://d.hatena.ne.jp/
- 1011 http://b.hatena.ne.jp/hotentry
- 1011 http://www.google.co.jp/search?hl=ja&q=シェル+%d&lr=
- 997 http://secure.ddo.jp/~kaku/tdiary/
- 862 http://b.hatena.ne.jp/
- 704 http://reader.livedoor.com/reader/
- 405 http://feed.designlinkdatabase.net/feed/outsite_61319.aspx
- 397 http://www.google.co.jp/search?q=シェル リダイレクト 2つ&sourceid=navclient-ff&ie=UTF-8&rlz=1B3GGGL_jaJP218JP218
- 374 http://www.google.co.jp/search?sourceid=navclient&hl=ja&ie=UTF-8&rlz=1T4SUNA_jaJP229JP230&q=make+INCLUDES+-I+構文