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

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

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

参照用 記事

シェルのリダイレクトの補遺

昨日書いた「シェルのリダイレクトを『こわいものなし』というくらい完全に理解しよう」に、随分とブックマークやトラックバックをいただきました。それらのフィードバックを拝見して、僕の説明にいたらない所があったと思いますので、ここで補足します。

コマンド実行単位≒simple command

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

シェルの構文(grammar)的概念としてsimple commandというものがあります。ここで言ったコマンド実行単位は、ほぼsimple commandだと言っていいでしょう。「ほぼ」と付けたのは次の理由からです; 「コマンド実行単位」は、1つのプロセスを起動することを念頭においた言葉ですが、simple command は構文的な単位なので、シェルの変数代入や組み込みコマンドも含まれます。

コマンドラインの構文

yoshukiさん、http://saikyoline.jp/weblog/2007/12/post_165.html にて:

そうか、自分は[檜山注:コマンドラインの]バラし方を知らなかったんだ。

コマンドラインの構文はあまり構造化されてないので、バラし方、つまりパージング(構文解析)方法やパーズツリー(パージング結果のデータ構造)が分かりにくいですよね。

おおよそ次のような階層構造だと理解すればいいかと:

  1. 前節のsimple commandが基本単位(リダイレクトはsimple commandの一部です)。
  2. いくつかのsimple commandをパイプ記号「|」でつないだものがパイプライン、単一のsimple commandも特別なパイプラインとみなす。
  3. いくつかのパイプラインを逐次実行記号「;」でつないだものがコマンドライン、単一のパイプライン(単一のsimple commandかもしれない)も特別なコマンドラインとみなす。

ほかに、「&」を添えてバックグラウンド実行、「(」と「)」で囲めばまとめてサブシェルで実行とかでアクセントを付けます。

「&&」と「||」も実行制御に使えますが、これらは、終了ステータスを反転させる「!」と共に、論理演算子とみなすほうが理解しやすいでしょう。つまり、「&&」、「||」、「!」を使うときは、コマンドを真偽値を返す関数のように扱っているのです。commandA && commandB が if (commandA) then commandB else false と同値であることから、「&&」や「||」を条件付き実行のために流用できるのです。

右から左か、左から右か

tohokuaikiさん、http://d.hatena.ne.jp/tohokuaiki/20071221 にて:

んで、次に思ったのは、「左から右って書いてあるのに、パイプは違うじゃん」ってこと。

全体を読んでると、「パイプしてあるって」言うのは「後ろの方に」書かれているのにもかかわらず、「最初に」考慮しなければいけないこととなる。これって、左から右の原則に従ってないよ。

おっしゃるとおり。僕は、「シェルがコマンドラインを右から左に解釈実行するなんてウソ」と書いて、解釈実行は左から右だと強調しました。しかし、どうも強調し過ぎたようです。

シェルは、コマンドラインを左から順に少し読んでは実行し、少し読んでは実行しとやっているわけではありませんコマンドライン全体の構造を把握してから実行します。「|」や「;」で切り分けて、パイプの配管、ファイルディスクリプタの繋ぎ替え(リダイレクト)、サブシェルを含むプロセスの割り当てなどを計画します。

コマンド実行単位内で、右に書いてあるリダイレクト指定を先に考慮し、コマンドそのものをその後で実行する点では、「右から左」とも言えますね。ただし、僕がほんとに強調したかったのは、リダイレクト指定が「左から右」に処理されることです。

ファイルディスクリプタと標準入出力

tohokuaikiさん:

ファイルディスクリプタってなに?って感じだったのだけど、データを出し入れ運ばせられるホースみたいな機能を持ったものを想像した。

はい、その理解でいいと思います。ファイルディスクリプタはデータ(生のバイトや、文字と解釈されたバイト)のストリームの出入り口を表すものです。

また、コマンドにファイル引数があるときはそれが入力に使われるメカニズムですが:

grepは第二引数にきたものをファイルとして認識し、ファイルディスクリプタの0にリダイレクトさせるという処理を最初に行う。

っていう機能があるからだと思う。

ちょっと違います。grepは、「第二引数がなければ標準入力を使い、あればそのファイルを入力とする」だけです。わざわざ「ファイルディスクリプタの0にリダイレクト」はしません。実例で示しましょう; 次はcatの単純版です。


/* kitten.c -- tiny cat */
#include
#include

main(int argc, char** argv)
{
if (argc == 1) {
/* no args */
output(stdin);
} else {
/* some args */
int i;
FILE *fp;
for (i = 1; i < argc; i++) {
if (strcmp(argv[i], "-") == 0) {
fp = stdin;
} else {
fp = fopen(argv[i], "r");
}
if (fp == NULL) {
fprintf(stderr, "kitten: Can not open file %s\n", argv[i]);
continue;
} else {
output(fp);
}
if (fp != stdin)
fclose(fp);
}
}
}
output(FILE *fp)
{
int c;
while ((c = getc(fp)) != EOF) {
putchar(c);
}
}

  1. 引数がない(argc == 1)なら、標準入力(stdin)を処理。
  2. 引数があれば、それらをファイルとみなして順番に処理。
  3. 特別なファイル名"-" は、標準入力を意味する*1

というルールです。


$ echo This is foo.txt | kitten - foo.txt
とすると、This is foo.txt に続けてfoo.txtの内容が表示されます。

標準出力と端末画面

標準出力(stdout)と端末画面(/dev/tty)は概念的に違います。デフォルトでは、標準出力も標準エラー出力も端末画面に繋がっているので混乱しがちです。

標準入出力(ファイルディスクリプタの0, 1, 2番)は、プログラム(実行時はプロセス)から見た概念です。プログラムが、それらの標準入出力が実際にどこに向かっているか(ファイルか端末かなど)は知りませんし、知らなくていいことがすごいメリットなのです。

ですから、


$grep [ prototype.js 2>&1 > hoge
だと、標準出力にエラーがでる

は、やや不正確な表現です。プログラムgrepは、いつだって標準エラー出力にエラーメッセージを書いています。我々がコマンドラインを実行して観測したら、端末画面にエラーメッセージがでたのです。

どうでもいいけど

僕は、「とあるコマンド」を表すためにcommandを使って、command >file 2>&1とか書いてました。bashには、その名もズバリcommandという組み込みコマンドがあります。ですから、commandという綴りをそのまま入力して実行すると、不可解な結果になるかも知れません。

勘違いする人はそういないと思いますが、「シェルのリダイレクトを『こわいものなし』というくらい完全に理解しよう」のcommandをsome-commandに直しました。

*1:それじゃ、ほんとに"-"って名前のファイルがあったらどうすんだ? ってのがクイズ・ネタになったりします。