パスワードを忘れた? アカウント作成
41237 journal

okkyの日記: リダイレクションの説明が難しい… 4

日記 by okky

UNIXシェルスクリプトコマンドブック

のpage9から16には、sh 系最大の謎についての説明が書かれている。そう。あの、

[%1] command1 > file 2>&1

[%2] command1 2>&1 | command2

の謎だ。

%1 の方は 2>&1 はリダイレクトの「後」にくる。%2 の場合は 2>&1 はリダイレクトの前。なーんでだ?! というあれ。

えー、はっきり言って。初心者を混乱させるために書いているとしか思えん。
「m>&n: ファイルディスクリプタm番の内容をファイルディスクリプタn番にコピーする (p.9)」
「「2>&1」は、ファイルディスクリプタ1番の内容をファイルディスクリプタ2番にコピーするという意味です。すなわち、標準エラー出力を標準出力と同じにするということです。(p.15)」

ファイルディスクリプタの「内容」って…C を使い、システムコールプログラミングを日常的に行っている人ならばともかく、『これからunixに慣れようか』という人たちに「ファイルディスクリプタの内容のコピー」とか言ってもわからん。

ましてや p.15 の説明の2行目「stderr と stdout を『同じにする』」ならそれ以降片方をリダイレクトしたら両方リダイレクトする、と考えるのが自然じゃないか。なんで

[%3] command1 2>&1 > file

と %1 が違う結果になるのか、判らないじゃないか。
p.16 の図もこの混乱に拍車をかけるだけだ。

.

というわけで、じゃぁどういうことなのか、システムコールプログラミングをしたことがない人たちでも判るように書くと、どう書けばいいのか。ちょっと考えてみた。いや、眠気覚ましに。

真実とは80%ぐらい似ていればいいとする。100%同じにすると先にファイルディスクリプタの意味を説明しなくちゃいけない。「シェルを使えるようになってからシステムコールとかもできるようになろう」という順序に反するので20%分の誤差は許してもらう。

まず、command1 は通常 標準入力(0)、標準出力(1)、標準エラー(2)という3つの「口」を持っている。これは水道管のようなもので、「管」はあるけれど、そのままではどこにも繋がっていない。ちなみに、0はcommand1にデータを「入れる」口。1,2 は「出てくる」口だ。上水と下水みたいなもん。2の方がババッちい。

[%0] command1

のようにシェル上でcommand1を実行すると、実際には

[0>] command1 0< /dev/tty 1> /dev/tty 2> /dev/tty

という処理が行われる。/dev/tty は「今あなたがシェルを叩いているそのコンソール」だ。

0< /dev/tty

は/dev/tty (多分、あなたの叩いているキーボードに繋がっている) からの入力を「管」として標準入力0への入力にしろ、1> /dev/tty は標準出力1から出てきたデータをそのまま /dev/tty (多分、あなたの見ているターミナルエミュレーターに繋がっている)に送り出せ(そうすれば自動的に画面に出してくれるから)ということだ。

[%1] command1 > file 2>&1

は展開するとこうなる。

[1>] command1 0< /dev/tty 1> /dev/tty 2> /dev/tty 1> file 2>&1

これを左から順番に見ていく。デフォルトまではいいとして

[1>] command 0< /dev/tty 1> /dev/tty 2> /dev/tty 1> file

とこの段階で、1 を/dev/tty に向けていたのを file に向けなおす、という事になる。つまり

[1>] command 0< /dev/tty 1> file 2> /dev/tty

と同じ。で、「2>&1」だが、これは「1番の出力先(この場合は file)を2番の出力先にもしなさい」と言うこと。つまり

[1>] command1 0< /dev/tty 1> /dev/tty 2> /dev/tty 1> file 2>&1

[1>] command 0< /dev/tty 1> file 2> file

という事になる。

[%3] command1 2>&1 > file

の場合、まず最初に 2>&1 が実行されるので2の出力先が1の出力先と同じになる。が、この場合まだ /dev/tty なので

[%3] command1 2>&1

というここまでだと

[3>] command1 0< /dev/tty 1> /dev/tty 2>/dev/tty

は同じ意味になる。で、その後で『> file』を処理するので

[%3] command1 2>&1 > file

[3>] command1 0< /dev/tty 1> file 2>/dev/tty

となってしまうわけだ。

.......................

ここまではいいのだが、そうすると1つ問題が出る。

[%2] command1 2>&1 | command2

これはなぜ 『2>&1』をしないと、標準出力だけが command2 に行って標準エラーは /dev/tty のままなのか。『2>&1』をすると両方とも command2 に行ってくれるのか。うーむこれはどうやって説明するといいかなぁ。

.

もちろん、実体としては

( command1 2>&1 | command2 )
- 0 = open /dev/tty
- 1 = open /dev/tty
- 2 = open /dev/tty
- close 2
- dup 1 ( 2 = dup 1 と書かせてくれ)
- 1,2 を close せずに fork/exec して command1,command2 を作る

(command1 > file 2>&1 )
- 0 = open /dev/tty
- 1 = open /dev/tty
- 2 = open /dev/tty
- close 1
- open "file" ( 1 = open "file" と書かせてくれ )
- close 2
- 2 = dup 1

(command1 2>&1 >file )
- 0 = open /dev/tty
- 1 = open /dev/tty
- 2 = open /dev/tty
- close 2
- 2 = dup 1
- close 1
- open "file" ( 1 = open "file" と書かせてくれ )

の違いなのだが…これをファイルディスクリプタの概念無しで説明するのは意外と難しいなぁ。うーむ、うーむ。

というわけで、ここで止まってしまった。
何かいい説明のしかたは無いかしら。

もちろん「混乱を招くぐらいならば説明しないほうがまし」な方向で。

この議論は、okky (2487)によって ログインユーザだけとして作成されたが、今となっては 新たにコメントを付けることはできません。
  • by stehan (37041) on 2008年11月25日 22時08分 (#1461812) 日記
    okky さんこんばんは。
    linux 初心者の stehan といいます。はじめまして。
    たまたまこの週末にこんなページ [q-e-d.net]を見つけておりまして、自分には分かりやすかったです。

    でも正直、これをシェルでどう書くかなんて、必要なら嫌でも身につく知識だし、
    必要でなければ積極的に覚える必要もまた無いだろうと思いました。
    単にそういう記述が可能であるとだけ分かっていて、
    必要なときに調べることができれば、それで良いかな、と。

    • 「シェルでかければよい」だけでその先に進む必要が無いならば、おっしゃるとおりです。

      しかし、知識は「量」ではなく「本質」です。2>&1 が「本当に意味していることは何か」(中で何をやっているのか)を理解すれば、それらを足がかりに「その先」に進むことができます。また、どのような環境で同じことができるのか、なぜできるのか、どういう環境ではできないのか、等も理解できるようになります。

      このように「先の存在する」形でものを理解するか、何も考えずに呪文のごとく暗記するか、その差が『先々で新しいことを習得する際の体力の差』となって現れます。また、丸暗記しているとそのような差があること自体気がつかない。

      私は人に物を教える場合、絶対「丸暗記をさせない」事にしています。私が物を教えるのは大抵後輩ですので、彼らがより優秀になってくれれば、その分将来私が楽をできますからね。その楽というのには「本質を理解していれば絶対に発生しない間違い」を彼らが犯したせいで、お客様の所に謝罪しに行かなくてはいけないなどのオーバーヘッドから、デバッグサイクルが余計に回ったせいで製品リリースまで時間がかかるようになるケースから…全部に一度に一発で、対処できるわけです。

      .

      しかし、「本質」はどうしても「学習曲線が急峻になる」という弱点を持っています。だからどうしても「足がかり」というか「ちょっとだけ寄り道」というか…そういう物を作ってあげたほうが良い。ここでの悩みとはつまり、そういう「足がかり」をどう作ると良いか、という問題なんですよ。

      そういうポイントを押さえておくことで、向こう10年20年の「楽さ」が違うんです。
      --
      fjの教祖様
      親コメント
      • だとすると、最初に長々と書かれていた説明は意図が不明瞭だったように思いますね。
        sh系最大の謎、なんて書かれていたものですから、
        ・入出力とは何か
        ・リダイレクトとはどういう概念か
        ・コードではどう書くか
        ・シェルの記法ではどう表現するか
        などの要素に分解した場合の最後の部分のみを問題にしているのだと思っていました。
        最初の2つについて充分理解できているのなら、残りは苦しむような問題ではないと思います。
        実際に私にとっては、リンクした先の説明が間違ってなければ、実に単純な話でしたし。

        逆に、最初の2つが分からないうちは、後の2つは公式の丸暗記にしかならないでしょう。
        かといって最初の2つを学ぶための例題とするには、「sh系の謎」は荷が重過ぎると思います。
        むしろ、本質を学んだ後の、仕上げや確認のために学ぶくらいで良いのではないかと思います。

        本質とは okky さんが書かれたように、入出力とは管であるということでしょう。
        小さなプログラムを管で結ぶことによって、大きな仕事をこなせること、
        デバイスまでもが管を介することで、柔軟に何でもつなげられるということ、
        これこそが unix の本質ですよね。
        それをある特定のシェルで具体的にどう記述するかなど、後回しで良い問題のはずです。
        hello world を書かせるときも、#include<stdio.h> の説明は後回しです。
        それこそ、実際に書く必要、読む必要に迫られてからでも遅くはないとすら思います。

        「謎」そのものは、分かってしまえば実に単純な話でした。
        要するに m>&n の &n は n ではなくて「nがいまつながってる先」だったということですよね。
        (ここ間違ってると私の一生の恥なので遠慮なくしばき倒してもらえると幸いです)

        でもそれを理解するためには、前提となる様々な知識が必要です。例えば
        プログラムには0:標準入力、1:標準出力、2:標準エラー、3本の管が標準装備されている。
        管には一本一本に必ず口が2つある。
        3本の管それぞれの口の片側はプログラムが握っていて動かしようがない。
        一方その反対側、外側の口をそれぞれどこかにつなぐのはシェルの仕事。
        デフォルトでキーボードとスクリーンにつなげてくれている。
        など。
        これらはシェルの記法とは独立した知識です。
        そして、これらの方が「謎」よりもずっと本質的な知識だと思うのです。

        それこそ、リダイレクションの記述の秘密をうまく説明する方法を考えるより、
        うまく理解するために必要な知識を先に学ばせることを考えた方が、
        実は近道で、あとあともつぶしが効くんじゃないかとすら思います。
        親コメント
        • う~む。何も知らない人は、その説明だと誤解するでしょう
          逆の言い方をすると、その説明の仕方をする人は自分が矛盾した情報を見ていることに気がついていない可能性がある。
          そのことを理解してもらうためにも、あの長い説明は必要なのですよ。

          stehanさんの説明だと

          % command1 2>&1 | command2

          のときに何が起こっているのか、その説明が判らないままです。そして、初心者は必ずここで混乱する。
          だって「管」は1入力1出力でしょう?

          .

          % command

          だけの段階で1 も 2 も /dev/tty に繋がっています

          2>&1 をやっても1も2も /dev/tty に繋がっている事に変わりはありません。なおかつ、管には1つの入り口と1つの出口しかないのですから、単純に管として考えた場合、1番が繋がっている先の /dev/tty と 2番が繋がっている先の /dev/tty は2>&1 をやろうがやるまいが関係なく同じ「仮想ファイル」である事に違いはありません。

          その後の | …つまり 「1|」で変更できるのは「stdout の出口」だけのはずです。実際

          % command1 | command2

          とやると 2> /dev/tty な状態は変わらない。なのに、2>&1 をやるとその次の | で1だけでなく、2が繋がっているはずの先まで一緒になって変化するのはな~ぜだ?! というのがここの問題なわけです。

          .

          あえて「管」の説明に拘泥するならば。

          実は 2>&1 は「出口を」いじっているのではありません。入り口をいじっているのでもない。これは「管を」いじっているのです。

          unixの「パイプ」は1入力1出力ではありません。この管は「入力1出力」なんです。ただし、デフォルトは1入力。2以上にしても難しいだけだから、デフォルトは1。

          2>&1 は「今まで2に繋がっていた管を外せ。そして1が繋がっている管の入力側を1つ増やして、それを2につなげ」と言うことなんです。

          だから

          % command1 2>&1 | command2

          において、最初 1 も 2 も /dev/tty に繋がっていて、2>&1 後もやはり「1の出口も2の出口も /dev/tty に繋がっているのは変わらない」のですが、2>&1 をやる前は別々の管で繋がってたものが、2>&1 をやることで1と2の出口を両方とも同じ管で /dev/tty に繋がった事になる。確かに「それぞれが繋がっている先」だけを考えると 2>&1 をやってもやらなくても同じに見えるんだけれど、使っている管を共有しているか否かが違う

          .

          じゃぁ

          % command 2>&1 > file

          だとなぜ stdout しか file に出て行かないのか?

          リダイレクションは必ず「出口側を引っぺがす」からです。

          2>&1 をやった直後、command からの出口 1 と 2 は「同じ管」を通して /dev/tty に繋がっています。 『1> file』をやると、

          - まず1番の出口と管を切り離します (この段階で 2だけが /dev/tty と繋がっている)
          - 次に1番の出口に新しい管をつなぎ、その先に file をつなぎます

          すると、2は /dev/tty を向いたまま、1はfileを向く事になる。

          .....

          あぁ、なるほど。説明的にはそう説明すればいいのか。

          この説明で混乱しないぐらい時間をかけるとして、どれぐらい時間間隔をあけるのか、と言うのが問題だな。
          --
          fjの教祖様
          親コメント
typodupeerror

人生の大半の問題はスルー力で解決する -- スルー力研究専門家

読み込み中...