Bash

bash: 標準出力、標準エラー出力をファイル、画面それぞれに出力する方法

More than 1 year has passed since last update.

この記事では、標準出力、標準エラー出力を好き勝手に出力する。
ファイルに出力、画面に出力、両方出力などなどパターン化してみた。

出力用スクリプト作成

なんのコマンドを使うか悩む前に、両方をちゃんと出力するスクリプト作成から開始しよう。
今後の内容ではこれを使っていく。

stdout_and_stderr.sh
echo "stdout" >&1
echo "stderr" >&2

まずは基本

ファイル・ディスクリプター

ファイルディスクリプタとは、プログラムがアクセスするファイルや標準入出力などをOSが識別するために用いる識別子。0から順番に整数の値が割り当てられる。
つまりは、プログラムが開いているファイルが番号付けされていると考えてくれればよい。
OSで標準的に番号付けされているのが、標準入力、標準出力、標準エラー出力の3になっている。

ファイル・ディスクリプター番号 出力先
0 標準入力
1 標準出力
2 標準エラー出力
n 任意の入出力先

以下の説明はman bashからの転記だ。余裕があれば読んでみるといいだろう。
実例については、以降に上げているので、とりあえず動かしたい人は後から戻ってくればいい。
最悪読まなくてもやっていける?

演算子 名前 説明
[n]<word 入力のリダイレクト 入力をリダイレクトすると、wordを展開した結果の名前を持つファイルがオープンされ、ファイル・ディスクリプターnで読み込めるようになります。nが指定されていなければ、読み込みは標準入力(ファイル・ディスクリプター0)で行われます。
[n]>word 出力のリダイレクト 出力をリダイレクトすると、wordの展開した結果の名前を持つファイルがオープンされ、ファイル・ディスクリプターnで書き込めるようになります。nが指定されていなければ、書き込みは標準出力(ファイル・ディスクリプター1)に行われます。ファイルが存在しなかった場合は作成されます。ファイルが存在した場合はサイズ0に切り詰められます。
[n]>>word リダイレクトによる追加出力 この形式を使って出力のリダイレクトを行うと、wordを展開した結果の名前を持つファイルがオープンされ、ファイル・ディスクリプターnに対する出力がこのファイルに追加されるようになります。nを指定しなければ、標準出力(ファイル・ディスクリプター1)で追加されます。ファイルが存在しなければ、新しく作られます。
&>word
>&word
標準出力と標準エラー出力のリダイレクト この構造を使うと、標準出力(ファイル・ディスクリプター1)と標準エラー出力(ファイル・ディスクリプター2)の両方を、wordを展開した結果の名前を持つファイルにリダイレクトできます。
&>>word 標準出力と標準エラー出力の追加出力 この構造を使うと、標準出力(ファイル・ディスクリプター1)と標準エラー出力(ファイル・ディスクリプター2)の両方を、wordを展開した結果の名前を持つファイルに追加できます。
[n]<&word 入力ファイル・ディスクリプターの複製 入力ファイル・ディスクリプターを複製できます。wordが1桁以上の数値に展開された場合、nで示されるファイル・ディスクリプターが生成され、wordで指定された数値のファイル・ディスクリプターのコピーとなります。wordに含まれる数値が入力用にオープンされたファイル・ディスクリプターを指していない場合、リダイレクト・エラーが起きます。wordを評価した結果が-となった場合、ファイル・ディスクリプターnはクローズされます。nが指定されていない場合、標準入力(ファイル・ディスクリプター0)が使われます。
[n]>&word 出力ファイル・ディスクリプターの複製 出力ファイル・ディスクリプターを複製できます。nが指定されていない場合は、標準出力(ファイル・ディスクリプター1)が使われます。wordに含まれる数値が、出力用にオープンされたファイル・ディスクリプターを指していない場合、リダイレクト・エラーが起きます。特別な場合ですが、nが省略され、かつwordが1桁以上の数字には展開されなかった場合、前に説明したように標準出力と標準エラー出力がリダイレクトされます。

標準出力⇒画面、標準エラー出力⇒画面

まずは、なんの捻りもない。
両方とも画面に出力する最も基本的なパターン。

$ bash stdout_and_stderr.sh
stdout
stderr

標準出力⇒ファイル、標準エラー出力⇒画面

次は、一般的なリダイレクト「>」だ。
みんなここから始めるだろう。

$ bash stdout_and_stderr.sh > stdout.log
stderr
$ cat stdout.log
stdout

ちなみに、以下の書き方でも同じように動く。
ファイル・ディスクリプター1は標準出力として定義されている。
デフォルトでは「1」が省略されているだけだ。

$ bash stdout_and_stderr.sh 1> stdout.log
stderr
$ cat stdout.log
stdout

標準出力⇒画面、標準エラー出力⇒ファイル

標準エラー出力をファイルに書き込んでみよう。
bashを扱い始めた頃はこんな基本的なことも覚えられなかった。

ファイル・ディスクリプター2は標準エラー出力として定義されている。
つまり、標準エラー出力だけをリダイレクトしますよってのを書いている。

$ bash stdout_and_stderr.sh 2> stderr.log
stdout
$ cat stderr.log
stderr

標準出力⇒ファイル1、標準エラー出力⇒ファイル1

標準出力、標準エラー出力の両方を同じファイルに出力する。
基本的な書き方は以下の3つ、どれも同じ動作になる。

ちなみに、「> file 2>&1」と「2>&1 > file」では全く処理結果が異なってくる。これについては、後の「リダイレクトの記載順と処理内容について」を参照してほしい。

$ bash stdout_and_stderr.sh >& stdout_and_stderr.log
$ cat stdout_and_stderr.log
stdout
stderr
$ bash stdout_and_stderr.sh &> stdout_and_stderr.log
$ cat stdout_and_stderr.log
stdout
stderr
$ bash stdout_and_stderr.sh > stdout_and_stderr.log 2>&1
$ cat stdout_and_stderr.log
stdout
stderr

標準出力⇒ファイル1、標準エラー出力⇒ファイル2

標準出力と標準エラー出力を別ファイルに出力してみよう。

$ bash stdout_and_stderr.sh 1> stdout.log 2> stderr.log
$ cat stdout.log
stdout
$ cat stderr.log
stderr

出力を捨ててみよう

出力を捨てる場合、一般的に/dev/null(nullデバイス)を利用する。

標準出力⇒なし、標準エラー出力⇒なし

標準出力、標準エラー出力の両方を捨てるケースだ。
ファイルにも画面にも出力したないときに利用する。
いろんな書き方ができるので、どれでも覚えやすいのを使えばいいだろう。

$ bash stdout_and_stderr.sh >& /dev/null
$ bash stdout_and_stderr.sh &> /dev/null
$ bash stdout_and_stderr.sh > /dev/null 2>&1
$ bash stdout_and_stderr.sh 1> /dev/null 2> /dev/null

※ 「1> /dev/null 2> /dev/null」の書き方は捨てる前提の処理であり、通常のファイルへ出力する場合は利用してはいけない。利用したい場合は、標準エラー出力側を追記にする「1> file 2>> file」

標準出力、標準エラー出力を入れ替えてみよう

標準入力、標準出力のリダイレクト先を指定できることを知ったところで、標準出力、標準エラー出力を入れ替える方法を知っておこう。

  • 「3>&1 1>&2 2>&3」
  • 「3>&2 2>&1 1>&3」

どちらでも実現できる。

$ bash stdout_and_stderr.sh 3>&1 1>&2 2>&3
stdout
stderr
$ bash stdout_and_stderr.sh 3>&2 2>&1 1>&3
stdout
stderr

一応複合コマンドを使って確認。

$ { bash stdout_and_stderr.sh 3>&1 1>&2 2>&3; } 1> stdout.log 2> stderr.log
$ cat stdout.log
stderr
$ cat stderr.log
stdout
$ { bash stdout_and_stderr.sh 3>&2 2>&1 1>&3; } 1> stdout.log 2> stderr.log
$ cat stdout.log
stderr
$ cat stderr.log
stdout

「3>&1 1>&2 2>&3」を例にとって、動作を説明しておこう。
内容としては、単に中身の入れ替えだ。C言語で番号だけ入れ替えを行ってみたが、やっていることはこれと同じだ。

#include<stdio.h>

int main() {
     // ファイル・ディスクリプター1(標準出力)
     int fd1=1;
     // ファイル・ディスクリプタ―2(標準エラー出力)
     int fd2=2;

     print("fd1=%d, fd2=%d", fd1, fd2);

     // 出力先をswapする
     // ファイル・ディスクリプタ―1をファイル・ディスクリプタ―3へ退避
     int fd3=fd1;
     // ファイル・ディスクリプタ―2をファイル・ディスクリプタ―1にする
     fd1=fd2;
     // ファイル・ディスクリプタ―3をファイル・ディスクリプタ―2にする
     fd2=fd3;

     print("fd1=%d, fd2=%d", fd1, fd2);
}
$ gcc swap.c
$ ./a.out
fd1=1, fd2=2
fd1=2, fd2=1
ファイル・ディスクリプターの番号 参照先
1 標準出力
2 標準エラー出力

「3>&1」では、ファイル・ディスクリプタ3にファイルディスクリプタ1を複製する。

ファイル・ディスクリプターの番号 参照先
1 標準出力
2 標準エラー出力
3 標準出力

「1>&2」では、ファイル・ディスクリプタ1にファイルディスクリプタ2を複製する。

ファイル・ディスクリプターの番号 参照先
1 標準エラー出力
2 標準エラー出力
3 標準出力

「2>&3」では、ファイル・ディスクリプタ2にファイルディスクリプタ3を複製する。

ファイル・ディスクリプターの番号 参照先
1 標準エラー出力
2 標準出力
3 標準出力

ということで、入れ替わった。

ファイルと画面両方へ出力してみよう

標準出力、標準エラー出力をファイルと画面両方に渡したいことがあるだろう。
いろいろ試してみる。

標準出力⇒ファイル、標準出力⇒画面(標準出力)、標準エラー出力⇒画面(標準エラー出力)

まずは、標準出力だけをファイルと画面の両方に出力する。
teeコマンドを使って実現する。

$ bash stdout_and_stderr.sh | tee stdout.log
stderr
stdout
$ cat stdout.log
stdout

標準出力⇒ファイル1、標準出力⇒画面(標準出力)、標準エラー出力⇒ファイル1、標準エラー出力⇒画面(標準出力)

標準出力、標準エラー出力をファイルと画面両方に出力する。
ただし、この方法では標準エラー出力が標準出力として出力されてしまうので注意が必要だ。

$ bash stdout_and_stderr.sh |& tee stdout_and_stderr.log
stdout
stderr
$ cat stdout_and_stderr.log
stdout
stderr
$ bash stdout_and_stderr.sh 2>&1 | tee stdout_and_stderr.log
stdout
stderr
$ cat stdout_and_stderr.log
stdout
stderr

標準エラー出力⇒ファイル、標準出力⇒画面(標準出力)、標準エラー出力⇒画面(標準エラー出力)

さぁ、ここからが検索してもなかなか出てきてくれない内容だ。
標準エラー出力のみをファイル、画面に出力する方法だ。

思いついたのは、標準出力と標準エラー出力を入れ替えてからteeコマンドに渡す方法だ。
ただ、この状態では、teeコマンド後も入れ替わったままになるので、最後に戻している。

$ { bash stdout_and_stderr.sh 3>&2 2>&1 1>&3 | tee stderr.log; } 3>&2 2>&1 1>&3
stdout
stderr
$ cat stderr.log
stderr

少し入れ替え方を変えてみるとこんな感じだ。

$ { { bash stdout_and_stderr.sh 1>&3; } 2>&1 | tee stderr.log 1>&2; } 3>&1
stdout
stderr
$ cat stderr.log
stderr

標準エラー出力⇒ファイル1、標準出力⇒画面(標準出力)、標準エラー出力⇒ファイル2、標準エラー出力⇒画面(標準エラー出力)

最後に標準出力、標準エラー出力両方をファイル、画面それぞれに出力する方法だ。
ここまでくるとさすがにコマンドが長くなってくるが、テンプレさえ覚えておけば活用できるだろう。

$ { { bash stdout_and_stderr.sh | tee stdout.log; } 3>&2 2>&1 1>&3 | tee stderr.log; } 3>&2 2>&1 1>&3
stderr
stdout
$ cat stdout.log
stdout
$ cat stderr.log
stderr
$ { { bash stdout_and_stderr.sh | tee stdout.log 1>&3; } 2>&1 | tee stderr.log 1>&2; } 3>&1
stdout
stderr
$ cat stdout.log
stdout
$ cat stderr.log
stderr

リダイレクトの記載順と処理内容について

「> file 2>&1」と「2>&1 > file」の結果が変わってくる原因について見ていこう。
リダイレクトは記載した順番、左から右へと順番にファイル・ディスクリプターの参照を更新していく。

まずは、どんな結果になるのか見ていく。

$ bash stdout_and_stderr.sh > stdout_and_stderr.log 2>&1
$ cat stdout_and_stderr.log
stdout
stderr
$ bash  stdout_and_stderr.sh 2>&1 > stdout_and_stderr.log
stderr
$ cat stdout_and_stderr.log
stdout

後者の結果では、ファイルに書き込めたのは標準出力の内容であり、stderrは標準出力で吐き出されている。
stderrが標準出力であることの確認は以下でできる。

$ { bash  stdout_and_stderr.sh 2>&1 > file.txt; } 1> stdout.log 2> stderr.log
$ vim file.txt
$ cat file.txt
stdout
$ cat stdout.log
stderr
$ cat stderr.log

リダイレクトを勉強し始めた頃だと、この違いが理解できずに書く順番を忘れることもある。

ちなみに、man bashの説明は以下だ。

リダイレクトの順番には意味がある点に注意してください。 例えば、次のコマンド

  ls > dirlist 2>&1

は標準出力と標準エラー出力を両方ともファイル dirlist に書き込みますが、次のコマンド

  ls 2>&1 > dirlist

では標準出力だけがファイル dirlist に書き込まれます。なぜなら後者の場合には、標準エラー出力は dirlist にリダイレクトされる前の標準出力の複製となるからです。

初めて読んだときは、少し悩まされたが、解き明かしていけば単純だ。

「> file 2>&1」について

「> file 2>&1」を実施した場合どうなっているか。

まずは、ファイル・ディスクリプターの参照先をしっかり理解しよう。
デフォルトの状態は以下だ。

ファイル・ディスクリプターの番号 参照先
1 標準出力
2 標準エラー出力

「> file」の時点では、ファイル・ディスクリプタ―1がfileを参照する。

ファイル・ディスクリプターの番号 参照先
1 file
2 標準エラー出力

次に、「2>&1」では、ファイル・ディスクリプタ―2がファイル・ディスクリプタ―1の中身を複製したものとなる。

ファイル・ディスクリプターの番号 参照先
1 file
2 1の中身を複製⇒file

結果的にどうなっているか。

ファイル・ディスクリプターの番号 参照先
1 file
2 file

ファイル・ディスクリプタ―1、2が同じ参照先になったことがわかるだろう。

「2>&1 > file」について

同じように、「2>&1 > file」について見ていく。

開始は同じデフォルトの状態だ。

ファイル・ディスクリプターの番号 参照先
1 標準出力
2 標準エラー出力

「2>&1」では、ファイル・ディスクリプタ―2がファイル・ディスクリプタ―1の中身を複製したものとなる。

ファイル・ディスクリプターの番号 参照先
1 標準出力
2 1の中身を複製⇒標準出力

「> file」の時点では、ファイル・ディスクリプタ―1がfileを参照する。

ファイル・ディスクリプターの番号 参照先
1 file
2 標準出力

結果は、ファイル・ディスクリプター1⇒file、ファイル・ディスクリプター2⇒標準出力の形になっていることがわかる。

C言語で疑似的に再現

面白いので、C言語で疑似的に書いてみた(久々過ぎて忘れてた)。
# プログラムの方が好きな人は理解が早いかも知れない。

#include<stdio.h>

int main() {

    // ファイル・ディスクリプタ―1 = 標準出力
    FILE* fd1 = stdout;
    // ファイル・ディスクリプタ―2 = 標準エラー出力
    FILE* fd2 = stderr;

    // デフォルト状態
    // df1⇒標準出力、df2⇒標準エラー出力
    fprintf(fd1, "stdout_1\n");
    fprintf(fd2, "stderr_1\n");

    // リダイレクト「> file」を実施
    // ファイル・ディスクリプタ―1の参照先をfileにする。
    fd1 = fopen("./file.txt", "w");

    // df1⇒file、df2⇒標準エラー出力
    fprintf(fd1, "stdout_2\n");
    fprintf(fd2, "stderr_2\n");

    // リダイレクト「2>&1」を実施
    // ファイル・ディスクリプタ―2の参照先をファイル・ディスクリプタ―1にする。
    // ※本当はこの時点ではポインタのコピーではなく、複製を実施するべきだが疑似的な再現のため、ポインタコピー
    fd2 = fd1;

    // df1⇒file、df2⇒file
    fprintf(fd1, "stdout_3\n");
    fprintf(fd2, "stderr_3\n");

    // クローズ
    fclose(fd1);
    if (fd1 != fd2) {
        fclose(fd2);
    }

    return 0;
}
実行結果
$ gcc redirect_1.c
$ ./a.out 1> stdout.log 2> stderr.log
$ cat stdout.log
stdout_1
$ cat stderr.log
stderr_1
stderr_2
$ cat file.txt
stdout_2
stdout_3
stderr_3

ついでにちゃんと低水準入出力関数も使ってみた。
# さすがにあまり使ったことがないので、自信はないが、実現はできていると思う。

#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main() {

    // ファイル・ディスクリプター1(標準出力)
    int fd1 = 1;
    // ファイル・ディスクリプター2(標準エラー出力)
    int fd2 = 2;

    // デフォルト状態
    // df1⇒標準出力、df2⇒標準エラー出力
    fprintf(stdout, "stdout_1\n");
    fprintf(stderr, "stderr_1\n");

    // 「> file」の処理
    // fileを開く。
    int fd_tmp = open("./file.txt",  O_WRONLY | O_CREAT);
    // fileのファイル・ディスクリプター(fd_tmp)の中身をファイル・ディスクリプタ―1(fd1)へ複製する。
    dup2(fd_tmp, fd1);

    // df1⇒file、df2⇒標準エラー出力
    fprintf(stdout, "stdout_2\n");
    fprintf(stderr, "stderr_2\n");

    // リダイレクト「2>&1」を実施
    // ファイル・ディスクリプター1の中身(file)をファイル・ディスクリプタ―2(fd2)へ複製する。
    dup2(fd1, fd2);

    // df1⇒file、df2⇒file
    fprintf(stdout, "stdout_3\n");
    fprintf(stderr, "stderr_3\n");

    close(fd1);
    close(fd2);

    return 1;
}
実行結果
$ gcc redirect_2.c
$ ./a.out 1> stdout.log 2> stderr.log
$ cat stdout.log
stdout_1
$ cat stderr.log
stderr_1
stderr_2
$ cat file.txt
stdout_2
stdout_3
stderr_3

参考文献

覚えてると案外便利なBashのリダイレクト・パイプの使い方9個
標準出力と標準エラー出力を別々のファイルと画面に出力する