"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先生に伺ってみましょう。

cshtcsh の場合
標準出力と標準エラー出力を両方リダイレクトしたい場合は、

  • % command >& file.txt
  • % command |& command2

とする。

--------------------- 筆者注:中略 ----------------------

●sh・bash の場合
まず、

ということを覚えてほしい (ちなみに標準入力は 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"'という行がある