リダイレクトのエラーの謎4(たぶん解決)

Fri Oct 13 18:37:02 JST 2023 (modified: Fri Oct 13 19:33:14 JST 2023)
views: 2618, keywords:シェル, bash, 連載 この記事は最終更新日が1年以上前のものです。

 これの解決編です。理論派ではなく感覚派なので、複雑なの苦手なのですが・・・解決しているような気がします。環境は次の通り。

$ echo $BASH_VERSION
   5.1.16(1)-release
   $ cat /etc/lsb-release
   DISTRIB_ID=Ubuntu
   DISTRIB_RELEASE=22.04
   DISTRIB_CODENAME=jammy
   DISTRIB_DESCRIPTION="Ubuntu 22.04.3 LTS"

まとめてリダイレクトする記号の怪

 以前の記事でも触れていますが、Bashには標準出力と標準エラー出力をまとめてリダイレクトする&>という記号があります。また、&>の(manによると)「望ましくない」別の書き方に、>&があります。例を示します。

### lsの通常の出力(ファイル名)とエラーをaというファイルに格納 ###
   $ ls /etc/hostname /etc/うんこ >& a 
   $ cat a
   ls: '/etc/うんこ' にアクセスできません: そのようなファイルやディレクトリはありません
   /etc/hostname

で、manに書いてないんですけど、>&1>&とも書けるみたいです。2>&だとダメです。

### これでもよい(なんでや) ###
   $ ls /etc/hostname /etc/うんこ 1>& b
   $ cat b
   ls: '/etc/うんこ' にアクセスできません: そのようなファイルやディレクトリはありません
   /etc/hostname
   ### これはダメ ###
   $ ls /etc/hostname /etc/うんこ 2>& b
   bash: b: 曖昧なリダイレクトです

1>&ファイル記述子なのか1>&ファイルなのか紛らわしい

 で、ここで問題になるのは、Bashだと&>ファイルと等価な1>&ファイルと、ファイル記述子1を別のファイル記述子xにリダイレクトする1>&xという書き方があることです。このふたつの書き方があることで、次のようにやたらややこしいことが起こります。

### これは「3わーお」がファイル扱いされて、&>と同じ動きになる ###
   $ ls /etc/hostname /etc/うんこ 1>&3わーお
   $ cat 3わーお 
   ls: '/etc/うんこ' にアクセスできません: そのようなファイルやディレクトリはありません
   /etc/hostname
   ### これは FD>&FD という扱いになり、FD3が開いてないので怒られる ###
   $ ls /etc/hostname /etc/うんこ 1>&3
   bash: 3: 不正なファイル記述子です
   # 3を2にリダイレクトしておけば大丈夫
   $ ls /etc/hostname /etc/うんこ 3>&2 1>&3
   ls: '/etc/うんこ' にアクセスできません: そのようなファイルやディレクトリはありません
   /etc/hostname
   ### これもダメ ###
    ls /etc/hostname /etc/うんこ 1>&1000000000000000
   bash: 1000000000000000: 不正なファイル記述子です

謎の解決1

 で、以前の記事で謎とされていた、

$ ls /etc/hostname /etc/うんこ 1>&1000000000000000
   bash: 1000000000000000: 不正なファイル記述子です    #右側のFDが怒られる
   $ ls /etc/hostname /etc/うんこ 2>&1000000000000000
   bash: 2: 不正なファイル記述子です                   #左側のFDが怒られる

という挙動の違いですが、とりあえずこれは、1>&1000000000000000 のときは&>の処理、2>&1000000000000000のときは2>&FD という処理が走っていると考えると合点がいきます。 (コードをまだ読んでいないし、読むの辛いので確証は得てませんが。) つまり、別のことをやっているのですから、 コード中の別の箇所がエラーを出しているということになります。 1000000000000000がファイルでなくファイル記述子として扱われる理由はなぜかよく分からないので、 そこは疑問として残りますが、 少なくとも同じ箇所でエラーが出ているわけではなさそうです。

謎の解決2

 ただ、なんで2>&1000000000000000のときに左側のファイル記述子2のほうが叱られるか、 という問題がまだ未解決です。また、右側のファイル記述子がそこまで大きな数字でないと、 右側のほうが叱られるという現象もありました。例を再掲します。

$ echo 2>&1
                                           #OK
   $ echo 2>&10
   bash: 10: 不正なファイル記述子です   #10が悪い
   $ echo 2>&10000
   bash: 10000: 不正なファイル記述子です #10000が悪い
   $ echo 2>&100000000000
   bash: 2: 不正なファイル記述子です #なぜか2が悪いことになる

 これについては、 次のようにstraceを使うと分かります。 grepを使ってリダイレクト処理をしている部分 (最初にdup2している部分) を抽出すると、

### 開いているFD ###
   $ strace bash -c 'echo a 2>&1' |& grep dup2 | head -n 1
   dup2(1, 2)                              = 2
   ### 開いていないFD ###
   $ strace bash -c 'echo a 2>&10' |& grep dup2 | head -n 1
   dup2(10, 2)                             = -1 EBADF (不正なファイル記述子です)
   ### ulimitの値を超えるFD ###
   $ strace bash -c 'echo a 2>&10000' |& grep dup2 | head -n 1
   dup2(10000, 2)                          = -1 EBADF (不正なファイル記述子です)
   ### int(32bit符号付き整数)の値を超えるFD ###
   $ strace bash -c 'echo a 2>&100000000000' |& grep dup2 | head -n 1
   dup2(-1, 2)                             = -1 EBADF (不正なファイル記述子です)

となります。最後の例のように右側が intの範囲をこえた場合でもBashは(なぜか)無理にリダイレクトを実行して、 dup2-1が渡って失敗します。 dup2というシステムコールは、ふたつの引数のどっちがおかしいかを一切教えてくれないので、 Bashは第1引数が-1でなければ第2引数、そうでなければ第1引数のところを「不正」と表示するのでしょう。 おそらく。

 ということで、ほんとうならBashのコードを読めばいいものを、 ブラックボックスとして解析しているのはよいことなのかどうなのか分かりませんが、 個人的には納得しました。

 現場からは以上です。やべえさん、議論にお付き合いいただきありがとうございます!細かい話が好きな人は、Software Designでやってる連載もどうぞー。

ノート   このエントリーをはてなブックマークに追加 
 

prev:リダイレクトのエラーの謎3 next:【ごめんなさい】連載の実装漏れとバグ

やり散らかし一覧

記事いろいろ