お問い合わせはこちら
+81-80-2437-0232
hosokawa@dear-jpn.com

bashリダイレクト

とある作業の結果をメールで送りたいというのはよくある話。
ループ内でごりごりと処理を実行して、その結果をリダイレクトでメールを送るという処理をbashで書いたので備忘録。
さすがにどんな処理をしていたかってのをお見せするわけにはいかないので、適当にこんな処理を書いて標準出力やら標準エラー出力やらにたくさん出力されている奴を全部まとめてメールで送るという形で見てもらうことにしよう。

#include <stdio.h>

static void output(FILE *fp, int argc, char *argv[])
{
    if (argc > 1) {
        for (int i = 1; i < argc; i++) {
            fprintf(fp, "fd = %d, arg %d = \"%s\"\n", fileno(fp), i, argv[i]);
        }
    } else {
        fprintf(fp, "fd = %d, no arg\n", fileno(fp));
    }
}

int main(int argc, char *argv[])
{
    output(stdout, argc, argv);
    output(stderr, argc, argv);
}

まずは、リダイレクトの復習から。
コマンドからの画面への出力先となると、stdout(標準出力)とstderr(標準エラー出力)とのふたつがある。
通常の実行結果は標準出力に出力されてエラーなど処理結果とは別のものを画面に表示したい場合には標準エラー出力に出力される。
ふたつあるがゆえに、普通の結果とそうではないものとを区別してリダイレクトすることができたりする。以下 ./main と叩いているのは先程のcソースを gcc -o main main.c とやって作成したコマンドを実行していると理解して欲しい。

$ ./main a b c
fd = 1, arg 1 = "a"
fd = 1, arg 2 = "b"
fd = 1, arg 3 = "c"
fd = 2, arg 1 = "a"
fd = 2, arg 2 = "b"
fd = 2, arg 3 = "c"

単純にmainコマンドを実行するとこんな感じで画面に表示される。
標準出力を捨ててみよう。

$ ./main a b c > /dev/null
fd = 2, arg 1 = "a"
fd = 2, arg 2 = "b"
fd = 2, arg 3 = "c"

標準エラー出力に出ているものだけが残った。(fdが2と表示されていることからこちらが標準エラー出力だとわかる)
同様に今度は標準エラー出力を捨ててみる。

$ ./main a b c 2> /dev/null
fd = 1, arg 1 = "a"
fd = 1, arg 2 = "b"
fd = 1, arg 3 = "c"

今度はfdが1のもののみが表示されているので標準エラー出力が捨てられたことがわかる。
ここまでは「捨てる」ということで/dev/nullを出力先に使ったが、ファイルに出力させてみよう。

$ ./main a b c > hoge.txt
fd = 2, arg 1 = "a"
fd = 2, arg 2 = "b"
fd = 2, arg 3 = "c"
$ cat hoge.txt
fd = 1, arg 1 = "a"
fd = 1, arg 2 = "b"
fd = 1, arg 3 = "c"

というわけで、画面には標準エラーへの出力結果が、ファイルには標準出力の結果が残っていることがわかる。
処理中のエラーを画面で確認しながら、処理結果をファイルに保存していくというのがこのパターンだろう。
逆に処理結果を画面で確認しながらエラーはあとで確認するのでファイルに保存しておくというのが逆のパターン。

$ ./main a b c 2> hoge.txt
fd = 1, arg 1 = "a"
fd = 1, arg 2 = "b"
fd = 1, arg 3 = "c"
$ cat hoge.txt
fd = 2, arg 1 = "a"
fd = 2, arg 2 = "b"
fd = 2, arg 3 = "c"

となるわけだ。
これ以外に、両方の出力をファイルに保存しておいて後で確認するってときはこうなる?

$ ./main a b c > hoge.txt 2> hoge.txt
$ cat hoge.txt
fd = 1, arg 1 = "a"
fd = 1, arg 2 = "b"
fd = 1, arg 3 = "c"

最初のmainコマンドの実行で同じファイルに標準出力も標準エラー出力も同じファイルに出力してやれば良いという発想だとこんなコマンドを叩いてしまうかもしれない。
このあたりは、ファイルを同時に開いて同時に書き込んだら何が起きるかわからないという別の話になってしまうので詳細は書かないが、おかしな結果になるということだけ知っておこう。
正しくはこうだ。

$ ./main a b c > hoge.txt 2>&1
$ cat hoge.txt
fd = 2, arg 1 = "a"
fd = 2, arg 2 = "b"
fd = 2, arg 3 = "c"
fd = 1, arg 1 = "a"
fd = 1, arg 2 = "b"
fd = 1, arg 3 = "c"

たしかに両方の結果がhoge.txtに入っていることがわかるだろう。
まず最初の > hoge.txt の部分で、標準出力をhoge.txtに出力すると定義し、次に 2> の出力先を1(標準出力)にマージ(&)するということになる。
逆に書いても結果は同じだ。

$ ./main a b c 2> hoge.txt >&2
$ cat hoge.txt
fd = 2, arg 1 = "a"
fd = 2, arg 2 = "b"
fd = 2, arg 3 = "c"
fd = 1, arg 1 = "a"
fd = 1, arg 2 = "b"
fd = 1, arg 3 = "c"

最初の 2> で、標準エラー出力をhoge.txtに出力すると定義し、次に >&2 で出力先を2(標準エラー出力)にマージするという意味だ。
このあたりは「マージ」と書いたあたりがunix系のdupシステムコールだとわかっている人には素直に理解できる話になってしまうのだが今どきこんな話をすると「おじいちゃんの話は古すぎる」とか言われちゃうのでやめておく。

次はパイプの話。
パイプもリダイレクトの話が理解できていれば話は早い。
mainの出力結果をsed使って書き換えてみよう。

$ ./main a b c | sed -e 's/fd = \([12]\), /replace fd = \1, /'
fd = 2, arg 1 = "a"
fd = 2, arg 2 = "b"
fd = 2, arg 3 = "c"
replace fd = 1, arg 1 = "a"
replace fd = 1, arg 2 = "b"
replace fd = 1, arg 3 = "c"

標準出力の側だけが書き換えられていることがわかるだろう。つまりはパイプした先というのはあくまでも標準出力なのであって、標準エラー出力は書き換えの対象ではないというわけだ。
標準エラー出力も対象として書き換えたい場合はどうするか?
さっきの話を理解しているならば、標準エラー出力を標準エラーにマージ(dup)してしまえば良いということになる。

$ ./main a b c 2>&1 | sed -e 's/fd = \([12]\), /replace fd = \1, /'
replace fd = 2, arg 1 = "a"
replace fd = 2, arg 2 = "b"
replace fd = 2, arg 3 = "c"
replace fd = 1, arg 1 = "a"
replace fd = 1, arg 2 = "b"
replace fd = 1, arg 3 = "c"

というわけだ。
2>&1をsedの後ろに書いても良さそうに思えるかもしれないがこれはうまく行かない。
なぜなら、sedのうしろに書いてしまうとsedの標準エラー出力を標準出力にマージするという意味になってしまうので、そもそもsedの処理対象にはなりえないわけだ。

$ ./main a b c | sed -e 's/fd = \([12]\), /replace fd = \1, /' 2>&1
fd = 2, arg 1 = "a"
fd = 2, arg 2 = "b"
fd = 2, arg 3 = "c"
replace fd = 1, arg 1 = "a"
replace fd = 1, arg 2 = "b"
replace fd = 1, arg 3 = "c"

てなわけで、ずいぶんと長くなってしまったが、ここまでがリダイレクトの復習。
今回のメインの話であるごりごりと処理した結果をメールで飛ばすという話に戻ろう。
まずは、ごりごりと処理した、、、という部分をお見せできる形でコマンド化しよう。

#!/bin/bash
echo "Start @"`date +"%Y-%m-%d %H:%M:%S"`
echo "in mail"
for i in `seq 3`
do
    ./main $i
    echo "他のコマンドの結果代わり"
    sleep 3
done
echo "also in mail"
echo "Done @"`date +"%Y-%m-%d %H:%M:%S"`

こいつをhoge.shとして保存して試していくことにしよう。
まずは、最後のDoneという出力も含めて標準エラーも標準エラー出力もパイプでつないでメールしちゃえば良いということであれば話は簡単。単純にhoge.shの出力をエラー含めてパイプしてしまえば良い。

$ ./hoge.sh 2>&1 | mail -s "Output of hoge.sh" hosokawa@dear-jpn.com

これで、本文がこんなメールが飛んでくる。

Start @2017-02-18 12:44:53
in mail
fd = 2, arg 1 = "1"
fd = 1, arg 1 = "1"
他のコマンドの結果代わり
fd = 2, arg 1 = "2"
fd = 1, arg 1 = "2"
他のコマンドの結果代わり
fd = 2, arg 1 = "3"
fd = 1, arg 1 = "3"
他のコマンドの結果代わり
also in mail
Done @2017-02-18 12:45:02

で、今回の場合はごりごりやっている一部はこちらのメールに飛ばし、他の部分はそっちのメールアドレスに飛ばし、、、ということでコマンド内全ての出力結果が欲しかったわけではないのだ。
そんなわけで、必要な部分だけ(今回であれば、echo “in mail”としているところからecho “also in mail”としているところまで)をひとつのコマンドとして認識させてメールを飛ばしてやればよい。
書き換えたhoge.shはこちら。

#!/bin/bash
echo "Start @"`date +"%Y-%m-%d %H:%M:%S"`
(
echo "in mail"
for i in `seq 3`
do
    ./main $i
    echo "他のコマンドの結果代わり"
    sleep 3
done
echo "also in mail"
) 2>&1 | mail -s "Output inner commands" hosokawa@dear-jpn.com
echo "Done @"`date +"%Y-%m-%d %H:%M:%S"`

かっこで囲んで、その最後にエラー出力を含めたパイプ処理が追加になっていることがわかるだろう。
これで、以下のコマンドを実行するとふたつのメールが飛んでくる。

$ ./hoge.sh 2>&1 | mail -s "Output of hoge.sh" hosokawa@dear-jpn.com

タイトルが「Output of hoge.sh」のメールの本文は以下のとおり。

Start @2017-02-18 12:48:22
Done @2017-02-18 12:48:31

内部のみのメールはタイトルが「Output inner commands」で本文はこうなっている。

in mail
fd = 2, arg 1 = "1"
fd = 1, arg 1 = "1"
他のコマンドの結果代わり
fd = 2, arg 1 = "2"
fd = 1, arg 1 = "2"
他のコマンドの結果代わり
fd = 2, arg 1 = "3"
fd = 1, arg 1 = "3"
他のコマンドの結果代わり
also in mail

うまいこと、内部と外部とが分離できたという話だ。

最後におまけ。
気がついた人は気がついたと思うが、標準出力と標準エラー出力とをマージした結果をcatしたものと、単純にmainコマンドを叩いたものとの結果が微妙に異なっている(マージした結果は標準エラー出力が先に出力されてしまっている)のだ。
これは、FILE *を使用する場合はバッファリングが効いてしまうことが関係している。ここも「おじいちゃんの話」になってしまうので詳細は書かないが調べてみるとおもしろい部分だ。
今回は書かないが気が向いたら書くことにしよう。