okkyの日記: リダイレクションの説明が難しい例 21
日記 by
okky
リダイレクションの説明が難しい… で教えてもらったURL:
http://x68000.q-e-d.net/~68user/unix/pickup?%A5%EA%A5%C0%A5%A4%A5%EC%A5%AF%A5%C8
ですが。
ここにある説明だと次のような例について説明がつきません。詳解してくれたstehanさんは、このサイトの説明とは「無関係に」動作原理はわかっているようなのですが、逆にそのせいで初心者が何で引っかかるのか判らないようなので。そこらへんを強調する例を:
.
次の2つの例を考えてください。
例1) command1 2>&1 > file
例2) command1 2>&1 | command2
このとき、command1 の stdout と stderr はそれぞれどこに繋がっているのかを考えます。
例1) command1 2>&1 > file
- stdout: file
stderr: /dev/tty 例2) command1 2>&1 | command2
- stdout: comamnd2 の stdin
stderr: command2 の stdin
質問: 例1の場合、stdout とstderr は「異なる対象」に接続しました。しかし、例2 の場合 stdout と stderr は「同じ対象」に接続しました。例1と例2の違いは >file か |command2 かの違いだけです。なのにこのような違いが発生した、その理由を説明しなさい。ただし:
条件1) 「そういうもんだから」は無し。
条件2) 内部構造を説明するのも、なし。
条件3) 本当の動作を厳密に説明する必要は無い。
条件4) 初心者が納得するような説明にしなさい。例などを使うのは当然、あり。多少の嘘も許そう。
事象をおいかけるだけではダメでしょうか。 (スコア:1)
2>&1
stderr の出力を stdout が示す出力へ切り替えた
stderr: /dev/tty
stdout: /dev/tty
> file
stdout の出力を file へ切り替えた
stderr: /dev/tty
stdout: ./file
例2) command1 2>&1 | command2
stderr の出力を stdout が示す出力へ切り替えた
stderr: /dev/tty
stdout: /dev/tty
| command2
command2 の stdin を command1 の stdout に切り替えた
Re:事象をおいかけるだけではダメでしょうか。 (スコア:1)
stdout: command1 のstdin
stderr: /dev/tty
にならないのはなぜ??!」
となりますよ?だって、stderr はあくまでも「stdout と同じ…って事は /dev/tty に」切り替わったんだよね??
と合致しません。
.
この問題を解くには、事象を「追う」のでは駄目です。パイプやリダイレクションを判りやすいモデルにマッピングして説明する必要があります。
「事象を追いかけられる」って事は頭の中にモデルがあるって事ですが、初心者にはそのモデルが無いのです。モデルを「作ってあげなくてはいけない」。ここを理解しないと「初心者がなぜはまるのか判らない」という状態に陥ります。そのモデルの説明からスタートしましょう。
fjの教祖様
Re:事象をおいかけるだけではダメでしょうか。 (スコア:1)
| command2
(command1とcommand2とを繋ぐpipeを生成し、command2のstdinをpipeの出口から読むsub shellと、command1のstdoutをpipeの入口に書くsub shellと、を起動。)
で、次いで
2>&1
(command1のsub shellで、stderrの出力をstdoutへ切り替える。)
では?
Re:事象をおいかけるだけではダメでしょうか。 (スコア:1)
まぁ、その辺はうそが入っていても構わない(判りやすければ)というルールを適用するとして。
おもわず実験してしまいましたよ。 で、strace-num.log を見てみました。
# 行番号が無いと、前後関係がわからなくなるので nl で番号を打ってから読むことにした。
おー。
ややこしいところを全部除去すると
1) まず、pipe(3, 4)
2) 次に fork (Linux上だったので、実際には clone ) 2連発。片方を Process 1 (P1)、もう一方を Process 2 (P2) としよう。
3) P1: dup2 (4, 1), close(4) これで上記 pipe の一方が 1 に
4) P1: dup2 (1, 2)
5) P1: cat になれ~~
6) P2: dup2 (3, 0), close(3) これで上記 pipe のもう一方が0に
7) P2: tee になれ~~~
と言うわけで、先に | してから 2>&1 ですね。
fjの教祖様
Re:事象をおいかけるだけではダメでしょうか。 (スコア:1)
…
と書いてあるので、「多少の嘘」もつかずに「先ずパイプ、次いで左側からリダイレクト」と叩き込むのが無難と思います。
# 未公開でレビュー中ならば「パイプ及びリダイレクトは右側から」仕様に直すのが良いのでせうが、今更は無理でせう。
Re:事象をおいかけるだけではダメでしょうか。 (スコア:1)
すごーく乱暴に言うと「モノの本の方が間違ってる」わけですな。
コマンド同士の区切りは「;と|」になっていて「;の場合はそこで終わり」「|の場合は左の1を右の0につなぐ」。
これが最初にパースされる内容だと。
うーむ。前フリ説明が必要だなぁ。
fjの教祖様
Re:事象をおいかけるだけではダメでしょうか。 (スコア:1)
という部分がくだらない、と感じた理由なんですよね、失礼を承知でいうと。
そのような順序で解釈される、と規定されているのなら「そういうもの」とする以外になく、その上で
そういうものとなるよう設計された理由は何かという話から説明するべきでしょうし、
そうでなければシェルの一実装におけるローカルルールをそこまで追求する価値って何なんでしょうと。
もっとも、仮に本当にローカルルールだったとしても、
もはや今さら避けては通れないスタンダードとなってしまっているのかもしれませんが。
もうひとつ、先週は分かったつもりになっていたけど、
いろいろ試すとやはり怪しい理解しかできていなかったことに気がつきました。
ごく簡単なことだ、などと生意気なことを書いてすみませんでした。
Re:事象をおいかけるだけではダメでしょうか。 (スコア:1)
若い証拠ですな。いい意味で、ですが。
全てのソフトウェアは、まず「仕様」があり、ついでそれに矛盾しない範囲での「実装」があります。
この逆の順序はありえませんし、あってはいけません。
なぜなら、使う人は「どうやって実装されているのか」ではなく「どういう仕様になっているのか」に従って使っているからです。逆にその仕様の範囲内であれば、どのように実装しても構わない。
しかし sh のように、あるコミュニティの黎明期に作られたものは「実装」が先にあってそれに合わせて「仕様」を作っていきます。これはもう defacto standard の悪い一面としか言いようが無い。
問題は「実装から仕様を作る」時に、正確に仕様化できなかった時に生じます。仕様に実装を合わせられればいいんですが、POSIXのように「全体を見ると自己矛盾している」場合はどうやっても実装は仕様に合致しません。その場合は、『このソフトではこういう仕様にします』と言うのを先に作らなくてはいけない。仕様の段階で自己矛盾がない事を確認しなくてはいけない。
.
でないと何がバグで何はバグではないのかを知る術がなくなります。利用者も、作る側も。
利用者のその場その場の要求に従っていると、ソフトウェアは見る見るうちにカオスになっていきます。
「今日は右から左にパースして欲しい。」
「明日は左から右にパースして欲しい。」
「午前中は & を無視して欲しい。」
という要求は(若いうちは信じられないかもしれませんが)当たり前のように発生します。なにしろ Unix の重鎮とその道のプロが集まって作ったはずの POSIX が『80%以上準拠する事は絶対不可能な自己矛盾の塊』になるぐらいですから。
なので、「なにが仕様なのか」、「何は実装に過ぎないのか」、「何はバグなのか」、「何はバグではないのか」は慎重に見極める癖をつけなくてはいけません。また、人に教えるときもこれらをきちんと踏まえなくてはいけません。その上で、「仕様だけで自己矛盾を起こしていない」事を確認する能力を養わなくてはいけない。それが出来ない人はプログラマではなく、ただのコーダーです。
.
ちなみに。この辺を深く考えなかった有名人に、「z80を開発した 嶋正利さん」がいます。
彼はプロセッサのデザインもしましたが、その上で動くプログラムも一杯書いています。
が、彼のコードはその殆どが z80の高速版では動きませんでした。
彼は z80 の仕様書をちゃんと書いていたのですが、彼のソフトはその仕様書を無視して「電気回路的に考えて何が起こるのか」を元にビットパターンを並べたものだったからです。z80を高速化した際、回路が何箇所も最適化されたのですが、当然その結果として「仕様外」だった彼のコードはことごとく動作しなかった。ザイログ社はこの書き直しにえらい手間を食ったといいます。当人も何がどうなっているのかよく判らない部分がたくさんあったそうですし。
.
歳を取ると、若かったときに「3年ぐらいでなくなるんじゃないか」と思い込んでいたコードやスクリプトがいまだに生きている事を知らされて愕然とする事がよくあります。『やべっ、あの辺りコンパイラ依存にしてるよ』的な心当たりが秘かに増えるわけです (^^;)。
なので、仕様と実装は分けるとか、可能な限り「仕様に準拠した形で」説明をするとか、そういうことに口うるさくなるのです。逆にここが判っているかどうか、がプログラムデザイナーとしての能力を測る際の、私が重きを置いている基準の1つです(つーか最重要視しています)。
fjの教祖様
管に名前をつけて説明 (スコア:1)
---------------------------------------
○前提
1. コマンドのデータ出口(であるstdoutとstderr)は"管"で出力装置とつながっており、stdout/stderr -> 出力装置の方向にデータが流れます。
2. コマンドのデータ出力は1つの管しかつなげることができません。
3. "管"は入口の数を増やして、複数のものをつなげることができます。
○初期状態
1. command の1番出口(stdout) は "stdout管"という管を使用して
/dev/tty につながっています。
2. command の2番出口(stderr) は "stderr管"という管を使用して
/dev/tty につながっています。
○例1) command1 2>&1 > file の場合
1. "2>&1" を実行すると、command1 の 2番出口(stderr)は、1番出口(stdout)
が使用している"stdout管"を使用します。
具体的には、"stdout管"の入口を1つ増やして
2つにして、2番出口(stderr)を"stdout管"の入口につなぎます。
2. "> file"を実行すると、command1 の 1番出口(stdout)から file に向かって、
新しく管("file管"と命名)が作られます。
"stdout管"の入口はcommand1 の2番出口(stderr)がつながっているのみとなります。
3. したがって、command1 の 1番出口(stdout)は"file管"を通して、 file につながり、
2番出口(stderr)は"stdout管"を通して、 /dev/tty につながることになります。
○例2) command1 2>&1 | command2 の場合
1. "2>&1" を実行すると、command1 の 2番出口(stderr)は、1番出口(stdout)
が使用している"stdout管"を使用します。
具体的には、"stdout管"の入口を1つ増やして
2つにして、2番出口(stderr)を"stdout管"の入口につなぎます。
2. "| command2"を実行すると、command1 の "stdout管"の出口をcommand2 の0番入口(stdin)につなぎます。
3. したがって、command1 の1番出口(stdout)と2番出口(stderr)は"stdout管"を通して、
command2の0番入口(stdin) につながることになります。
Re:管に名前をつけて説明 (スコア:1)
誤
2. コマンドのデータ出力は1つの管しかつなげることができません。
正
2. コマンドのそれぞれのデータの出口は1つの管しかつなげることができません。
Re:管に名前をつけて説明 (スコア:1)
○例3) command1 > file 2>&1 の場合
1. "> file"を実行すると、command1 の 1番出口(stdout)から file に向かって、
新しく管("file管"と命名)が作られます。
2. "2>&1" を実行すると、command1 の 2番出口(stderr)は、1番出口(stdout)
が使用している"file管"を使用します。
具体的には、"file管"の入口を1つ増やして
2つにして、2番出口(stderr)を"file管"の入口につなぎます。
3. したがって、command1 の 1番出口(stdout)と2番出口(stderr)は"file管"を通して、 file につながることになります。
Re:管に名前をつけて説明 (スコア:1)
を追加したほうがよいかと(今回の例では不要でもどうにかなりますが)。
やはりこれに収斂するんでしょうね。
『> は管の「入口」を、 | は管の「出口」を操作しているのだ』
と「管」に対する操作を中心とした説明にしないと、説明が難しい。
fjの教祖様
Re:管に名前をつけて説明 (スコア:1)
書いていて思ったのですが、この説明を聞いた初心者が、
エラー出力だけをパイプで別プロセスに流そうとして、
command1 2| command2
と書いて失敗してなぜなんだろうと思いそうです。
#実際には command1 2>&1 > $SSH_TTY | command2 のように書かないといけない?
結局、最後は「そういうもの」になりますが... (スコア:1)
仮に自分がホワイトボードの前に居て、目の前にいるのが弊社の1~3年坊主だとすると、こういう説明をすると思います (要求されているものとは違うかもしれませんが)。
ルール
stderr => stderr の出力先 (初期値は別の端末)
右 stdin <= 元々の入力元 (初期値は端末(入力))
↓
右 stdin <= パイプ
↓
stdout =>
元々の出力先新しい出力先stderr => 元々の stderr の出力先 (初期値は別の端末)
↓
stdout => 元々の stdout の出力先
stderr => 元々の stdout の出力先 (複製)
stderr => 元々の stderr の出力先 (初期値は別の端末)
↓
stdout => 元々の stderr の出力先 (複製)
stderr => 元々の stderr の出力先
まずはこれ↑を踏まえて戴いて、
この例では、
よって、command1 の標準出力は file へ、command1 のエラーメッセージは端末へ出力される。
一方、
この例では、
よって、command1 の標準出力、エラーメッセージ共に command2 へ送られる。
パイプをリダイレクションよりも先に評価するのがポイントですね。
後は上述のルールを元に、
の動作がどう違うか自力で説明せよ、という課題を出すと思います。 結局、口やら図やらでどう説明しようが、自力で演習させないと分かってくれないことが多いので。
Re:結局、最後は「そういうもの」になりますが... (スコア:1)
これは現象としては正しいのですが、説明としては良くない、と私は思っています。
というのは、シェルに関するものの本はほぼ全て「行は全て左から右に評価される」とあるからです。
もちろん、「評価する順序」と「その結果を実装するために処理が行われる順序」は違って構わないのですが、少なくとも評価する順序として与えられた、その通りの順序で評価して、最終結果がどうなるのかを説明できないのでは、教わる側は混乱すると思います。
fjの教祖様
こんな風に考えてみました。 (スコア:1)
ファイルディスクリプションを箱に、
openを、箱を作り、パイプをつなぎかえる動作に、
dup2を、(箱を作らずに)パイプを別のパイプと同じ箱につなぎかえる動作に対応させているつもりです。
closeまわりは適当です。
0 - [ /dev/tty ]
1 - [ /dev/tty ]
2 - [ /dev/tty ]
> file で、fileの箱を新しく作り、パイプを新しく作った箱につなぎ変えます
0 - [ /dev/tty ]
[ /dev/tty ]
2 - [ /dev/tty ]
1 - [ file ]
2>&1 は、2を1と同じ箱につなぎ変えます。
リダイレクトがm>&nの場合、mのパイプはnのパイプと全く同じ箱と接続されます。
(このとき、新しい箱は作られません)
0 - [ /dev/tty ]
[ /dev/tty ]
[ /dev/tty ]
___________
1 - | file |
2 - |__________|
誰もつないでいない箱を消して整理すると
0 - [ /dev/tty ]
___________
1 - | file |
2 - |__________|
のようになります。
2>&1 で2を1と同じ箱につなぎ
0 - [ /dev/tty ]
___________
1 - | /dev/tty |
2 - |__________|
[ /dev/tty ]
> file で、新しくfileの箱を作り、1をそちらにつなぎ替え、
0 - [ /dev/tty ]
2 - [ /dev/tty ]
[ /dev/tty ]
1 - [ file ]
整理して
0 - [ /dev/tty ]
1 - [ file ]
2 - [ /dev/tty ]
となります。
command1の0 - [ /dev/tty ]
command1の1 - [ /dev/tty ] - command2の0
command1の2 - [ /dev/tty ]
command1の0 - [ /dev/tty ]
___________
command1の1 - | /dev/tty |
command1の2 - |__________|
[ /dev/tty ]
のように接続されていることになります。
1をcommand2の0につなぎ、整理すると
command1の0 - [ /dev/tty ]
___________
command1の1 - | /dev/tty | - command2の0
command1の2 - |__________|
となります。
1を聞いて0を知れ!
Re:こんな風に考えてみました。 (スコア:1)
単語そのものすら間違っているのに、概念を理解できているとは到底思えませんので、勉強しなおしてきます。
1を聞いて0を知れ!
Re:こんな風に考えてみました。 (スコア:1)
一つ問題が。
この最後の例の最後。/dev/tty には command1 の 1 と 2 の出力内容が「見える」様な気がする…。
全体として「箱」の中身が同じなのに「1つ」でつながったり「2つ」で繋がったりできるのはなぜ??しかも同じ内容を持った「箱」が何個もあるように見えるんだけれど…という辺りについて、前提説明をしっかりする必要がありますね。
一つのファイルを複数のプログラムが同時に操作する、と言うことはありえる状態ですので、そちらのほうの説明からパイプ/名前付きパイプへと話を展開していく、という順序を取ると説明しやすい気がします。
fjの教祖様
Re:こんな風に考えてみました。 (スコア:1)
どうすりゃいいか考えてみたら、
command1 2>&1 >/dev/null | command2
の説明がおかしくなりますので。
eiさんが言っているように、|を先につないでしまった方がよさそうですね。
その場合、
command1 | command2
は、
command1の0 - [ /dev/tty ]
command1の1 - command2の0
command1の2 - [ /dev/tty ]
となります。
これだと、見えちゃうこともないかと。
さらに、command2の0と箱に入ったファイルは同列に扱えると気づいてくれたりすると、話の展開もしやすそうです。
同じ箱が複数個あることや、つなぐ数が変えられちゃうことに疑問を持ってくれる人なら、きっと気づいてくれるでしょう。
1を聞いて0を知れ!
右だとか左だとかいいだしたのは (スコア:1)
$ 1> file command
具体的には
$ 1> a.txt ls -l
みたいな。
Re:右だとか左だとかいいだしたのは (スコア:1)
とか、この辺の記述の柔軟さは勘弁して欲しいときがありますね。
実際に処理する順番としては fork をまずやって、close 1, 1=open("a.txt") をしてから exec("ls -l") なのはいいんですが、どうして
「1> という名前のコマンドじゃない、と判るのか(ファイルとしてはそういう名前のファイルは作れるし、chmod +x もできるのに)」
とか、いっぱいザワザワしますよね。
この辺のザワザワ感が、csh, sh 論争が宗教論争になる一端だったりしますし、私が最後には csh/tcsh 派である理由だったりします。
# いえ、cshにだってザワザワ感は一杯あるんですが…
.....
とりあえず、最初は教えない。
多分、named pipe を教える辺りで、「名前のあるパイプ」「名前の無いパイプ」ってどういう意味? 辺りから入りなおすときに、| が実際は何をしているのか、という辺りの一環として教えると思います。
fjの教祖様