STDOUTとSTDERRの両方をターミナルとログファイルに移動するにはどうすればよいですか?


103

技術者以外のユーザーがインタラクティブに実行するスクリプトがあります。スクリプトは、ユーザーがスクリプトが正常に実行されていることを確認できるように、ステータスの更新をSTDOUTに書き込みます。

STDOUTとSTDERRの両方をターミナルにリダイレクトしてください(ユーザーがスクリプトが機能していることを確認できるようにしたり、問題があったかどうかを確認したりできます)。また、両方のストリームをログファイルにリダイレクトする必要があります。

私はネットでたくさんの解決策を見てきました。機能しないものもあれば、恐ろしく複雑なものもあります。私は実行可能なソリューションを開発しました(答えとして入力します)が、それは扱いにくいです。

完璧な解決策は、両方のストリームをターミナルとログファイルの両方に送信するスクリプトの先頭に組み込むことができる1行のコードです。

編集: STDERRをSTDOUTにリダイレクトし、結果をT型にパイプすることは機能しますが、出力をリダイレクトしてパイプすることを覚えているユーザーに依存します。ロギングを誰にでもできる自動化したいのです(そのため、スクリプト自体にソリューションを埋め込むことができます。)


:同様の質問:他の読者のためにstackoverflow.com/questions/692000/...
pevik

1
@JasonSydesを除くすべての人(私を含む!)が脱線して別の質問に回答したのはうれしいです。そして、私がコメントしたように、ジェイソンの答えは信頼できません。私はあなたが尋ねた(そしてあなたの編集で強調された)質問に対する本当に信頼できる答えを見たいです。
Don Hatch、

待って、私はそれを取り戻します。@PaulTromblinの受け入れられた答えはそれを答えます。私はそれを十分に読みませんでした。
ドンハッチ

回答:


166

「tee」を使用して、ファイルと画面にリダイレクトします。使用するシェルに応じて、最初にstderrをstdoutにリダイレクトする必要があります

./a.out 2>&1 | tee output

または

./a.out |& tee output

cshには、「スクリプト」と呼ばれる組み込みコマンドがあり、画面に移動するすべてのものをファイルにキャプチャします。「script」と入力して開始し、キャプチャしたいことをすべて実行してから、control-Dを押してスクリプトファイルを閉じます。sh / bash / kshに相当するものを知りません。

また、これらは変更可能な独自のshスクリプトであることを示したので、スクリプト全体を中かっこまたは角かっこで囲むことにより、内部でリダイレクトを行うことができます。

  #!/bin/sh
  {
    ... whatever you had in your script before
  } 2>&1 | tee output.file

4
シェルスクリプトでコマンドをブラケットできるかどうかは知りませんでした。面白い。
ジェイミー

1
ブラケットショートカットにも感謝しています。何らかの理由で2>&1 | tee -a filename、スクリプトからstderrをファイルに保存していませんでしたが、コマンドをコピーしてターミナルに貼り付けたときに問題なく動作しました。ただし、ブラケットトリックは正常に機能します。
Ed Brannin 2009

8
teeはすべてをstdoutに出力するため、stdoutとstderrの区別が失われることに注意してください。
Flimm

2
参考: 'script'コマンドはほとんどのディストリビューションで利用可能です(util-linuxパッケージの一部です)
SamWN

2
@ Flimm、stdoutとstderrの違いを保持する方法(他の方法)はありますか?
ガブリエル

20

半年後に迫っています...

これはOPが求める「完璧なソリューション」だと思います。

Bashスクリプトの先頭に追加できるライナーは次のとおりです。

exec > >(tee -a $HOME/logfile) 2>&1

次に、その使用方法を示す小さなスクリプトを示します。

#!/usr/bin/env bash

exec > >(tee -a $HOME/logfile) 2>&1

# Test redirection of STDOUT
echo test_stdout

# Test redirection of STDERR
ls test_stderr___this_file_does_not_exist

(注:これはBashでのみ機能します。/bin/sh では機能しません。)

ここから改作。オリジナルは、私が知ることができることから、ログファイルでSTDERRをキャッチしませんでした。ここからのメモで修正。


3
teeはすべてをstdoutに出力するため、stdoutとstderrの区別が失われることに注意してください。
Flimm

@Flimm stderrは別のT字プロセスにリダイレクトされ、再びstderrにリダイレクトされる可能性があります。
jarno 2017

@ Flimm、jarnoの提案をここに書きました:stackoverflow.com/a/53051506/1054322
MatrixManAtYrService

1
このソリューションは、これまでに提案されている他のほとんどのソリューションと同様に、競合が発生しやすいものです。つまり、現在のスクリプトが完了してユーザーのプロンプトまたはいくつかの高レベルの呼び出しスクリプトに戻ったとき、バックグラウンドで実行されているT型は引き続き実行され、画面に最後の数行を出力して、ログファイルの遅延(つまり、プロンプトの後の画面、およびログファイルの完了が予想される後のログファイル)。
ドンハッチ

1
しかし、これがこれまでに提案された唯一の答えであり、実際に問題に対処しています!
Don Hatch、

9

パターン

the_cmd 1> >(tee stdout.txt ) 2> >(tee stderr.txt >&2 )

これにより、stdoutとstderrの両方が別々にリダイレクトされ、stdoutとstderrの別々のコピーが呼び出し元(端末である可能性があります)に送信されます。

  • zshでは、teesが終了するまで次のステートメントに進みません。

  • bashでは、出力の最後の数行が、次のステートメントの後に表示されることがあります。

どちらの場合でも、適切なビットは適切な場所に移動します。


説明

これがスクリプトです(./exampleに保存されています):

#! /usr/bin/env bash
the_cmd()
{
    echo out;
    1>&2 echo err;
}

the_cmd 1> >(tee stdout.txt ) 2> >(tee stderr.txt >&2 )

ここにセッションがあります:

$ foo=$(./example)
    err

$ echo $foo
    out

$ cat stdout.txt
    out

$ cat stderr.txt
    err

仕組みは次のとおりです。

  1. 両方のteeプロセスが開始され、それらのstdinがファイル記述子に割り当てられます。それらはプロセス置換で囲まれているため、これらのファイル記述子へのパスは呼び出しコマンドで置換されているため、次のようになります。

the_cmd 1> /proc/self/fd/13 2> /proc/self/fd/14

  1. the_cmd stdoutを最初のファイル記述子に書き込み、stderrを2番目のファイル記述子に書き込みます。

  2. bashの場合、the_cmd終了すると、次のステートメントがすぐに実行されます(端末が呼び出し元の場合は、プロンプトが表示されます)。

  3. zshの場合、いったんthe_cmd終了すると、シェルは次に進むtee前に両方のプロセスが終了するのを待ちます。詳細はこちら

  4. のstdout teeから読み取る最初のプロセスは、the_cmdそのstdoutのコピーを呼び出し元に書き込みますtee。その出力はリダイレクトされないため、変更されずに呼び出し元に戻されます。

  5. 2番目のteeプロセスはstdout、呼び出し元にリダイレクトされますstderr(これは、stdinがthe_cmdのstderr から読み取っているので、これで十分です)。したがって、stdoutに書き込むとき、これらのビットは呼び出し元のstderrに送られます。

これにより、ファイルとコマンドの出力の両方でstderrがstdoutから分離されます。

最初のティーがエラーを書き込んだ場合、それらはstderrファイルとコマンドのstderrの両方に表示されます。2番目のティーがエラーを書き込んだ場合、それらは端末のstderrにのみ表示されます。


これは本当に便利に見え、私が欲しいものです。ただし、Windowsバッチスクリプトでブラケット(最初の行に示されている)の使用を複製する方法がわかりません。(tee問題のシステムで利用可能です。)私が取得するエラーは、「ファイルが別のプロセスによって使用されているため、プロセスがファイルにアクセスできません」です。
Agi Hammerthief

このソリューションは、これまでに提案されている他のほとんどのソリューションと同様に、競合が発生しやすいものです。つまり、現在のスクリプトが完了してユーザーのプロンプトまたはいくつかの高レベルの呼び出しスクリプトに戻ったとき、バックグラウンドで実行されているT型は引き続き実行され、画面に最後の数行を出力して、ログファイルの遅延(つまり、プロンプトの後の画面、およびログファイルの完了が予想される後のログファイル)。
Don Hatch、

2
@DonHatchこの問題を修正するソリューションを提案できますか?
フィリップ

レースを明確にするテストケースにも興味があります。疑わしいというわけではありませんが、実際に起きたことはないので避けようとはしません。
MatrixManAtYrService

@pylipp私には解決策がありません。私は次のようになり非常に 1に興味を持って。
Don Hatch、

4

stderrをstdoutにリダイレクトするには、これをコマンドに追加します。2>&1 ターミナルへの出力とファイルへのログインには、tee

両方を合わせると次のようになります。

 mycommand 2>&1 | tee mylogfile.log

編集:スクリプトに埋め込む場合も、同じようにします。だからあなたのスクリプト

#!/bin/sh
whatever1
whatever2
...
whatever3

最終的に

#!/bin/sh
( whatever1
whatever2
...
whatever3 ) 2>&1 | tee mylogfile.log

2
teeはすべてをstdoutに出力するため、stdoutとstderrの区別が失われることに注意してください。
Flimm

4

編集:私は脱線し、尋ねられたものとは異なる質問に答えてしまったようです 本当の質問に対する答えは、ポール・トンブリンの答えの一番下にあります。(何らかの理由でstdoutとstderrを別々にリダイレクトするようにソリューションを拡張したい場合は、ここで説明する手法を使用できます。)


stdoutとstderrの違いを維持する答えを求めていました。残念ながら、その区別を維持するためにこれまでに与えられたすべての回答は、競合が発生しやすくなります。私がコメントで指摘したように、プログラムは不完全な入力を参照するリスクがあります。

私はようやく、その区別を維持し、人種が発生しにくく、ひどく面倒でもない答えを見つけたと思います。

最初の構成要素:stdoutとstderrを交換するには:

my_command 3>&1 1>&2 2>&3-

2番目の構成要素:stderrのみをフィルタリング(たとえば、tシャツ)したい場合は、stdout&stderrを交換し、フィルタリングしてから、元に戻すことで実現できます。

{ my_command 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3-

あとは簡単です。最初にstdoutフィルターを追加できます。

{ { my_command | stdout_filter;} 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3-

または最後に:

{ my_command 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3- | stdout_filter

上記のコマンドが両方とも機能することを納得させるために、私は次のコマンドを使用しました。

alias my_command='{ echo "to stdout"; echo "to stderr" >&2;}'
alias stdout_filter='{ sleep 1; sed -u "s/^/teed stdout: /" | tee stdout.txt;}'
alias stderr_filter='{ sleep 2; sed -u "s/^/teed stderr: /" | tee stderr.txt;}'

出力は次のとおりです。

...(1 second pause)...
teed stdout: to stdout
...(another 1 second pause)...
teed stderr: to stderr

teed stderr: to stderr期待どおり、プロンプトが「」の直後に戻ってきます。

zshに関する脚注

上記の解決策はbashで機能します(おそらく他のいくつかのシェル、私にはわかりません)が、zshでは機能しません。zshで失敗する理由は2つあります。

  1. 構文2>&3-はzshによって理解されません。次のように書き換える必要があります2>&3 3>&-
  2. zshでは(他のシェルとは異なり)、すでに開いているファイル記述子をリダイレクトすると、場合によっては(それがどのように決定するのか完全に理解できません)、代わりに組み込みのTシャツのような動作をします。これを回避するには、リダイレクトする前に各fdを閉じる必要があります。

したがって、たとえば、私の2番目のソリューションは、zshのように書き直す必要があります{my_command 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stderr_filter;} 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stdout_filter(これはbashでも機能しますが、非常に冗長です)。

一方、zshの不可解な組み込み暗黙的ティーイングを利用して、ティーをまったく実行しないzshのはるかに短いソリューションを取得できます。

my_command >&1 >stdout.txt 2>&2 2>stderr.txt

(私は私がことがわかったドキュメントから推測しなかったであろう>&12>&2、トリガーのzshの暗黙のティーイングという事であり、私は試行錯誤によってそのアウトを発見しました)


私はbashでこれをいじってみましたが、うまくいきます。(私のような)との互換性を想定しての習慣とzshのユーザーのための単なる警告、それはそこに異なった動作:gist.github.com/MatrixManAtYrService/...
MatrixManAtYrService

@MatrixManAtYrService zshの状況を把握できたと思いますが、zshにはもっとすてきな解決策があることがわかりました。私の編集「zshに関する脚注」を参照してください。
ドンハッチ

そのような詳細で解決策を説明してくれてありがとう。my_functionネストされたstdout / stderrフィルタリングで関数()を使用するときに戻りコードを取得する方法も知っていますか?私はそうしました{ { my_function || touch failed;} 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3- | stdout_filterが、失敗の指標としてファイルを作成するのは奇妙に感じます...
pylipp

@pylipp私は手放しません。別の質問として(おそらく、より単純なパイプラインで)質問することもできます。
ドンハッチ

2

scriptスクリプトでコマンドを使用する(man 1スクリプト)

script()を設定してからexitを呼び出すラッパーシェルスクリプト(2行)を作成します。

パート1:wrap.sh

#!/bin/sh
script -c './realscript.sh'
exit

パート2:realscript.sh

#!/bin/sh
echo 'Output'

結果:

~: sh wrap.sh 
Script started, file is typescript
Output
Script done, file is typescript
~: cat typescript 
Script started on fr. 12. des. 2008 kl. 18.07 +0100
Output

Script done on fr. 12. des. 2008 kl. 18.07 +0100
~:

1

teeプログラムとdup stderrを使用してstdoutを実行します。

 program 2>&1 | tee > logfile

1

「RunScript.sh」というスクリプトを作成しました。このスクリプトの内容は次のとおりです。

${APP_HOME}/${1}.sh ${2} ${3} ${4} ${5} ${6} 2>&1 | tee -a ${APP_HOME}/${1}.log

私はそれを次のように呼びます:

./RunScript.sh ScriptToRun Param1 Param2 Param3 ...

これは機能しますが、アプリケーションのスクリプトを外部スクリプト経由で実行する必要があります。それは少し賢いです。


9
$ 1 $ 2 $ 3 ...の空白を含む引数のグループ化は失われます。(引用符付き)を使用する必要があります: "$ @"
NVRAM

1

1年後、何かをログに記録するための古いbashスクリプトを次に示します。たとえば
teelog make ...、生成されたログ名にログを記録します(ネストされたをログに記録するためのトリックも参照してくださいmake。)

#!/bin/bash
me=teelog
Version="2008-10-9 oct denis-bz"

Help() {
cat <<!

    $me anycommand args ...

logs the output of "anycommand ..." as well as displaying it on the screen,
by running
    anycommand args ... 2>&1 | tee `day`-command-args.log

That is, stdout and stderr go to both the screen, and to a log file.
(The Unix "tee" command is named after "T" pipe fittings, 1 in -> 2 out;
see http://en.wikipedia.org/wiki/Tee_(command) ).

The default log file name is made up from "command" and all the "args":
    $me cmd -opt dir/file  logs to `day`-cmd--opt-file.log .
To log to xx.log instead, either export log=xx.log or
    $me log=xx.log cmd ...
If "logdir" is set, logs are put in that directory, which must exist.
An old xx.log is moved to /tmp/\$USER-xx.log .

The log file has a header like
    # from: command args ...
    # run: date pwd etc.
to show what was run; see "From" in this file.

Called as "Log" (ln -s $me Log), Log anycommand ... logs to a file:
    command args ... > `day`-command-args.log
and tees stderr to both the log file and the terminal -- bash only.

Some commands that prompt for input from the console, such as a password,
don't prompt if they "| tee"; you can only type ahead, carefully.

To log all "make" s, including nested ones like
    cd dir1; \$(MAKE)
    cd dir2; \$(MAKE)
    ...
export MAKE="$me make"

!
  # See also: output logging in screen(1).
    exit 1
}


#-------------------------------------------------------------------------------
# bzutil.sh  denisbz may2008 --

day() {  # 30mar, 3mar
    /bin/date +%e%h  |  tr '[A-Z]' '[a-z]'  |  tr -d ' '
}

edate() {  # 19 May 2008 15:56
    echo `/bin/date "+%e %h %Y %H:%M"`
}

From() {  # header  # from: $*  # run: date pwd ...
    case `uname` in Darwin )
        mac=" mac `sw_vers -productVersion`"
    esac
    cut -c -200 <<!
${comment-#} from: $@
${comment-#} run: `edate`  in $PWD `uname -n` $mac `arch` 

!
    # mac $PWD is pwd -L not -P real
}

    # log name: day-args*.log, change this if you like --
logfilename() {
    log=`day`
    [[ $1 == "sudo" ]]  &&  shift
    for arg
    do
        log="$log-${arg##*/}"  # basename
        (( ${#log} >= 100 ))  &&  break  # max len 100
    done
            # no blanks etc in logfilename please, tr them to "-"
    echo $logdir/` echo "$log".log  |  tr -C '.:+=[:alnum:]_\n' - `
}

#-------------------------------------------------------------------------------
case "$1" in
-v* | --v* )
    echo "$0 version: $Version"
    exit 1 ;;
"" | -* )
    Help
esac

    # scan log= etc --
while [[ $1 == [a-zA-Z_]*=* ]]; do
    export "$1"
    shift
done

: ${logdir=.}
[[ -w $logdir ]] || {
    echo >&2 "error: $me: can't write in logdir $logdir"
    exit 1
    }
: ${log=` logfilename "$@" `}
[[ -f $log ]]  &&
    /bin/mv "$log" "/tmp/$USER-${log##*/}"


case ${0##*/} in  # basename
log | Log )  # both to log, stderr to caller's stderr too --
{
    From "$@"
    "$@"
} > $log  2> >(tee /dev/stderr)  # bash only
    # see http://wooledge.org:8000/BashFAQ 47, stderr to a pipe
;;

* )
#-------------------------------------------------------------------------------
{
    From "$@"  # header: from ... date pwd etc.

    "$@"  2>&1  # run the cmd with stderr and stdout both to the log

} | tee $log
    # mac tee buffers stdout ?

esac

コメントを追加するのが遅いのはわかっていますが、このスクリプトに感謝の意を表しただけです。非常に有用で、十分に文書化されています!
stephenmm

@stephenmmに感謝します。それはません決して「役に立つ」または「改善することができた」と言って遅すぎます。
denis
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.