sh/bashのパイプ/リダイレクションの使い方

sh/bashのパイプ/リダイレクションの使い方についてよく使いがちなものを理解できるようにまとめておく。

モチベーション:挙動がよくわからない

シェルスクリプトに書かれているリダイレクションが理解できない。

これまで、運用とかしてこなかったので人の書いたスクリプトを読むこともなく生きてきて、シェルはtcshでできる部分でいいや、という感じで生きてきた。でも環境はいつもデフォルトで使っちゃう派なので何かしら自動化しようとするとsh/bashを使わないと色々めんどいことに気づいた。しかたがないのでいまさらながらリダイレクションの記法について学ぶことにしてみた。

自分の理解度テスト的に下記を上げてみたけど、こんな感じ。

cmd > file # わかる
cmd >& file # なんだっけ?
cmd1 | cmd2 # わかる 
cmd1 |& cmd2 # わかる気もする。でも自分で書くときはこれが出てこない。

cmd 2>&1 > /dev/null # えっ?

以下に大体の読み方の基本から順番に書いていく。

基本

sh/bashのリダイレクションは基本的に (ディスクリプタ)> (ファイル) の羅列の形式である。
たとえば、

cmd 1> file

だとディスクリプタ1の出力先をfileという名前のファイルに設定する、という意味になる。

基本的にディスクリプタの1は標準出力、2は標準エラー出力なので、下記は「標準出力の出力先をfile1に、標準エラー出力の出力先をfile2に設定する」という意味になる。

cmd 1> file1 2> file2

>の代わりに>>を使うとファイルに追記する意味になる。

cmd 1>> file1
cmd >> file1

パイプは標準出力の出力先を後続のコマンドの標準入力に設定することを意味する。
下記はcmd1の標準出力をcmd2の標準入力に設定する。

cmd1 | cmd2

パイプにはディスクリプタ番号は指定できない。常に標準出力を次のコマンドの標準入力につなぐ。

リダイレクションの省略形

リダイレクションのディスクリプタが省略された場合は1を意味する。
つまり、下記の2つは同じ。

cmd > file
cmd 1> file

ディスクリプタの複製

>& 記号を使ったディスクリプタの複製について説明するが、これは複製?よくわからないので複製というのは「そういう名前」だと思って深く考えないほうが読みやすいかもしれない。

a>&b はディスクリプタaの出力先をディスクリプタbと同じにすることを意味する。
下記は標準エラー出力を標準出力と同じターミナルに出力する。

cmd 2>&1

なお、ディスクリプタaは省略できてデフォルトは2である。bは省略不可。

つまり、下記も同じ意味になる。

cmd >&1

複数の記述がある場合の理解の仕方(これ重要)

基本パターン

複数の記述がある場合、基本的には左から解釈される。
この過程をこれから述べるように表の形式で理解していくと出力結果を考えやすい。

基本的に何もしなければ標準出力等の出力先は下記のように設定されている。

ディスクリプタ 出力先(当初)
1 標準出力(ターミナル)
2 標準エラー出力(ターミナル)

ファイルへのリダイレクションが追加されると

cmd 1> file1 2> file2
ディスクリプタ 出力先(当初) 1> file1 適用後 2> file2 適用後
1 標準出力(ターミナル) file1 file1
2 標準エラー出力(ターミナル) 標準エラー出力(ターミナル) file2

標準出力はfile1へ、標準エラー出力はfile2へ出ることになる。

複製を含む場合

複製を含む場合、上記で説明したように複製を「出力先を同じにする」と理解するのがポイントになる。

下記のような複製がなされた場合、複製は出力先を同じにするので下記のようになる。

cmd 2>&1
ディスクリプタ 出力先(当初) 2>&1適用後
1 標準出力(ターミナル) 標準出力(ターミナル)
2 標準エラー出力(ターミナル) 標準出力(ターミナル)

下記のように順序を逆にすると結果が変わる。

cmd 2>&1 1> file1
ディスクリプタ 出力先(当初) 2>&1適用後 1> file1 適用後
1 標準出力(ターミナル) 標準出力(ターミナル) file1
2 標準エラー出力(ターミナル) 標準出力(ターミナル) 標準出力(ターミナル)

逆パターンだと

cmd 1> file1 2>&1
ディスクリプタ 出力先(当初) 1> file1 適用後 2>&1適用後
1 標準出力(ターミナル) file1 file1
2 標準エラー出力(ターミナル) 標準エラー出力(ターミナル) file1

ディスクリプタの数を増やしてみる。新しいディスクリプタ3にはデフォルトでは何も設定されていない。

cmd 2>&1 3>&2
ディスクリプタ 出力先(当初) 2>&1適用後 3>&2適用後
1 標準出力(ターミナル) 標準出力(ターミナル) 標準出力(ターミナル)
2 標準エラー出力(ターミナル) 標準出力(ターミナル) 標準出力(ターミナル)
3 未設定 未設定 標準出力(ターミナル)

逆にしてみると

cmd 3>&2 2>&1
ディスクリプタ 出力先(当初) 3>&2適用後 2>&1適用後
1 標準出力(ターミナル) 標準出力(ターミナル) 標準出力(ターミナル)
2 標準エラー出力(ターミナル) 標準エラー出力(ターミナル) 標準出力(ターミナル)
3 未設定 標準エラー出力(ターミナル) 標準エラー出力(ターミナル)

パイプを含む場合

パイプを含む場合、標準出力の当初値が変わる。

cmd1 | cmd2
ディスクリプタ 出力先(当初)
1 cmd2の標準入力
2 標準エラー出力(ターミナル)

複製と併用するとこんな感じになる。

cmd1 2>&1 | cmd2
ディスクリプタ 出力先(当初) 2>&1適用後
1 cmd2の標準入力 cmd2の標準入力
2 標準エラー出力(ターミナル) cmd2の標準入力

この例は比較的記法の似た cmd 2>&1 1> file のパターンよりは cmd 1> file 2>&1 のパターンとの類似性を強く感じるものになっている。
よく「左から読め」と言われるのだけれども、パイプだけは右側に書かれているのに、一番左に書いたような挙動をするので非常にわかりにくい感じがする。

複製と他の構文をまとめた記法

複製には他の構文とまとめる記法がある。

ファイル出力とまとめて書くことができる。

cmd 2>& file

これは下記と同じ。

cmd 1> file 2>&1

パイプとまとめて書く記法がある。

cmd1 |& cmd2

これは下記と同じ。

cmd1 2>&1 | cmd2

復習

出だしにあったコマンド群

cmd > file # わかる
cmd >& file # なんだっけ?
cmd1 | cmd2 # わかる 
cmd1 |& cmd2 # わかる気もする。書けない。

cmd 2>&1 > /dev/null # えっ?

は下記であることがわかるようになります。

cmd > file # 標準出力をfileに設定
cmd >& file # 標準出力と標準エラー出力をfileに設定
cmd1 | cmd2 # 標準出力をcmd2の標準入力に 
cmd1 |& cmd2 # 標準出力と標準エラー出力をcmd2の標準入力に
cmd 2>&1 > /dev/null # 標準出力を/dev/nullに、標準エラー出力をターミナルに標準出力として出力

結論

大事なことを書きます。

ググるのもいいけど、man (1) bash 読め。全部書いてある。

その他

2年ぶりに書いた記事がこれかよ、と悲しんでいる。