Bagaimana cara agar STDOUT dan STDERR pergi ke terminal dan file log?

105

Saya memiliki skrip yang akan dijalankan secara interaktif oleh pengguna non-teknis. Skrip menulis pembaruan status ke STDOUT sehingga pengguna dapat yakin bahwa skrip berjalan dengan baik.

Saya ingin STDOUT dan STDERR dialihkan ke terminal (sehingga pengguna dapat melihat bahwa skrip berfungsi serta melihat apakah ada masalah). Saya juga ingin kedua aliran dialihkan ke file log.

Saya telah melihat banyak solusi di internet. Beberapa tidak berfungsi dan yang lainnya sangat rumit. Saya telah mengembangkan solusi yang bisa diterapkan (yang akan saya masukkan sebagai jawaban), tetapi itu kludgy.

Solusi sempurna adalah satu baris kode yang dapat digabungkan ke awal skrip apa pun yang mengirimkan kedua aliran ke terminal dan file log.

EDIT: Mengalihkan STDERR ke STDOUT dan menyalurkan hasilnya ke tee berfungsi, tetapi itu tergantung pada pengguna yang mengingat untuk mengalihkan dan menyalurkan output. Saya ingin logging menjadi sangat mudah dan otomatis (itulah sebabnya saya ingin dapat menyematkan solusi ke dalam skrip itu sendiri.)

JPLemme
sumber
Untuk pembaca lain: pertanyaan serupa: stackoverflow.com/questions/692000/…
pevik
1
Saya kesal karena semua orang (termasuk saya!) Kecuali @JasonSydes tergelincir dan menjawab pertanyaan yang berbeda. Dan jawaban Jason tidak bisa diandalkan, seperti yang saya komentari. Saya ingin melihat jawaban yang benar-benar dapat diandalkan untuk pertanyaan yang Anda ajukan (dan tekankan dalam EDIT Anda).
Don Hatch
Oh tunggu, saya ambil kembali. Jawaban diterima @PaulTromblin memang menjawabnya. Saya tidak membaca cukup jauh.
Don Hatch

Jawaban:

169

Gunakan "tee" untuk mengarahkan ke file dan layar. Bergantung pada shell yang Anda gunakan, pertama-tama Anda harus mengarahkan stderr ke stdout menggunakan

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

atau

./a.out |& tee output

Di csh, ada perintah built-in yang disebut "script" yang akan menangkap semua yang masuk ke layar ke sebuah file. Anda memulainya dengan mengetik "script", lalu melakukan apa pun yang ingin Anda tangkap, lalu tekan control-D untuk menutup file skrip. Saya tidak tahu padanan untuk sh / bash / ksh.

Juga, karena Anda telah menunjukkan bahwa ini adalah skrip sh Anda sendiri yang dapat Anda modifikasi, Anda dapat melakukan pengalihan secara internal dengan mengelilingi seluruh skrip dengan tanda kurung atau tanda kurung, seperti

  #!/bin/sh
  {
    ... whatever you had in your script before
  } 2>&1 | tee output.file
Paul Tomblin
sumber
4
Saya tidak tahu Anda bisa memasukkan perintah dalam skrip shell. Menarik.
Jamie
1
Saya juga menghargai pintasan Bracket! Untuk beberapa alasan, 2>&1 | tee -a filenametidak menyimpan stderr ke file dari skrip saya, tetapi berfungsi dengan baik ketika saya menyalin perintah dan menempelkannya ke terminal! Trik braket berfungsi dengan baik.
Ed Brannin
8
Perhatikan bahwa perbedaan antara stdout dan stderr akan hilang, karena tee mencetak semuanya ke stdout.
Flimm
2
FYI: Perintah 'script' tersedia di sebagian besar distribusi (ini bagian dari paket util-linux)
SamWN
2
@Flimm, apakah ada cara (cara lain) untuk mempertahankan perbedaan antara stdout dan stderr?
Gabriel
20

Mendekati setengah dekade kemudian ...

Saya yakin ini adalah "solusi sempurna" yang dicari oleh OP.

Inilah satu liner yang dapat Anda tambahkan ke bagian atas skrip Bash Anda:

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

Berikut ini skrip kecil yang mendemonstrasikan penggunaannya:

#!/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

(Catatan: Ini hanya berfungsi dengan Bash. Ini tidak akan berfungsi dengan / bin / sh.)

Diadaptasi dari sini ; yang asli tidak, dari apa yang saya tahu, menangkap STDERR di logfile. Diperbaiki dengan catatan dari sini .

Jason Sydes
sumber
3
Perhatikan bahwa perbedaan antara stdout dan stderr akan hilang, karena tee mencetak semuanya ke stdout.
Flimm
@Flimm stderr dapat dialihkan ke proses tee berbeda yang lagi-lagi dapat dialihkan ke stderr.
jarno
@Flimm, saya menulis saran jarno di sini: stackoverflow.com/a/53051506/1054322
MatrixManAtYrService
1
Solusi ini, seperti kebanyakan solusi lain yang diusulkan sejauh ini, rawan balapan. Artinya, ketika skrip saat ini selesai dan kembali, baik ke prompt pengguna atau beberapa skrip panggilan tingkat yang lebih tinggi, tee, yang berjalan di latar belakang, akan tetap berjalan, dan mungkin memancarkan beberapa baris terakhir ke layar dan ke logfile terlambat (yaitu, ke layar setelah prompt, dan ke file log setelah logfile diharapkan selesai).
Don Hatch
1
Namun, sejauh ini hanya inilah jawaban yang diajukan yang benar-benar menjawab pertanyaan tersebut!
Don Hatch
9

Pola

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

Ini mengalihkan baik stdout dan stderr secara terpisah, dan mengirimkan salinan terpisah dari stdout dan stderr ke pemanggil (yang mungkin merupakan terminal Anda).

  • Dalam zsh, ini tidak akan dilanjutkan ke pernyataan berikutnya sampai tees selesai.

  • Di bash, Anda mungkin menemukan bahwa beberapa baris keluaran terakhir muncul setelah pernyataan apa pun yang muncul berikutnya.

Dalam kedua kasus tersebut, bit yang tepat menuju ke tempat yang tepat.


Penjelasan

Berikut skripnya (disimpan dalam ./example):

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

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

Inilah sesi:

$ foo=$(./example)
    err

$ echo $foo
    out

$ cat stdout.txt
    out

$ cat stderr.txt
    err

Begini cara kerjanya:

  1. Kedua teeproses dimulai, stdinsnya ditetapkan ke deskriptor file. Karena mereka tertutup dalam substitusi proses , jalur ke deskriptor file tersebut diganti dalam perintah pemanggil, jadi sekarang terlihat seperti ini:

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

  1. the_cmd berjalan, menulis stdout ke deskriptor file pertama, dan stderr ke deskriptor kedua.

  2. Dalam kasus bash, setelah the_cmdselesai, pernyataan berikut segera terjadi (jika terminal Anda adalah pemanggil, maka Anda akan melihat prompt Anda muncul).

  3. Dalam kasus zsh, setelah the_cmdselesai, shell menunggu kedua teeproses selesai sebelum melanjutkan. Lebih lanjut tentang ini di sini .

  4. Proses pertama tee, yang membaca dari the_cmdstdout, menulis salinan dari stdout itu kembali ke pemanggil karena itulah yang teedilakukannya. Keluarannya tidak dialihkan, sehingga membuatnya kembali ke pemanggil tanpa diubah

  5. Proses kedua teetelah stdoutdialihkan ke pemanggil stderr(yang bagus, karena stdin sedang membaca dari the_cmdstderr). Jadi ketika menulis ke stdout-nya, bit-bit itu pergi ke stderr pemanggil.

Ini membuat stderr terpisah dari stdout baik dalam file maupun dalam output perintah.

Jika tee pertama menulis kesalahan apa pun, kesalahan tersebut akan muncul di file stderr dan di stderr perintah, jika tee kedua menulis kesalahan, kesalahan tersebut hanya akan muncul di stderr terminal.

MatrixManAtYrService
sumber
Ini terlihat sangat berguna dan apa yang saya inginkan. Saya tidak yakin bagaimana cara mereplikasi penggunaan tanda kurung (seperti yang ditunjukkan pada baris pertama) di Windows Batch Script. ( teetersedia pada sistem yang dimaksud.) Kesalahan yang saya dapatkan adalah "Proses tidak dapat mengakses file karena sedang digunakan oleh proses lain."
Agi Hammerthief
Solusi ini, seperti kebanyakan solusi lain yang diusulkan sejauh ini, rawan balapan. Artinya, ketika skrip saat ini selesai dan kembali, baik ke prompt pengguna atau skrip panggilan tingkat yang lebih tinggi, tee, yang berjalan di latar belakang, akan tetap berjalan, dan mungkin memancarkan beberapa baris terakhir ke layar dan ke logfile terlambat (yaitu, ke layar setelah prompt, dan ke file log setelah logfile diharapkan selesai).
Don Hatch
2
@DonHatch Bisakah Anda mengusulkan solusi yang memperbaiki masalah ini?
pylipp
Saya juga tertarik dengan kasus uji yang membuat balapan menjadi jelas. Bukannya saya ragu, tetapi sulit untuk berusaha menghindarinya karena saya belum melihatnya terjadi.
MatrixManAtYrService
@pylipp Saya tidak punya solusi. Saya akan sangat tertarik pada salah satunya.
Don Hatch
4

untuk mengarahkan stderr ke stdout tambahkan ini di perintah Anda: 2>&1 Untuk keluaran ke terminal dan masuk ke file, Anda harus menggunakantee

Keduanya akan terlihat seperti ini:

 mycommand 2>&1 | tee mylogfile.log

EDIT: Untuk menyematkan ke skrip Anda, Anda akan melakukan hal yang sama. Jadi naskahmu

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

akan berakhir sebagai

#!/bin/sh
( whatever1
whatever2
...
whatever3 ) 2>&1 | tee mylogfile.log
flolo
sumber
2
Perhatikan bahwa perbedaan antara stdout dan stderr akan hilang, karena tee mencetak semuanya ke stdout.
Flimm
4

EDIT: Saya melihat saya tergelincir dan akhirnya menjawab pertanyaan yang berbeda dari yang ditanyakan. Jawaban atas pertanyaan sebenarnya ada di bagian bawah jawaban Paul Tomblin. (Jika Anda ingin meningkatkan solusi itu untuk mengalihkan stdout dan stderr secara terpisah karena alasan tertentu, Anda dapat menggunakan teknik yang saya jelaskan di sini.)


Saya menginginkan jawaban yang menjaga perbedaan antara stdout dan stderr. Sayangnya, semua jawaban yang diberikan sejauh ini yang mempertahankan perbedaan itu rentan terhadap ras: program berisiko melihat masukan yang tidak lengkap, seperti yang saya tunjukkan dalam komentar.

Saya pikir saya akhirnya menemukan jawaban yang mempertahankan perbedaan, tidak rawan ras, dan juga tidak terlalu fiddly.

Blok penyusun pertama: untuk menukar stdout dan stderr:

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

Blok penyusun kedua: jika kita ingin memfilter (misalnya tee) hanya stderr, kita dapat melakukannya dengan menukar stdout & stderr, memfilter, dan kemudian menukar kembali:

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

Sekarang sisanya mudah: kita bisa menambahkan filter stdout, baik di awal:

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

atau di akhir:

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

Untuk meyakinkan diri sendiri bahwa kedua perintah di atas berfungsi, saya menggunakan yang berikut ini:

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;}'

Outputnya adalah:

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

dan perintah saya muncul segera setelah " teed stderr: to stderr", seperti yang diharapkan.

Catatan kaki tentang zsh :

Solusi di atas berfungsi di bash (dan mungkin beberapa shell lain, saya tidak yakin), tetapi tidak berfungsi di zsh. Ada dua alasan gagal di zsh:

  1. sintaksnya 2>&3-tidak dipahami oleh zsh; yang harus ditulis ulang sebagai2>&3 3>&-
  2. di zsh (tidak seperti shell lain), jika Anda mengarahkan deskriptor file yang sudah terbuka, dalam beberapa kasus (saya tidak sepenuhnya memahami cara memutuskannya), ia akan melakukan perilaku seperti tee bawaan. Untuk menghindari hal ini, Anda harus menutup setiap fd sebelum mengalihkannya.

Jadi, misalnya, solusi kedua saya harus ditulis ulang untuk zsh sebagai {my_command 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stderr_filter;} 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stdout_filter(yang juga berfungsi di bash, tetapi sangat bertele-tele).

Di sisi lain, Anda dapat memanfaatkan tee implisit bawaan yang misterius dari zsh untuk mendapatkan solusi yang jauh lebih singkat untuk zsh, yang tidak menjalankan tee sama sekali:

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

(Saya tidak akan menebak dari dokumen yang saya temukan bahwa >&1dan 2>&2merupakan hal yang memicu tee implisit zsh; Saya mengetahuinya dengan trial-and-error.)

Don Hatch
sumber
Saya bermain-main dengan ini di pesta dan itu bekerja dengan baik. Hanya peringatan untuk pengguna zsh dengan kebiasaan mengasumsikan kompatibilitas (seperti saya), perilakunya berbeda di sana: gist.github.com/MatrixManAtYrService/…
MatrixManAtYrService
@MatrixManAtYrService Saya yakin saya dapat menangani situasi zsh, dan ternyata ada solusi yang jauh lebih rapi di zsh. Lihat edit saya "Catatan kaki tentang zsh".
Don Hatch
Terima kasih telah menjelaskan solusinya secara mendetail. Apakah Anda juga tahu cara mengambil kode yang dikembalikan saat menggunakan fungsi ( my_function) di pemfilteran stdout / stderr bersarang? Saya melakukannya { { my_function || touch failed;} 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3- | stdout_filtertetapi rasanya aneh membuat file sebagai indikator kegagalan ...
pylipp
@pylipp saya tidak begitu saja. Anda mungkin menanyakannya sebagai pertanyaan terpisah (mungkin dengan saluran yang lebih sederhana).
Don Hatch
2

Menggunakan script perintah dalam skrip Anda (skrip man 1)

Buat shellscript pembungkus (2 baris) yang menyiapkan script () dan kemudian memanggil exit.

Bagian 1: wrap.sh

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

Bagian 2: realscript.sh

#!/bin/sh
echo 'Output'

Hasil:

~: 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
~:
gnud
sumber
1

Gunakan program tee dan dup stderr untuk stdout.

 program 2>&1 | tee > logfile
tvanfosson.dll
sumber
1

Saya membuat skrip yang disebut "RunScript.sh". Isi dari script ini adalah:

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

Saya menyebutnya seperti ini:

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

Ini berfungsi, tetapi membutuhkan skrip aplikasi untuk dijalankan melalui skrip eksternal. Agak kludgy.

JPLemme
sumber
9
Anda akan kehilangan pengelompokan argumen yang berisi spasi kosong dengan $ 1 $ 2 $ 3 ... , Anda harus menggunakan (w / tanda kutip): "$ @"
NVRAM
1

Setahun kemudian, inilah skrip bash lama untuk mencatat apa pun. Misalnya,
teelog make ...membuat log ke nama log yang dihasilkan (dan lihat trik untuk membuat log bersarang makejuga.)

#!/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
denis
sumber
Saya tahu ini sudah terlambat untuk menambahkan komentar tetapi saya hanya harus mengucapkan terima kasih untuk skrip ini. Sangat berguna dan terdokumentasi dengan baik!
stephenmm
Terima kasih @stephenmm; itu tidak pernah terlalu terlambat untuk mengatakan "berguna" atau "dapat ditingkatkan".
denis