はじめに(この記事を書くに至った経緯)
以下のコマンドを実行すると, 1.txt
と2.txt
で異なるハッシュ値が得られる。
$ echo "hoge" | md5sum > 1.txt
$ echo "hoge" > 2.txt
$ md5sum 2.txt > 2.txt
$ cat 1.txt
c59548c3c576228486a1f0037eb16a1b -
$ cat 2.txt
d41d8cd98f00b204e9800998ecf8427e 2.txt
この原因は, $ md5sum 2.txt > 2.txt
を実行した際に, リダイレクトによって2.txt
が空のファイルになった後にmd5sum
が実行されてしまうためである。
シェルの仕様です。既存のファイルをリダイレクト先に選ぶと、 ファイルサイズを0にします。
ref: 同じ名前のファイルにリダイレクト
この例はつまらないものだったが, 原因を究明している間にリダイレクトについてまとめていたため, Qiitaにて共有することにした。
もくじ
- はじめに
- 標準入出力
- リダイレクト
- パイプ
- ファイルと標準出力へ同時に出力したい時
- $ cmd > out.txt 2&>1 と $ cmd > 2&>1 > out.txtの違い
- $ cmd < in.txt > out.txtと$ cmd < in.txt 2>&1について
- パイプを含んだ例
- おわりに
- 参考文献
標準入出力
標準入出力には標準入力, 標準出力, 標準エラー出力の3種類がある。
Wikipediaによると, POSIXでは, 標準入力はファイルディスクリプタ(以下FDとする)の0, 標準出力はFD1, 標準エラー出力はFD2が割り当てられている。
したがって, コマンドを実行する際に以下のフローとなる。
- キーボードから標準入力が行われる
- FD0を介してコマンドが渡され, コマンドが実行される
- FD1を介して端末画面にコマンド実行結果の標準出力が行われる
- FD2を介して端末画面にコマンド実行結果の標準エラー出力が行われる
リダイレクト
FDの参照先を変更する操作 のこと。
標準入力や標準出力, 標準エラー出力そのものを操作するわけではない。
出力先をファイルにリダイレクトする
リダイレクト演算子>
または>>
を用いる。
>
と>>
の違いは, >
がファイルを上書きモードで開くのに対し, >>
がファイルを追記モードで開くという違いである。
リダイレクト演算子>
を使う場合は, リダイレクト先のファイル有無に関わらず, 空のファイルが生成される。今回調べるきっかけとなった$ md5sum 2.txt > 2.txt
は, md5sum
が実行される前に2.txt
が空ファイルになったために, 空ファイルに対してmd5sumを計算してしまい結果が異なってしまった。
書式は, $ コマンド [n]> ファイル名
である。n
にはリダイレクトするFD番号を書く。デフォルトではFD1番(=標準出力)がリダイレクトされるため, n
は省略可能である。実行例は以下の通りである。
$ : FD1番(標準出力)の内容(hoge)がhoge.txtに上書き保存される
$ echo "hoge" > hoge.txt
$ cat hoge.txt
hoge
$ : FD2番(標準エラー出力)の内容がerror.txtに上書きされる
$ ls --- 2> error.txt
$ cat error.txt
ls: illegal option -- -
usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]
以上の例の通り, リダイレクトによってFDの参照先を変更しているため, 結果は端末画面には表示されない。
出力先を他のFDにリダイレクトする
リダイレクト演算子>&
を用いる。
書式は, $ コマンド n>&m ファイル名
である。n
にはリダイレクトするFD番号を書き, m
にはリダイレクト先のFD番号を書く。このリダイレクト演算子は, n
のFDにm
のFDが複製されるという操作をする。実行例は以下の通りである。
$ : FD2番(標準エラー出力)をFD1番にリダイレクトしてまとめてerror.txtに上書きする
$ ls --- 2>&1 error.txt
$ cat error.txt
ls: illegal option -- -
usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]
標準入力をファイルにリダイレクトする
リダイレクト演算子<
を用いる。
書式は, $ コマンド [n]< ファイル
である。n
にはリダイレクトするFD番号を書く。デフォルトではFD0番(=標準入力)がリダイレクトされるため, n
は省略可能である。実行例は以下の通りである。
$ cat hoge.txt
hoge
$ : hoge.txtの内容をwcコマンドにリダイレクトする
$ wc -l < hoge.txt
パイプライン
前のコマンドの標準出力を次のコマンドに標準入力として渡す操作のこと。
ここで、パイプラインで多段に接続された各々のコマンドは別プロセスとして動作する。
演算子|
を用いる。
書式は, $ コマンドA [| コマンド]+
である。これで, コマンドAの標準出力をコマンドBの標準入力に渡すことになる。実行例は以下の通りである。
$ : ls -aコマンドの標準出力結果をgrepコマンドの標準入力に渡している
$ ls -a | grep hoge
hoge.txt
ファイルと標準出力へ同時に出力したい時
今までは出力先が1つしか選択できなかったが, teeコマンド
を使うとファイルと標準出力へ同時に出力することができる。
書式は, $ tee ファイル名
である。これで, ファイルに対して標準出力を書き出し, 同時に標準出力にも書き出すことができる。
このコマンドは, コマンドが以下のようにTに見えるので, teeコマンドと名付けられたらしい。
$ ls -l | tee file.txt | less
stdout ------+------> stdin
|
v
file.txt
The name tee comes from this scheme - it looks like the capital letter T
ref: tee(command)
$ cmd > out.txt 2&>1 と $ cmd > 2&>1 > out.txtの違い
ここで, 似ている以下の3つのコマンドを提示する。
これらを実行した後, どのような結果がout.txtに出力され, どのような結果が端末画面に出力されるか分かるだろうか。
$ ./test > out.txt 2>&1
$ ./test 2>&1 > out.txt
なお, ./test
は以下のC++プログラムをコンパイルした実行ファイルである。
#include <iostream>
int main() {
std::cout << "This is a stdout\n";
std::cerr << "This is a stderr\n";
return 0;
}
$ ./test > out.txt 2>&1
この結果は, FD1およびFD2の出力がout.txtに出力され, 端末画面には何も出力されないという結果になる。これは順番に確認すれば分かる。
初期状態は以下の通りである。
FD | 参照先 |
---|---|
1 | 標準出力 |
2 | 標準エラー出力 |
次に, > out.txt
までが読まれる。すると, FDの指定がないので, FD1についてリダイレクトが行われて以下の状態になる。
FD | 参照先 |
---|---|
1 | out.txt |
2 | 標準エラー出力 |
そして, 2>&1
までが読まれる。ここで, &>
のリダイレクト演算子がFD2にFD1の複製を作るので, 以下の状態になる。
FD | 参照先 |
---|---|
1 | out.txt |
2 | out.txt |
したがって, 両方のFDがout.txtに出力されることが分かる。
$ ./test > out.txt 2>&1
$ cat out.txt
This is a stdout
This is a stderr
$ ./test 2>&1 > out.txt
この結果は, 標準出力のみがout.txtに出力され, 端末画面には標準エラー出力のみが出力されるという結果になる。これも同様に順番に確認する
。
初期状態は以下の通りである。
FD | 参照先 |
---|---|
1 | 標準出力 |
2 | 標準エラー出力 |
次に 2>&1
までが読まれる。すると, FD2がFD1の複製を作るので以下の状態になる。
FD | 参照先 |
---|---|
1 | 標準出力 |
2 | 標準出力 |
そして, > out.txt
が読まれる。FDの指定がないので, FD1についてリダイレクトが行われて以下の状態になる。
FD | 参照先 |
---|---|
1 | out.txt |
2 | 標準出力 |
したがって, FD1がout.txtに, FD2が標準出力に出力されることがわかる。
$ ./test 2>&1 > out.txt
This is a stderr
$ cat out.txt
This is a stdout
$ cmd < in.txt > out.txtと$ cmd < in.txt 2>&1について
上の例が理解できていれば大したことはない。
$ cmd < in.txt > out.txt
これを実行すると, in.txtを入力して, 標準出力をout.txtに出し, 標準エラー出力を端末画面に出力する。この結果も同様に順番に見ていけば良い。
初期状態は以下の通りである。
FD | 参照先 |
---|---|
0 | 標準入力 |
1 | 標準出力 |
2 | 標準エラー出力 |
次に< in.txt
が読まれると以下のようになる。
FD | 参照先 |
---|---|
0 | in.txt |
1 | 標準出力 |
2 | 標準エラー出力 |
次に, > out.txt
が読まれると以下のようになる。
FD | 参照先 |
---|---|
0 | in.txt |
1 | out.txt |
2 | 標準エラー出力 |
参考までに例を載せておく。
$ cat test.cpp
#include <iostream>
int main() {
std::cout << "This is a stdout\n";
std::cerr << "This is a stderr\n";
return 0;
}
$ wc -l < test.cpp > count.txt
$ cat count.txt
7
$ cmd < in.txt 2>&1について
これを実行すると, in.txt
を入力としてFD1およびFD2の結果が端末画面に出力される。この結果も同様に順番に見ていけば良い。
途中の< in.txt
までは同じなので, そこまで実行した以下の状態から考える。
FD | 参照先 |
---|---|
0 | in.txt |
1 | 標準出力 |
2 | 標準エラー出力 |
そして, 2>&1
を実行すると, FD2にFD1の複製を作るので以下のようになる。
FD | 参照先 |
---|---|
0 | in.txt |
1 | 標準出力 |
2 | 標準出力 |
例は必要ないと判断して省略する。
パイプを含んだ例
ここから先はパイプを含んだ例を提示する。
復習になるが, パイプは前のコマンドの標準出力を次のコマンドに標準入力として渡す操作のことである。
$ cmdA 2>&1 | cmdB
これを実行すると, cmdAのFD1およびFD2の結果が端末画面に出力される。この結果も同様に順番に見ていけば良い。
初期状態は以下の通りである。
FD | 参照先 |
---|---|
0 | 標準入力 |
1 | 標準出力 |
2 | 標準エラー出力 |
まず, cmdA 2>&1
までが実行される。すると, cmdAの実行後にFD2にFD1の複製が作られるため, 以下のようになる。
FD | 参照先 |
---|---|
0 | 標準入力 |
1 | cmdAの実行後のFD1 |
2 | cmdAの実行後のFD1 |
次に, | cmdB
までが実行される。すると, パイプ操作が行われ以下のようになる。
初期状態は以下の通りである。
FD | 参照先 |
---|---|
0 | cmdAの標準出力と標準エラー出力 |
1 | 標準出力 |
2 | 標準エラー出力 |
おわりに
きっかけはつまらないことで, Bashのリダイレクトやパイプは殆ど関係なかった((最近, 本質でない部分でつまづくことが多い......))。しかし, 調べてみると私の中で曖昧にしていた部分があったことがわかったので, Bashの仕様について理解が深められた。
また, 以上の知識とググり力を用いることで分かるコマンド列を3つ用意した。
考えてみると面白いので, あえて実行の流れ・結果は掲載しない。
ぜひ考えてみてほしい。
ls -la | grep hoge
$ cmd > /dev/null 2>&1
$ bash -i >& /dev/tcp/localhost/8888 0>&1