# シェルの実行順序について めも

# きっかけ

以下のコマンドを実行すると, 異なる結果が得られる。 この原因は, $ md5sum 2.txt > 2.txtを実行した際に, リダイレクトによって2.txtが空のファイルになった後にmd5sumが実行されてしまうためである。

シェルの仕様です。既存のファイルをリダイレクト先に選ぶと、 ファイルサイズを0にします。 ref: 同じ名前のファイルにリダイレクト

こういったことがあったので, シェルの実行順序について少し調べることにした。

$ echo "hoge" | md5sum > 1.txt
$ echo "hoge" > 2.txt
$ md5sum 2.txt > 2.txt
$ cat 1.txt
c59548c3c576228486a1f0037eb16a1b  -
$ cat 2.txt
d41d8cd98f00b204e9800998ecf8427e  2.txt

# 標準入出力

標準入出力には標準入力, 標準出力, 標準エラー出力の3種類がある。

デフォルトでは, 標準入力はファイルディスクリプタ(以下FDとする)の0, 標準出力はFD1, 標準エラー出力はFD2が割り当てられている。

したがって, コマンドを実行する際に以下のフローとなる。

  1. キーボードから標準入力が行われる
  2. FD0を介してコマンドが渡され, コマンドが実行される
  3. FD1を介して端末画面にコマンド実行結果の標準出力が行われる
  4. 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 [| コマンドB]+である。これで, コマンド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 標準エラー出力

# 終わりに

きっかけはつまらないことだったが, 調べてみると私の中で曖昧にしていた部分があったことがわかった。

また, 以上の知識とググり力を用いることで分かるコマンド群を3つ用意した。
考えてみると面白いので, あえて実行の流れ・結果は掲載しない。
ぜひ考えてみると面白いかもしれない。

  • ls -la | grep hoge
  • $ cmd > /dev/null 2>&1
  • $ bash -i >& /dev/tcp/localhost/8888 0>&1

# ref

Last Updated: 2020-1-5 12:35:49