"Bad fd number"について詳細に調査してみた
色々あって急に空き時間が出来たので、
"Bad fd number"の検索結果で訪れてくる方が以外に多い事もあって
折角なのでこの問題を詳細に調査してみることにしました。
さて、まずはこのメッセージを出しているのが一体誰なのかを確認してみましょう。
エラーがエラーだし、リダイレクトする時点でエラーを吐いてる気がするので
多分/bin/shか、普段使っている/usr/local/bin/bash辺りではないかと思うのですが……
# cd /usr/src # grep -R "Bad fd number" * bin/sh/parser.c: synerror("Bad fd number"); # cd /usr/ports/shells/bash (ソースコードがなかったので一端拾ってくる) # make # cd work/bash-4.0 # grep -R "Bad fd number" * #
ふーむ……やっぱり/bin/shが吐いてたエラーのようですね。
それじゃあどの辺でエラーを吐いているかトレースしていくことにします。
幸運にもshにはデバッグモードに応じてトレースログを出力する機能があるようなので、
これをそのまま借りてくることにします。
/usr/src/bin/shの中身を以下のように変更して……
diff -aur sh/shell.h sh.tmp/shell.h --- sh/shell.h 2009-04-15 12:14:26.000000000 +0900 +++ sh.tmp/shell.h 2009-09-19 00:10:14.000000000 +0900 @@ -50,7 +50,7 @@ #define JOBS 1 -/* #define DEBUG 1 */ +#define DEBUG 2 /* * Type of used arithmetics. SUSv3 requires us to have at least signed long. diff -aur sh/show.c sh.tmp/show.c --- sh/show.c 2009-04-15 12:14:26.000000000 +0900 +++ sh.tmp/show.c 2009-09-19 00:06:34.000000000 +0900 @@ -394,7 +394,7 @@ strcat(s, "/trace"); } #else - scopy("./trace", s); + scopy("/tmp/trace", s); #endif /* not_this_way */ if ((tracefile = fopen(s, "a")) == NULL) { fprintf(stderr, "Can't open %s: %s\n", s, strerror(errno));
さらに
# cd /usr/src/bin/sh # make # make install
……でインストールします。ちなみにshow.cの方の変更は必須ではありませんが、
トレースログが取られる位置として/usr/src/bin/sh/TOURに書いてあった
"$HOME/trace"をファイルパスとして指定する方の処理に進まなかったので、
面倒くさくなって/tmpに突っ込むことにしてます。
さて、これで/bin/shをシェルとして使った時にトレースログが取れるようになりました。
早速前回問題になったコマンドを実行してみましょう。
ちなみに実行するコマンドは下記の通りです。
# perl -e "system('/bin/ls >& /dev/null');"
これによって取得できたトレースログはこちら。
Tracing started. Shell args: "sh" "-c" "/bin/ls >& /dev/null" token word /bin/ls pipeline: entered reread token word /bin/ls reread token word /bin/ls reread token word /bin/ls reread token word /bin/ls reread token word /bin/ls reread token word /bin/ls token redirection token word /dev/null Fix redir /dev/null 0 token end of file reread token end of file reread token end of file reread token end of file evaltree(0x2820524c: 1) called evalcommand(0x2820524c, 0) called Fix redir /dev/null 1 exverror(1, NULL) pid=73635 exitshell(2) pid=73635
これで材料が揃ったのでいよいよソースコードを探索していきます。
まずはエラーメッセージ"Bad fd number"を出力している近辺で何が発生しているのか見てみます。
先ほどgrepで検索した結果、parser.c内で件のメッセージを出力している事が解ったので
該当行近辺を見ることにしてみましょう……
void fixredir(union node *n, const char *text, int err) { TRACE(("Fix redir %s %d\n", text, err)); if (!err) n->ndup.vname = NULL; if (is_digit(text[0]) && text[1] == '\0') n->ndup.dupfd = digit_val(text[0]); else if (text[0] == '-' && text[1] == '\0') n->ndup.dupfd = -1; else { if (err) synerror("Bad fd number"); else n->ndup.vname = makename(); } }
この関数の中でエラーを吐き出しているようですね。
トレースログの中にあった"Fix redir /dev/null 1"はこの関数の中で出力している事が解ります。
shにおけるファイルのリダイレクトは実を言うと書式に従ってdup2()を実行しているだけなので、
詰まるところロジック的に正しいファイルディスクリプタ(1桁の数値)が取得できないので
"Bad fd number"と言われているようです。
そう考えて見るととても的を射ているエラーメッセージですね。
ちなみに実際にdup2()を呼び出しているのはredir.cの中です。
STATIC void openredirect(union node *redir, char memory[10]) { /* 筆者注:記事短縮のため省略 */ case NTOFD: case NFROMFD: if (redir->ndup.dupfd >= 0) { /* if not ">&-" */ if (memory[redir->ndup.dupfd]) memory[fd] = 1; else dup2(redir->ndup.dupfd, fd); /* 筆者注: fixredir()で取得したdupfdを使っている */ } else { close(fd); } break; /* 筆者注:後略 */
ということは、これは単なるコマンド書式ミス?
もしかしてbashとかshではこういう風に標準出力・標準エラー出力を
両方ともリダイレクトするんじゃなかったっけ?
こういう時にはいつもお世話になっているgoogle先生に伺ってみましょう。
●csh・tcsh の場合
標準出力と標準エラー出力を両方リダイレクトしたい場合は、
- % command >& file.txt
- % command |& command2
とする。
--------------------- 筆者注:中略 ----------------------
●sh・bash の場合
まず、
- 標準出力は 1 番
- 標準エラー出力は 2 番
ということを覚えてほしい (ちなみに標準入力は 0 番)。
標準出力を file1 に、標準エラー出力を file2 に、などと振り分けるには、
- % command 1>file1 2>file2
とする。
- % command 1>/dev/null
- % command 2>/dev/null
と、どちらかだけを表示することも簡単に指定できる。上記の find の例だと、
- % find / 2>/dev/null
とすればよい。
標準出力と標準エラー出力を両方まとめて他のコマンドに渡すには
- % command 2>&1 | less
とし、標準出力と標準エラー出力をまとめて file に書き出す場合は
- % command >file 2>&1
とする。ここで順番を逆にして
- % command 2>&1 >file (誤り!)
としてはうまくいかないことに注意。
http://x68000.q-e-d.net/~68user/unix/pickup?%A5%EA%A5%C0%A5%A4%A5%EC%A5%AF%A5%C8
……えーと、つまるところこういう事でした。
Perlはsystemを使う時に/bin/shを呼び出しているし& /dev/null"'という行がある">*1、シェル上で'/bin/ls >& /dev/null'を実行した時に使ったシェルはbashでした。
つまりcshとsh・bashの使い方を混同していたために発生しているエラーメッセージのようです。
というわけで、上記引用元の情報に基づいてコマンドを修正して実行してみます。
# perl -e 'system("/bin/ls > /dev/null 2>&1"); print "$?\n";' 0
エラーもなく終了しました。気合いを入れてトレースログまで取って探索した割にはしょうもない理由……
*1:トレースログ参照。'Shell args: "sh" "-c" "/bin/ls >& /dev/null"'という行がある