web-dev-qa-db-ja.com

stderrとstdoutを別々にbashスクリプトの関数にリダイレクトする

Cronjobの出力をjournaldに記録する ラッパースクリプト に取り組んでいます。

私にはいくつかの目標があります:

  • stderrとstdoutをさまざまな優先度レベルと接頭辞タグでjournaldに記録する必要があります
  • また、ラップされたコマンドのstderrをラッパースクリプトのstderrに出力する必要があります(cronによってキャッチされ、アラートで電子メールで送信されます)
  • 全体の行順序を維持する必要があります

これまでのところ、2つの問題があるようです。

  1. Stderrとstdoutを別々にロギング関数にリダイレクトする方法がわかりません。これまでに試みたすべてのことは、NULL以外のものをリダイレクトすることに失敗します。私の弁護では、リダイレクトは私の長所の1つではありません。 (解決済み、コメントを参照)
  2. 私の現在のアプローチ{ $_EXEC $_ARGS 2>&1 1>&7 7>&- | journaldlog error; } 7>&1 1>&2 | journaldlog infoは、ロギング関数を正確に2回実行するように見えます。1回はstderrに対して、もう1回はstdoutに対してです。これは行の順序を保持しません。私はむしろ、出力行ごとに1回関数を実行します。

私はこれをbashでやめることをあきらめて、代わりにPerlを試すべきですか?私のPerlの経験が戻ってくると確信しています...最終的には。

私は多くの異なるアプローチを試しました。そのほとんどはオンラインで、特にスタック交換で見つかりました。正直言って、他に何を試したか思い出せない...現時点ではぼやけているだけだ。また、bash docsもあまり役に立ちませんでした。

5
Cliff Armstrong

リダイレクトで直接 プロセス置換 を使用できます:

gen > >(one) 2> >(two)

これにより、genの標準出力がoneへの標準入力として、genの標準エラーがtwoへの入力として得られます。それらは実行可能なコマンドまたは関数である可能性があります。ここに私が使用した機能があります:

one() {
    while read line
    do
        echo $'\e[31m'"$line"$'\e[22m'
    done
}

two() {
    while read line
    do
        echo $'\e[32m'"$line"$'\e[22m'
        echo "$line" >&2
    done
}

それらは入力ラインをそれぞれ赤と緑で出力し、twoも通常の標準エラーに書き込みます。ループ内でやりたいことは何でもできますし、入力を別のプログラムや必要なものに送ることもできます。


この時点で「行の順序を維持する」などのことは実際にはないことに注意してください-それらは2つの別々のストリームであり、別々のプロセスです。また、スケジューラが1つのプロセスをしばらく実行してから、他のプロセスを実行し、データが読み取られるまでカーネルパイプバッファにデータが保持される可能性があります。私は、stdoutとstderrにテストする奇数を出力する関数を使用してきましたが、それぞれから数十行を連続して取得するのが一般的です。

ターミナルに表示される順序に近いものを取得することは可能ですが、おそらくBashにはありません。 pipe および select を使用するCプログラムは、順序付けを再構築できます(同じ理由で、表示されたものと同じであるとは限りません) :プロセスはしばらくスケジュールされない可能性があり、バックログがあると、最初に何が発生したのかわからない1)。順序が非常に重要な場合は、別のアプローチが必要であり、おそらくベースの実行可能ファイルの連携が必要です。それが難しい要件である場合は、再検討する必要があるかもしれません。

1パイプのバッファリングを制御するためのプラットフォーム固有の方法もあるかもしれませんが、それらがあなたを十分に助けることはほとんどありません。 Linuxでは、バッファサイズをシステムページサイズと同じくらい小さくすることができますが、それより小さくすることはできません。私はあなたがすぐに書き込みを強制する(またはそれらが読み取られるまでブロックする)ことができるものを知りません。いずれにせよ、それらはstdioストリームの方法でソース側でバッファリングされる可能性があり、その場合、順序が表示されることはありません。

6
Michael Homer

これは、異なるコマンドパイプラインにstdoutおよびstderrをキャプチャする1つの方法です。

#!/bin/bash
#
exec 3>&1 4>&2

loggerForStdout() {
    # FD 3 is the original stdout
    local x
    while IFS= read -r x; do printf "STDOUT\t%s\n" "$x"; done >&3
}
loggerForStderr() {
    # FD 4 is the original stderr
    local x
    while IFS= read -r x; do printf "STDERR\t%s\n" "$x"; done >&4
}


( (
    # This is where your command would be put
    # e.g. stdbuf -oL -eL rsync -a /from /to
    #
    echo this is stdout; echo this is stderr >&2

) | loggerForStdout ) 2>&1 | loggerForStderr

stdbuf -oL -eLを使用しても、インターリーブされた行の順序を保証できるかどうかは、完全にはわかりません。

別のロガーがある場合は、最初の3行を省略できます。これらは、コードが完全な自己完結型の例になるようにのみ存在します。

2
roaima

Stderrとstdoutを個別に関数にリダイレクトするために使用しているものは次のとおりです。

tag() { awk '$0="['$1'] "$0; fflush();' ; }

{
    {
        echo std1; sleep 0.1;
        echo error2 >&2; sleep 0.1;
        echo error3 >&2; sleep 0.1;
        echo std4; sleep 0.1;
    } 2>&1 1>&3 | tag STDERR 1>&2 ;
} 3>&1 | tag STDOUT 3>&- ;

これにより、次の出力が得られます。

[STDOUT] std1
[STDERR] error2
[STDERR] error3
[STDOUT] std4

Bashとksh(93u +)で動作します。

その場合、行の順序を維持するために、コマンドの間に「sleep 0.1」を追加していることに注意してください。