Como faço para que STDOUT e STDERR acessem o terminal e um arquivo de log?


104

Eu tenho um script que será executado interativamente por usuários não técnicos. O script grava atualizações de status em STDOUT para que o usuário possa ter certeza de que o script está funcionando bem.

Desejo STDOUT e STDERR redirecionados para o terminal (para que o usuário possa ver se o script está funcionando e também se houve um problema). Também quero que os dois fluxos sejam redirecionados para um arquivo de log.

Eu vi um monte de soluções na rede. Alguns não funcionam e outros são terrivelmente complicados. Desenvolvi uma solução viável (que entrarei como uma resposta), mas é confusa.

A solução perfeita seria uma única linha de código que pudesse ser incorporada no início de qualquer script que envie ambos os fluxos para o terminal e um arquivo de log.

EDIT: Redirecionando STDERR para STDOUT e canalizando o resultado para trabalhos em T, mas depende dos usuários se lembrarem de redirecionar e canalizar a saída. Quero que o registro seja à prova de idiotas e automático (é por isso que gostaria de poder incorporar a solução ao próprio script).


Para outros leitores: pergunta semelhante: stackoverflow.com/questions/692000/…
pevik

1
Estou aborrecido porque todos (inclusive eu!), Exceto @JasonSydes, descarrilaram e responderam uma pergunta diferente. E a resposta de Jason não é confiável, como comentei. Eu adoraria ver uma resposta realmente confiável para a pergunta que você fez (e enfatizou em sua edição).
Don Hatch

Oh, espere, eu retiro. A resposta aceita de @PaulTromblin responde. Eu não li o suficiente sobre isso.
Don Hatch

Respostas:


167

Use "tee" para redirecionar para um arquivo e a tela. Dependendo do shell que você usa, você primeiro deve redirecionar stderr para stdout usando

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

ou

./a.out |& tee output

No csh, existe um comando embutido chamado "script" que irá capturar tudo que vai para a tela para um arquivo. Você o inicia digitando "script", fazendo tudo o que deseja capturar e, em seguida, pressiona control-D para fechar o arquivo de script. Não sei de um equivalente para sh / bash / ksh.

Além disso, como você indicou que esses são seus próprios scripts sh que podem ser modificados, você pode fazer o redirecionamento internamente circundando todo o script com colchetes ou colchetes, como

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

4
Eu não sabia que você podia colocar comandos entre colchetes nos scripts de shell. Interessante.
Jamie,

1
Também aprecio o atalho do suporte! Por algum motivo, 2>&1 | tee -a filenamenão estava salvando stderr no arquivo do meu script, mas funcionou bem quando copiei o comando e colei no terminal! O truque do suporte funciona bem, no entanto.
Ed Brannin

8
Observe que a distinção entre stdout e stderr será perdida, pois tee imprime tudo em stdout.
Flimm de

2
Para sua informação: O comando 'script' está disponível na maioria das distribuições (é parte do pacote util-linux)
SamWN

2
@Flimm, existe uma maneira (qualquer outra maneira) de manter a distinção entre stdout e stderr?
Gabriel

20

Quase meia década depois ...

Acredito que essa seja a "solução perfeita" buscada pelo OP.

Aqui está um liner que você pode adicionar ao topo de seu script Bash:

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

Aqui está um pequeno script demonstrando seu uso:

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

(Nota: Isto só funciona com Bash ele vai. Não trabalho com / bin / sh.)

Adaptado daqui ; o original não capturou, pelo que posso dizer, STDERR no arquivo de log. Corrigido com uma nota daqui .


3
Observe que a distinção entre stdout e stderr será perdida, pois tee imprime tudo em stdout.
Flimm de

@Flimm stderr pode ser redirecionado para um processo tee diferente, que novamente pode ser redirecionado para stderr.
jarno

@Flimm, escrevi a sugestão de jarno aqui: stackoverflow.com/a/53051506/1054322
MatrixManAtYrService

1
Esta solução, como a maioria das outras soluções propostas até agora, é propensa a corrida. Ou seja, quando o script atual for concluído e retornar ao prompt do usuário ou a algum script de chamada de nível superior, o tee, que está sendo executado em segundo plano, ainda estará em execução e poderá emitir as últimas linhas para a tela e para o arquivo de log atrasado (ou seja, para a tela após o prompt e para o arquivo de log depois que o arquivo de log deve estar completo).
Don Hatch,

1
No entanto, esta é a única resposta proposta até agora que realmente aborda a questão!
Don Hatch

9

O padrão

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

Isso redireciona stdout e stderr separadamente e envia cópias separadas de stdout e stderr para o chamador (que pode ser o seu terminal).

  • No zsh, ele não irá para a próxima instrução até que o tees tenha terminado.

  • No bash, você pode descobrir que as poucas linhas finais da saída aparecem após qualquer instrução que vier a seguir.

Em ambos os casos, os bits certos vão para os lugares certos.


Explicação

Aqui está um script (armazenado em ./example):

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

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

Aqui está uma sessão:

$ foo=$(./example)
    err

$ echo $foo
    out

$ cat stdout.txt
    out

$ cat stderr.txt
    err

Funciona assim:

  1. Ambos os teeprocessos são iniciados, seus stdins são atribuídos a descritores de arquivo. Como eles estão incluídos em substituições de processo , os caminhos para esses descritores de arquivo são substituídos no comando de chamada, então agora parece algo assim:

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

  1. the_cmd é executado, gravando stdout no primeiro descritor de arquivo e stderr no segundo.

  2. No caso do bash, assim que the_cmdterminar, a seguinte instrução acontecerá imediatamente (se o seu terminal for o chamador, você verá seu prompt aparecer).

  3. No caso zsh, uma vez the_cmdconcluído, o shell espera que ambos os teeprocessos terminem antes de prosseguir. Mais sobre isso aqui .

  4. O primeiro teeprocesso, que está lendo do the_cmdstdout de, grava uma cópia desse stdout de volta para o chamador porque é isso que teefaz. Suas saídas não são redirecionadas, então elas voltam para o chamador inalteradas

  5. O segundo teeprocesso é stdoutredirecionado para o stderrresponsável pela chamada (o que é bom, porque o stdin está lendo do the_cmdstderr de). Portanto, quando ele grava em seu stdout, esses bits vão para o stderr do chamador.

Isso mantém o stderr separado do stdout tanto nos arquivos quanto na saída do comando.

Se o primeiro tee escrever algum erro, eles aparecerão tanto no arquivo stderr quanto no stderr do comando, se o segundo tee escrever qualquer erro, eles só aparecerão no stderr do terminal.


Isso parece muito útil e o que eu quero. Não tenho certeza de como replicar o uso de colchetes (conforme mostrado na primeira linha) em um Windows Batch Script, no entanto. ( teeestá disponível no sistema em questão.) O erro que recebo é "O processo não pode acessar o arquivo porque ele está sendo usado por outro processo."
Agi Hammerthief de

Esta solução, como a maioria das outras soluções propostas até agora, é propensa a corrida. Ou seja, quando o script atual for concluído e retornar ao prompt do usuário ou a algum script de chamada de nível superior, o tee, que está sendo executado em segundo plano, ainda estará em execução e poderá emitir as últimas linhas para a tela e para o arquivo de log atrasado (ou seja, para a tela após o prompt e para o arquivo de log depois que o arquivo de log deve estar completo).
Don Hatch,

2
@DonHatch Você pode propor uma solução que corrija esse problema?
pylipp de

Também estou interessado em um caso de teste que torne a corrida aparente. Não é que eu tenha dúvidas, mas é difícil tentar evitar porque não vi isso acontecer.
MatrixManAtYrService

@pylipp Não tenho uma solução. Eu estaria muito interessado em um.
Don Hatch de

4

o para redirecionar stderr para stdout acrescente isto ao seu comando: 2>&1 Para enviar para o terminal e fazer login no arquivo, você deve usartee

Ambos juntos ficariam assim:

 mycommand 2>&1 | tee mylogfile.log

EDIT: Para embutir em seu script, você faria o mesmo. Então seu roteiro

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

acabaria como

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

2
Observe que a distinção entre stdout e stderr será perdida, pois tee imprime tudo em stdout.
Flimm de

4

EDIT: Vejo que descarrilhei e acabei respondendo a uma pergunta diferente daquela feita. A resposta à verdadeira questão está na base da resposta de Paul Tomblin. (Se você quiser aprimorar essa solução para redirecionar stdout e stderr separadamente por algum motivo, pode usar a técnica que descrevo aqui.)


Tenho procurado uma resposta que preserve a distinção entre stdout e stderr. Infelizmente, todas as respostas dadas até agora que preservam essa distinção são propensas a raça: elas correm o risco de os programas receberem informações incompletas, como indiquei nos comentários.

Acho que finalmente encontrei uma resposta que preserva a distinção, não é propensa a raça e também não é terrivelmente complicada.

Primeiro bloco de construção: para trocar stdout e stderr:

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

Segundo bloco de construção: se quiséssemos filtrar (por exemplo, tee) apenas stderr, poderíamos fazer isso trocando stdout & stderr, filtrando e depois trocando de volta:

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

Agora o resto é fácil: podemos adicionar um filtro stdout, no início:

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

ou no final:

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

Para me convencer de que ambos os comandos acima funcionam, usei o seguinte:

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

O resultado é:

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

e meu prompt volta imediatamente após o " teed stderr: to stderr", conforme esperado.

Nota de rodapé sobre zsh :

A solução acima funciona em bash (e talvez em alguns outros shells, não tenho certeza), mas não funciona em zsh. Existem duas razões pelas quais ele falha no zsh:

  1. a sintaxe 2>&3-não é compreendida por zsh; que tem que ser reescrito como2>&3 3>&-
  2. em zsh (ao contrário de outros shells), se você redirecionar um descritor de arquivo que já está aberto, em alguns casos (não entendo completamente como ele decide), ele executa um comportamento semelhante a um tee embutido. Para evitar isso, você deve fechar cada fd antes de redirecioná-lo.

Então, por exemplo, minha segunda solução deve ser reescrita para zsh as {my_command 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stderr_filter;} 3>&1 1>&- 1>&2 2>&- 2>&3 3>&- | stdout_filter(que funciona em bash também, mas é terrivelmente prolixo).

Por outro lado, você pode tirar proveito do misterioso teeing implícito embutido de zsh para obter uma solução muito mais curta para zsh, que não executa tee de forma alguma:

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

(Eu não teria adivinhado pelos documentos, descobri que >&1e 2>&2são as coisas que acionam a tacada implícita de zsh; descobri isso por tentativa e erro.)


Eu brinquei com isso no bash e funcionou bem. Apenas um aviso para usuários zsh com o hábito de assumir compatibilidade (como eu), ele se comporta de forma diferente lá: gist.github.com/MatrixManAtYrService/…
MatrixManAtYrService

@MatrixManAtYrService Acredito que entendi a situação do zsh e descobri que há uma solução muito mais simples no zsh. Veja minha edição "Nota de rodapé sobre zsh".
Don Hatch

Obrigado por explicar a solução com tantos detalhes. Você também sabe como recuperar o código de retorno ao usar uma função ( my_function) na filtragem stdout / stderr aninhada? Sim, { { my_function || touch failed;} 3>&1 1>&2 2>&3- | stderr_filter;} 3>&1 1>&2 2>&3- | stdout_filtermas parece estranho criar um arquivo como indicador de falha ...
pylipp

@pylipp, eu não brinco. Você pode fazer isso como uma pergunta separada (talvez com um pipeline mais simples).
Don Hatch

2

Use o script comando em seu script (script man 1)

Crie um shellscript wrapper (2 linhas) que configura o script () e, em seguida, chama exit.

Parte 1: wrap.sh

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

Parte 2: realscript.sh

#!/bin/sh
echo 'Output'

Resultado:

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

Use o programa tee e dup stderr para stdout.

 program 2>&1 | tee > logfile

1

Criei um script chamado "RunScript.sh". O conteúdo deste script é:

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

Eu chamo assim:

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

Isso funciona, mas requer que os scripts do aplicativo sejam executados por meio de um script externo. É um pouco confuso.


9
Você perderá o agrupamento de argumentos contendo espaços em branco com $ 1 $ 2 $ 3 ... , você deve usar (com aspas): "$ @"
NVRAM

1

Um ano depois, aqui está um antigo script bash para registrar qualquer coisa. Por exemplo,
teelog make ...registra um nome de registro gerado (e veja o truque para registrar makes aninhados também.)

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

Eu sei que é tarde demais para adicionar um comentário, mas eu só tinha que agradecer por este script. Muito útil e bem documentado!
stephenmm

Obrigado @stephenmm; Está não tarde demais para dizer "útil" ou "poderia ser melhorado".
denis
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.