redirecionar COPY do stdout para o arquivo de log do próprio script bash

235

Eu sei como redirecionar stdout para um arquivo:

exec > foo.log
echo test

isso colocará o 'teste' no arquivo foo.log.

Agora eu quero redirecionar a saída para o arquivo de log E mantê-lo no stdout

isto é, pode ser feito trivialmente de fora do script:

script | tee foo.log

mas eu quero declarar isso dentro do próprio script

eu tentei

exec | tee foo.log

mas não funcionou.

Vitaly Kushner
fonte
3
Sua pergunta está mal formulada. Quando você chama 'exec> foo.log', o stdout do script é o arquivo foo.log. Eu acho que você quer dizer que deseja que a saída vá para foo.log e para o tty, já que ir para foo.log será stdout.
William Pursell
o que eu gostaria de fazer é usar o | no 'exec'. que seria perfeito para mim, isto é, "exec | tee foo.log", infelizmente, você não pode usar o redirecionamento de tubulação na chamada exec
Vitaly Kushner

Respostas:

297
#!/usr/bin/env bash

# Redirect stdout ( > ) into a named pipe ( >() ) running "tee"
exec > >(tee -i logfile.txt)

# Without this, only stdout would be captured - i.e. your
# log file would not contain any error messages.
# SEE (and upvote) the answer by Adam Spiers, which keeps STDERR
# as a separate stream - I did not want to steal from him by simply
# adding his answer to mine.
exec 2>&1

echo "foo"
echo "bar" >&2

Note que bashnão é isso sh. Se você chamar o script sh myscript.sh, receberá um erro ao longo das linhas de syntax error near unexpected token '>'.

Se você estiver trabalhando com traps de sinal, convém usar a tee -iopção para evitar interrupções na saída se ocorrer um sinal. (Obrigado a JamesThomasMoon1979 pelo comentário.)


As ferramentas que alteram sua saída dependendo de gravar em um tubo ou terminal ( lsusando cores e saída em colunas, por exemplo) detectarão a construção acima como significando que elas serão enviadas para um tubo.

Existem opções para aplicar a coloração / colunização (por exemplo ls -C --color=always). Observe que isso resultará na gravação dos códigos de cores no arquivo de log, tornando-o menos legível.

DevSolar
fonte
5
Como a maioria dos sistemas é armazenada em buffer, a saída pode não chegar até depois que o script for concluído. Além disso, como esse tee está sendo executado em um subshell, não em um processo filho, a espera não pode ser usada para sincronizar a saída com o processo de chamada. O que você quer é uma versão sem buffer de tee semelhante ao bogomips.org/rainbows.git/commit/...
14
@ Barry: POSIX especifica que teenão deve armazenar em buffer sua saída. Se ele faz buffer na maioria dos sistemas, está quebrado na maioria dos sistemas. Esse é um problema das teeimplementações, não da minha solução.
DevSolar
3
@ Sebastian: execé muito poderoso, mas também muito envolvido. Você pode "fazer backup" do stdout atual para um outro editor de arquivos e recuperá-lo posteriormente. Google "bash exec tutorial", há muitas coisas avançadas por aí.
DevSolar
2
@ AdamSpiers: Também não sei ao certo o que era Barry. Bash execé documentado para não iniciar novos processos, >(tee ...)é um padrão chamado de substituição da tubulação / processo, eo &no redirecionamento, obviamente, nada a ver com backgrounding tem ... :-)?
DevSolar
11
Eu sugiro passar -ipara tee. Caso contrário, as interrupções de sinal (interrupções) interromperão o stdout no script principal. Por exemplo, se você tiver um trap 'echo foo' EXITe depois pressionar ctrl+c, não verá " foo ". Então, eu modificaria a resposta para exec &> >(tee -ia file).
JamesThomasMoon1979
174

A resposta aceita não preserva STDERR como um descritor de arquivo separado. Que significa

./script.sh >/dev/null

não será enviado barpara o terminal, apenas para o arquivo de log e

./script.sh 2>/dev/null

produzirá ambos fooe barpara o terminal. Claramente, esse não é o comportamento que um usuário normal provavelmente espera. Isso pode ser corrigido usando dois processos tee separados, ambos anexando ao mesmo arquivo de log:

#!/bin/bash

# See (and upvote) the comment by JamesThomasMoon1979 
# explaining the use of the -i option to tee.
exec >  >(tee -ia foo.log)
exec 2> >(tee -ia foo.log >&2)

echo "foo"
echo "bar" >&2

(Observe que o acima não inicialmente trunca o arquivo de log - se você deseja esse comportamento, adicione

>foo.log

para o topo do script.)

A especificação POSIX.1-2008 detee(1) exige que a saída seja sem buffer, ou seja, nem com buffer de linha; portanto, neste caso, é possível que STDOUT e STDERR possam terminar na mesma linha de foo.log; no entanto, isso também pode acontecer no terminal, portanto o arquivo de log será um reflexo fiel do que pode ser visto no terminal, se não um espelho exato dele. Se você deseja que as linhas STDOUT sejam separadas de maneira limpa das linhas STDERR, considere o uso de dois arquivos de log, possivelmente com prefixos de carimbo de data em cada linha para permitir a remontagem cronológica posteriormente.

Adam Spires
fonte
Por alguma razão, no meu caso, quando o script é executado a partir de uma chamada de sistema do programa c (), os dois subprocessos tee continuam existindo mesmo após a saída do script principal. Então eu tive que adicionar armadilhas como esta:exec > >(tee -a $LOG) trap "kill -9 $! 2>/dev/null" EXIT exec 2> >(tee -a $LOG >&2) trap "kill -9 $! 2>/dev/null" EXIT
alveko
15
Eu sugiro passar -ipara tee. Caso contrário, as interrupções de sinal (interrupções) interromperão o stdout no script. Por exemplo, se você trap 'echo foo' EXITpressionar e, em seguida ctrl+c, não verá " foo ". Então, eu modificaria a resposta para exec > >(tee -ia foo.log).
JamesThomasMoon1979
Eu criei alguns scripts "sourceable" baseados nisso. Pode usá-los em um script como . logou . log foo.log: sam.nipl.net/sh/log sam.nipl.net/sh/log-a
Sam Watkins
1
O problema com esse método é que as mensagens serão STDOUTexibidas primeiro como um lote e, em seguida, as mensagens serão STDERRexibidas. Eles não são intercalados como normalmente esperado.
CMCDragonkai
28

Solução para shells busybox, macOS bash e não bash

A resposta aceita é certamente a melhor escolha para o bash. Estou trabalhando em um ambiente Busybox sem acesso ao bash e ele não entende a exec > >(tee log.txt)sintaxe. Ele também não funciona exec >$PIPEcorretamente, tentando criar um arquivo comum com o mesmo nome que o pipe nomeado, que falha e trava.

Espero que isso seja útil para outra pessoa que não tem festança.

Além disso, para qualquer pessoa que use um pipe nomeado, é seguro rm $PIPE, porque isso desassocia o pipe do VFS, mas os processos que o utilizam ainda mantêm uma contagem de referência até que sejam concluídos.

Observe que o uso de $ * não é necessariamente seguro.

#!/bin/sh

if [ "$SELF_LOGGING" != "1" ]
then
    # The parent process will enter this branch and set up logging

    # Create a named piped for logging the child's output
    PIPE=tmp.fifo
    mkfifo $PIPE

    # Launch the child process with stdout redirected to the named pipe
    SELF_LOGGING=1 sh $0 $* >$PIPE &

    # Save PID of child process
    PID=$!

    # Launch tee in a separate process
    tee logfile <$PIPE &

    # Unlink $PIPE because the parent process no longer needs it
    rm $PIPE    

    # Wait for child process, which is running the rest of this script
    wait $PID

    # Return the error code from the child process
    exit $?
fi

# The rest of the script goes here
jbarlow
fonte
Esta é a única solução que eu tenho visto até agora que funciona em Mac
Mike Baglio Jr.
19

Dentro do seu arquivo de script, coloque todos os comandos entre parênteses, assim:

(
echo start
ls -l
echo end
) | tee foo.log
WReach
fonte
5
pedantically, também poderia usar cintas ( {})
Glenn Jackman
Bem, sim, eu considerei isso, mas esse não é o redirecionamento do stdout atual do shell, é uma espécie de trapaça, você está executando um subshell e fazendo um redirecionamento regular do flautista. trabalha o pensamento. Estou dividido com isso e com a solução "tail -f foo.log &". vai esperar um pouco para ver se pode ser melhor. se não, provavelmente, vai se contentar;)
Vitaly Kushner
8
{} executa uma lista no ambiente atual do shell. () executa uma lista em um ambiente subshell.
Droga. Obrigado. A resposta aceita lá em cima não funcionou para mim, tentando agendar um script para ser executado no MingW em um sistema Windows. Eu reclamei, acredito, sobre a substituição não implementada de processos. Esta resposta funcionou bem, após a seguinte alteração, para capturar stderr e stdout: `` -) | tee foo.log +) 2> & 1 | td foo.log
Jon Carter
14

Maneira fácil de criar um script bash para o syslog. A saída do script está disponível através /var/log/sysloge através do stderr. O syslog adicionará metadados úteis, incluindo registros de data e hora.

Adicione esta linha na parte superior:

exec &> >(logger -t myscript -s)

Como alternativa, envie o log para um arquivo separado:

exec &> >(ts |tee -a /tmp/myscript.output >&2 )

Isso requer moreutils(para o tscomando, que adiciona timestamps).

Tobu
fonte
10

Usando a resposta aceita, meu script continuava retornando excepcionalmente cedo (logo após 'exec>> (tee ...)') deixando o restante do meu script em execução em segundo plano. Como não consegui fazer com que a solução funcionasse, encontrei outra solução / solução para o problema:

# Logging setup
logfile=mylogfile
mkfifo ${logfile}.pipe
tee < ${logfile}.pipe $logfile &
exec &> ${logfile}.pipe
rm ${logfile}.pipe

# Rest of my script

Isso faz com que a saída do script vá do processo, passando pelo canal até o processo secundário de 'tee', que registra tudo no disco e no stdout original do script.

Observe que 'exec &>' redireciona stdout e stderr; podemos redirecioná-los separadamente, se quisermos, ou mudar para 'exec>', se quisermos apenas stdout.

Mesmo que o tubo seja removido do sistema de arquivos no início do script, ele continuará funcionando até o término do processo. Nós simplesmente não podemos fazer referência a ele usando o nome do arquivo após a linha rm.

fgunger
fonte
Resposta semelhante ao da segunda ideia de David Z . Dê uma olhada em seus comentários. 1 ;-)
olibre 31/07
Funciona bem. Eu não estou entendendo a $logfileparte tee < ${logfile}.pipe $logfile &. Especificamente, tentei alterar isso para capturar linhas de log de comando expandidas completas (de set -x) para arquivo, enquanto mostrava apenas linhas sem levar '+' no stdout, alterando para (tee | grep -v '^+.*$') < ${logfile}.pipe $logfile &mas recebi uma mensagem de erro referente $logfile. Você pode explicar a teelinha um pouco mais detalhadamente?
Chris Johnson
Testei isso e parece que essa resposta não preserva o STDERR (é mesclado com o STDOUT); portanto, se você confiar que os fluxos são separados para detecção de erros ou outro redirecionamento, verifique a resposta de Adam.
HeroCC 16/06
2

O Bash 4 possui um coproccomando que estabelece um pipe nomeado para um comando e permite que você se comunique através dele.

Pausado até novo aviso.
fonte
1

Não posso dizer que estou confortável com qualquer uma das soluções baseadas em exec. Eu prefiro usar tee diretamente, então eu faço o script se chamar com tee quando solicitado:

# my script: 

check_tee_output()
{
    # copy (append) stdout and stderr to log file if TEE is unset or true
    if [[ -z $TEE || "$TEE" == true ]]; then 
        echo '-------------------------------------------' >> log.txt
        echo '***' $(date) $0 $@ >> log.txt
        TEE=false $0 $@ 2>&1 | tee --append log.txt
        exit $?
    fi 
}

check_tee_output $@

rest of my script

Isso permite que você faça isso:

your_script.sh args           # tee 
TEE=true your_script.sh args  # tee 
TEE=false your_script.sh args # don't tee
export TEE=false
your_script.sh args           # tee

Você pode personalizar isso, por exemplo, tornar tee = false o padrão, em vez disso, fazer com que o TEE retenha o arquivo de log etc. Acho que essa solução é semelhante à do jbarlow, mas mais simples, talvez a minha tenha limitações que ainda não encontrei.

Oliver
fonte
-1

Nenhuma dessas é uma solução perfeita, mas aqui estão algumas coisas que você pode tentar:

exec >foo.log
tail -f foo.log &
# rest of your script

ou

PIPE=tmp.fifo
mkfifo $PIPE
exec >$PIPE
tee foo.log <$PIPE &
# rest of your script
rm $PIPE

O segundo deixaria um arquivo de pipe parado se algo desse errado com o seu script, o que pode ou não ser um problema (ou seja, talvez você possa fazer rmisso no shell pai posteriormente).

David Z
fonte
1
tail deixará um processo em execução para trás no segundo script. O tee bloqueará, ou você precisará executá-lo com &, nesse caso, deixará o processo como no primeiro.
Vitaly Kushner
@ Vitaly: opa, esqueci o plano de fundo tee- eu editei. Como eu disse, nenhuma das duas é uma solução perfeita, mas os processos em segundo plano serão eliminados quando o shell pai terminar, para que você não precise se preocupar com a necessidade de consumir recursos para sempre.
David Z
1
Caramba: isso parece atraente, mas a saída do tail -f também vai para foo.log. Você pode corrigir isso executando tail -f antes do exec, mas a cauda ainda permanece em execução após o término do pai. Você precisa matá-lo explicitamente, provavelmente em uma armadilha 0. #
William Pursell
Yeap. Se o script estiver em segundo plano, ele deixará os processos por todo o lado.