Hem STDOUT hem de STDERR'ın terminale ve bir günlük dosyasına gitmesini nasıl sağlayabilirim?


113

Teknik olmayan kullanıcılar tarafından etkileşimli olarak çalıştırılacak bir komut dosyam var. Komut dosyası, durum güncellemelerini STDOUT'a yazar, böylece kullanıcı komut dosyasının Tamam olarak çalıştığından emin olabilir.

Hem STDOUT hem de STDERR'ın terminale yeniden yönlendirilmesini istiyorum (böylece kullanıcı komut dosyasının çalıştığını ve bir sorun olup olmadığını görebilsin). Ayrıca her iki akışın da bir günlük dosyasına yönlendirilmesini istiyorum.

İnternette bir sürü çözüm gördüm. Bazıları çalışmıyor ve diğerleri korkunç derecede karmaşık. Çalışabilir bir çözüm geliştirdim (cevap olarak gireceğim), ama bu çok karışık.

Mükemmel çözüm, her iki akışı da hem terminale hem de bir günlük dosyasına gönderen herhangi bir komut dosyasının başlangıcına dahil edilebilecek tek bir kod satırı olacaktır.

DÜZENLEME: STDERR'yi STDOUT'a yeniden yönlendirmek ve sonucu tee işlerine borulamak, ancak kullanıcıların çıkışı yeniden yönlendirmeyi ve yönlendirmeyi hatırlamasına bağlıdır. Günlüğe kaydetmenin kusursuz ve otomatik olmasını istiyorum (bu yüzden çözümü betiğin içine yerleştirebilmeyi istiyorum.)


Diğer okuyucular için: benzer soru: stackoverflow.com/questions/692000/…
pevik

1
@ JasonSydes dışında herkesin (ben dahil!) Raydan çıkması ve farklı bir soruyu yanıtlaması beni rahatsız etti. Ve yorumladığım gibi, Jason'ın cevabı güvenilmez. Sorduğunuz (ve DÜZENLEME'nizde vurguladığınız) soruya gerçekten güvenilir bir cevap görmeyi çok isterim.
Don Hatch

Oh bekle, geri alıyorum. @PaulTromblin 'in kabul edilen cevabı buna cevap veriyor. Yeterince derinlemesine okumadım.
Don Hatch

Yanıtlar:


179

Bir dosyaya ve ekrana yeniden yönlendirmek için "tee" kullanın. Kullandığınız kabuğa bağlı olarak, önce stderr'i stdout'a yönlendirmelisiniz.

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

veya

./a.out |& tee output

Csh'de, ekrana giden her şeyi bir dosyaya yakalayacak "betik" adında yerleşik bir komut vardır. "Komut dosyası" yazıp ardından yakalamak istediğiniz her şeyi yaparak başlatın, ardından komut dosyasını kapatmak için kontrol-D'ye basın. Sh / bash / ksh için bir eşdeğer bilmiyorum.

Ayrıca, bunların değiştirebileceğiniz kendi sh komut dosyalarınız olduğunu belirttiğinizden, yeniden yönlendirmeyi tüm komut dosyasını parantez veya parantez içine alarak dahili olarak yapabilirsiniz.

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

5
Kabuk betiklerinde komutları parantezleyebileceğinizi bilmiyordum. İlginç.
Jamie

1
Ayrıca Bracket kısayolunu da takdir ediyorum! Bazı nedenlerden dolayı, 2>&1 | tee -a filenamestderr'i betiğimden dosyaya kaydetmiyordum, ancak komutu kopyalayıp terminale yapıştırdığımda iyi çalıştı! Parantez numarası yine de iyi çalışıyor.
Ed Brannin

8
Te her şeyi standart çıktıya yazdırdığından, stdout ve stderr arasındaki ayrımın kaybolacağını unutmayın.
Flimm

2
Bilginize: 'script' komutu çoğu dağıtımda mevcuttur (util-linux paketinin bir parçasıdır)
SamWN

2
@Flimm, stdout ve stderr arasındaki farkı korumanın (başka bir yolu) var mı?
Gabriel,

20

Yaklaşık yarım on yıl sonra ...

Bunun OP'nin aradığı "mükemmel çözüm" olduğuna inanıyorum.

İşte Bash betiğinizin üstüne ekleyebileceğiniz bir satır:

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

İşte kullanımını gösteren küçük bir komut dosyası:

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

(Not: Bu sadece Bash ile çalışır Olacak. Değil iş ile / bin / sh.)

Buradan uyarlanmıştır ; orijinal, anlayabildiğim kadarıyla, günlük dosyasındaki STDERR'yi yakalamadı. Buradan bir notla düzeltildi .


3
Te her şeyi standart çıktıya yazdırdığından stdout ve stderr arasındaki ayrımın kaybolacağını unutmayın.
Flimm

@Flimm stderr, tekrar stderr'e yönlendirilebilecek farklı tee işlemine yönlendirilebilir.
jarno

@Flimm, jarno'nun önerisini buraya yazdım: stackoverflow.com/a/53051506/1054322
MatrixManAtYrService

1
Bu çözüm, şimdiye kadar önerilen diğer çözümlerin çoğu gibi, yarışa yatkındır. Yani, mevcut komut dosyası tamamlandığında ve kullanıcının istemine veya daha yüksek seviyeli bir çağrı komut dosyasına geri döndüğünde, arka planda çalışan tee hala çalışıyor olacak ve ekrana ve ekrana son birkaç satırı gönderebilir. günlük dosyası geç (yani, istemden sonraki ekrana ve günlük dosyasının tamamlanması beklendikten sonra günlük dosyasına).
Don Hatch

1
Ancak, şu ana kadar önerilen ve soruyu gerçekten ele alan tek cevap budur!
Don Hatch

11

Desen

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

Bu, hem stdout hem de stderr'i ayrı ayrı yönlendirir ve arayan kişiye (sizin terminaliniz olabilir) stdout ve stderr'in ayrı kopyalarını gönderir .

  • Zsh'da, tees bitene kadar bir sonraki ifadeye geçmeyecektir .

  • Bash'de, son birkaç satırın, sonraki ifadeden sonra göründüğünü görebilirsiniz .

Her iki durumda da, doğru bitler doğru yerlere gider.


Açıklama

İşte bir komut dosyası (./example içinde saklanır):

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

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

İşte bir oturum:

$ foo=$(./example)
    err

$ echo $foo
    out

$ cat stdout.txt
    out

$ cat stderr.txt
    err

Şu şekilde çalışır:

  1. Her iki teesüreç de başlatılır, stdinleri dosya tanımlayıcılara atanır. İşlem ikameleri arasına alındıklarından , bu dosya tanımlayıcılarının yolları çağıran komutta yer değiştirmiştir, bu nedenle şimdi şuna benzer:

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

  1. the_cmd ilk dosya tanımlayıcısına stdout ve ikinciye stderr yazar.

  2. Bash durumunda, the_cmdtamamlandığında, aşağıdaki ifade hemen gerçekleşir (eğer terminaliniz arayan ise, o zaman isteminizin göründüğünü göreceksiniz).

  3. Zsh durumunda, the_cmdbittikten sonra kabuk, devam teeetmeden önce her iki işlemin de bitmesini bekler . Daha fazlası burada .

  4. Standart çıktısından teeokuyan ilk süreç, the_cmdo standart çıkışın bir kopyasını arayana geri yazar, çünkü öyle teeyapar. Çıktıları yeniden yönlendirilmez, bu nedenle onu değiştirmeden arayan kişiye geri gönderir

  5. İkinci teeişlem, stdoutarayan kişiye yeniden yönlendirilir stderr(ki bu iyidir, çünkü stdin'in stderr'inden okur the_cmd). Böylece, standart çıkışına yazdığında, bu bitler arayanın stderrına gider.

Bu, stderr'i hem dosyalarda hem de komutun çıktısında standart çıktıdan ayrı tutar.

İlk tee herhangi bir hata yazarsa, hem stderr dosyasında hem de komutun stderr'inde görünürler, ikinci tee herhangi bir hata yazarsa, yalnızca terminalin stderrinde görünürler.


Bu gerçekten kullanışlı görünüyor ve benim istediğim. Yine de, bir Windows Toplu Komut Dosyasında parantez kullanımını (ilk satırda gösterildiği gibi) nasıl çoğaltacağımdan emin değilim. ( teesöz konusu sistemde mevcut.) Aldığım hata, "İşlem dosyaya erişemiyor çünkü dosya başka bir işlem tarafından kullanılıyor."
Agi Hammerthief

Bu çözüm, şimdiye kadar önerilen diğer çözümlerin çoğu gibi, yarışa yatkındır. Yani, mevcut komut dosyası tamamlandığında ve kullanıcının istemine veya daha yüksek seviyeli bir çağrı komut dosyasına geri döndüğünde, arka planda çalışan tee hala çalışıyor olacak ve ekrana ve ekrana son birkaç satırı gönderebilir. günlük dosyası geç (yani, istemden sonraki ekrana ve günlük dosyasının tamamlanması beklendikten sonra günlük dosyasına).
Don Hatch

2
@DonHatch Bu sorunu düzelten bir çözüm önerebilir misiniz?
pylipp

Ayrıca yarışı belirgin hale getiren bir test vakasıyla da ilgilenirim. Şüpheli olduğumdan değil, ama bundan kaçınmaya çalışmak zor çünkü bunun gerçekleştiğini görmedim.
MatrixManAtYrService

@pylipp Bir çözümüm yok. Biriyle çok ilgilenirim.
Don Hatch

4

stderr'i stdout'a yeniden yönlendirmek için bunu komutunuza ekleyin: 2>&1 Terminale çıktı almak ve dosyada oturum açmak için kullanmalısınıztee

İkisi birlikte şöyle görünürdü:

 mycommand 2>&1 | tee mylogfile.log

DÜZENLEME: Senaryonuza gömmek için aynısını yaparsınız. Yani senaryonuz

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

olarak biter

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

2
Te her şeyi standart çıktıya yazdırdığından stdout ve stderr arasındaki ayrımın kaybolacağını unutmayın.
Flimm

4

DÜZENLEME: Raydan çıktığımı ve sorulandan farklı bir soruyu yanıtladığımı görüyorum. Gerçek sorunun cevabı, Paul Tomblin'in cevabının altında. (Stdout ve stderr'i herhangi bir nedenle ayrı ayrı yönlendirmek için bu çözümü geliştirmek istiyorsanız, burada anlattığım tekniği kullanabilirsiniz.)


Standart çıkış ve stderr arasındaki ayrımı koruyan bir cevap bekliyordum. Ne yazık ki, bu ayrımı koruyan şimdiye kadar verilen yanıtların tümü yarışa yatkındır: yorumlarda belirttiğim gibi, programların eksik girdi görme riskini alıyorlar.

Sanırım sonunda ayrımı koruyan, yarışa yatkın olmayan ve çok da zor olmayan bir cevap buldum.

İlk yapı taşı: stdout ve stderr'i değiştirmek için:

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

İkinci yapı bloğu: Sadece stderr'i filtrelemek isteseydik (örneğin tee), bunu stdout ve stderr'i değiştirerek, filtreleyerek ve sonra geri değiştirerek başarabilirdik:

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

Şimdi gerisi kolay: başlangıçta bir standart çıkış filtresi ekleyebiliriz:

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

veya sonunda:

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

Kendimi yukarıdaki komutların her ikisinin de işe yaradığına ikna etmek için aşağıdakileri kullandım:

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

Çıktı:

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

ve teed stderr: to stderrbeklendiği gibi " " öğesinin hemen ardından istemim geri geliyor .

Zsh hakkında dipnot :

Yukarıdaki çözüm bash'de çalışıyor (ve belki başka kabuklarda, emin değilim), ancak zsh'de çalışmıyor. Zsh'da başarısız olmasının iki nedeni vardır:

  1. sözdizimi 2>&3-zsh tarafından anlaşılmaz; yeniden yazılması gerekiyor2>&3 3>&-
  2. zsh'da (diğer kabukların aksine), zaten açık olan bir dosya tanımlayıcısını yeniden yönlendirirseniz, bazı durumlarda (nasıl karar verdiğini tam olarak anlamıyorum) bunun yerine yerleşik bir tee benzeri davranış yapar. Bundan kaçınmak için, yeniden yönlendirmeden önce her fd'yi kapatmanız gerekir.

Yani, örneğin, ikinci çözümümün zsh olarak yeniden yazılması gerekiyor {my_command 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stderr_filter;} 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stdout_filter(ki bu da bash'da çalışıyor, ancak çok ayrıntılı).

Öte yandan, hiç te çalıştırmayan zsh için çok daha kısa bir çözüm elde etmek için zsh'nin gizemli yerleşik örtülü vuruşundan yararlanabilirsiniz:

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

(Bulduğum belgelerden zsh'nin örtük vuruşunu tetikleyen şeyin >&1ve olduğunu tahmin edemezdim 2>&2; Bunu deneme yanılma yoluyla buldum.)


Bash'ta bununla oynadım ve iyi çalışıyor. Uyumluluk varsayımı alışkanlığı olan zsh kullanıcıları için sadece bir uyarı (benim gibi), orada farklı davranıyor: gist.github.com/MatrixManAtYrService/…
MatrixManAtYrService

@MatrixManAtYrService zsh durumunu kontrol ettiğime inanıyorum ve zsh'de çok daha düzgün bir çözüm olduğu ortaya çıktı. "Zsh hakkında dipnot" düzenlememe bakın.
Don Hatch

Çözümü bu kadar detaylı açıkladığınız için teşekkürler. my_functionİç içe geçmiş stdout / stderr filtrelemede bir işlev ( ) kullanırken dönüş kodunu nasıl alacağınızı da biliyor musunuz ? Ben yaptım { { my_function || touch failed;} 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3- | stdout_filterama ... başarısızlığın göstergesi olarak bir dosya oluşturmak için garip hissediyor
pylipp

@pylipp Ben hazırlıksız değilim. Bunu ayrı bir soru olarak sorabilirsiniz (belki daha basit bir boru hattıyla).
Don Hatch

2

scriptKomut dosyanızdaki komutu kullanın (adam 1 komut dosyası)

Betiği () kuran ve ardından çıkışı çağıran bir sarmalayıcı kabuk komut dosyası (2 satır) oluşturun.

Bölüm 1: wrap.sh

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

Bölüm 2: realscript.sh

#!/bin/sh
echo 'Output'

Sonuç:

~: 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

Stdout için tee programını ve çift stderr kullanın.

 program 2>&1 | tee > logfile

1

"RunScript.sh" adlı bir komut dosyası oluşturdum. Bu komut dosyasının içeriği:

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

Ben şöyle diyorum:

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

Bu çalışır, ancak uygulamanın komut dosyalarının harici bir komut dosyası aracılığıyla çalıştırılmasını gerektirir. Biraz huysuz.


9
1 $ 2 $ 3 $ ile beyaz boşluk içeren bağımsız değişken gruplamasını kaybedeceksiniz ... , kullanmalısınız (w / quotes): "$ @"
NVRAM

1

Bir yıl sonra, her şeyi günlüğe kaydetmek için eski bir bash betiği var. Örneğin,
teelog make ...oluşturulan bir günlük adına günlüğe kaydedilir (ve iç içe geçmiş makee- postaları günlüğe kaydetme hilesine de bakın.)

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

Yorum eklemek için bunun çok geç olduğunu biliyorum ama bu senaryo için teşekkür etmek zorundaydım. Çok faydalı ve iyi belgelenmiş!
stephenmm

Teşekkürler @stephenmm; öyle asla "yararlı" veya "geliştirilebilir" demek için çok geç.
denis
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.