JavaScriptを有効にしてください

疑似コードでわかりやすい?リダレクトとファイルディスクリプタの解説

 ·  ☕ 7 分で読めます
Photo by Mike

Photo by Mike

はじめに

Linuxのシェルでコマンドを実行した結果をファイルに出力する際などに、次のようなリダイレクトを使うことがあると思う。

1
ls > output.txt

では標準出力と標準エラーの両方をひとつのファイルに出力する場合は、どうだろう?
以下のコマンドで標準出力と標準エラーがoutput.txtに出力される。

1
ls /path/to/wrong_file > output.txt 2>&1

しかし、似たような以下のコマンドでは正しく動作しない。
これだと標準出力のみがoutput.txtに出力され、標準エラーはコンソールに出力される。

1
ls /path/to/wrong_file 2>&1 > output.txt

この挙動を理解しようとしたときに、ファイルディスクリプタというものの理解が必要になる。
自分の中でこの動きを整理するのに時間がかかったため、考え方をまとめておこうと思う。

用語の説明

ファイルディスクリプタ

あるプロセスが入出力できるファイルを識別するための整数値

[0], [1], [2]はデフォルトで設定される特殊なFD(ファイルディスクリプタ)であり、それぞれ下記の用途で利用される。

FD 用途
0 標準入力
1 標準出力
2 標準エラー

[1], [2]はデフォルトではコンソール画面(もしくはパイプラインの先)を向いている。

3〜も使うことができ、後述の/dev/fd/数字でファイルとしてアクセスすることが可能。

リダイレクト

コマンドの末尾に>>>などを記述することで、入出力をコントロールできる。

演算子 意味
> 書込み
>> 追記
< 読込み

なお、>1>と同義となる。つまり標準出力の書き込みとなる。
同様に、<0<と同義となる。つまり標準入力への読み込みとなる。

ファイルディスクリプタに対してもリダイレクトを行うことができる。

演算子 意味
[n]>&[m] FD[n]からFD[m]へファイルディスクリプタの複製
[n]>&[m]- FD[n]からFD[m]へファイルディスクリプタの変更

特殊ファイル

ファイル 意味
/dev/fd/数字 FD~とつながるファイル
/dev/stdin FD0すなわち標準入力とつながるファイル
/dev/stdout FD1すなわち標準出力とつながるファイル
/dev/stderr FD2すなわち標準エラーとつながるファイル

結論:このルールで動いている

自分の中で色々と調べ整理した結果、以下のルールで動く・・・はず。
(ただしルールだけを読んでもわかりづらいので次節を参照するのをおすすめ)

  1. コマンドの標準出力はFD1の先にあるファイルに出力される
  2. コマンドの標準エラーはFD2の先にあるファイルに出力される
  3. デフォルトではFD1は、/dev/pts/<digit> に向いている(ただしFD2とは別の参照)
  4. デフォルトではFD2は、/dev/pts/<digit> に向いている(ただしFD1とは別の参照)
  5. [n]>&[m] の形式でリダイレクトを記述することで、FD[n]の向き先はFD[m]の複製となる(参照コピーのイメージ)
  6. [n] > file の形式でダイレクトを記述することで、FD[n]の向き先はfileになる
  7. リダイレクトは左から順に評価される

補足

※ FD = ファイルディスクリプタで省略
※ /dev/pts/<digit> は我々が画面上に見ている仮想ターミナルに繋がっているファイル。

挙動を疑似コードで理解する

上記の動きを理解するために、プログラミング言語に置き換えて考えることが有効だと思う。 具体的には、ファイルディスクリプタを変数、向き先のファイルを値として考える。
※ 以下ではPythonで書いてみるが、特に言語に意味はなく、変数が保持するのはあくまで参照であり値ではないということが大事。

まず、シェルでは標準出力はFD1に標準エラーはFD2に流れていくわけだが、これはFD1とFD2の変数に入っている値すなわちファイルに出力されるということだと捉える。

1
2
3
# イメージ
write_stdout_to(FD1)
write_stderr_to(FD2)

次に、FDをファイルにリダイレクトするという行為は、FDの変数にファイルのオブジェクトの参照を代入するということだと捉える。

1
2
3
# イメージ
# 1 > /path/to/file
FD1 = File('/path/to/file')

次に、FDからFDへリダイレクトするという行為は、FD1にFD2を代入するということだと捉える。

1
2
3
# イメージ
# 2>&1
FD2 = FD1

そして重要なのは、ファイルへのリダイレクトは参照の書き換えなので、同じ参照を持つ別の変数には影響しないということだ。

1
2
3
4
5
6
7
# イメージ
FD1 = File('/path/to/file1')
FD2 = FD1
FD1 = File('/path/to/file2')

print(FD1) # => /path/to/file2
print(FD2) # => /path/to/file1

例題

1. 標準出力と標準エラーをファイルに出力する

1
ls > output.txt 2>&1

解説

初期状態としてFD1、FD2はコンソール画面を向いている。
まず、FD1の参照が新たにoutput.txtを向いた参照に置き換わる。(>1>の省略であることに注意)
次に、FD2の向き先がFD1の複製となる。つまりFD1とFD2は同じ参照となり、ともにoutput.txtを向く。

結果として、FD1、FD2ともにoutput.txtを向いた参照となる。

疑似コード

1
2
3
4
5
# ls > output.txt 2>&1
FD1 = File('/dev/pts/8')
FD2 = File('/dev/pts/8')
FD1 = File('output.txt')
FD2 = FD1

2. 標準出力はファイルに、標準エラーはコンソール画面に出力する

1
ls 2>&1 > output.txt

解説

初期状態としてFD1、FD2はコンソール画面を向いている
まず、FD2の向き先がFD1の複製となる。つまりFD2とFD1は同じ参照となり、ともにコンソール画面を向く。
次に、FD1の参照が新たにoutput.txtを向いた参照に置き換わる。(ここで置き換わるのはFD1の参照であり、値ではないと理解する。つまりFD2の参照が向いている値は変更されないため、コンソール画面のままとなる。)

結果としてFD1はoutput.txtを向いた参照、FD2はコンソール画面を向いた参照となる。

疑似コード

1
2
3
4
5
# ls 2>&1 > output.txt
FD1 = File('/dev/pts/8')
FD2 = File('/dev/pts/8')
FD2 = FD1
FD1 = File('output.txt')

3. (応用)コマンドがファイルに書き込んだ文字列をコンソールに出力する

1
example.sh '/dev/fd/3' 3>&1 > /dev/null

前提として、example.shは引数としてファイルパスを受け取り、何らかのデータをそのファイルに書き込むスクリプトだとする。

初期状態としてFD1、FD2はコンソール画面を向いている。FD3の参照はデフォルトでnullである。
まず、FD3の向き先がFD1の複製となる。つまりFD1とFD3は同じ参照となり、ともにコンソール画面を向く。
次に、FD1の参照が新たに/dev/nullを向いた参照に置き換わる。(/dev/null は出力された内容を捨てるための仮想ファイル)

結果としてFD1は/dev/nullを向いた参照、FD3はコンソール画面を向いた参照となる。

example.shが/dev/fd/3に文字列を書き込むと、/dev/fd/3はFD3に繋がっているため文字列がコンソール画面に出力される。

疑似コード

1
2
3
4
5
6
# example.sh '/dev/fd/3' 3>&1 > /dev/null
FD1 = File('/dev/pts/8')
FD2 = File('/dev/pts/8')
FD3 = None
FD3 = FD1
FD1 = File('/dev/null')

おまけ:Linuxカーネルの実装との関係性

ファイルディスクリプタをファイルへの参照を持った変数として考えることは、最初は自分の単なる思いつきだったがよく調べてみると、実は理にかなっているような気がする。
というのもLinuxカーネルのCのコードを解説されている記事を読んでみると、Linuxの各プロセスはそれぞれfdという配列を持っていて、fd配列にはfile構造体のポインタが格納されているらしい。
そしてfd配列の添字こそがファイルディスクリプタの値なのである。

つまり、ファイルディスクリプタはCの実装上もファイルのポインタ(参照)として実装されているわけなので、上に書いたような疑似コードは割と実態に即していると言えるのでは。

参考

Man page of BASH
Linuxのファイルディスクリプタをハックする - Qiita

共有