перенаправить копию вывода в лог-файл в bash-скрипт сам

Я знаю, как перенаправить stdout файл:

exec > foo.log
echo test

это поставит "тест" в foo.журнал.

Теперь я хочу перенаправить вывод в файл журнала и сохранить его на stdout

т. е. это можно сделать тривиально извне скрипта:

script | tee foo.log

но я хочу объявить его в сам скрипт

пробовал

exec | tee foo.log

но это не сработало.

9 ответов


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

обратите внимание, что это bash, а не sh. Если вы вызываете скрипт с помощью sh myscript.sh, вы получите ошибку по строкам syntax error near unexpected token '>'.

если вы работаете с сигнальными ловушками, вы можете использовать tee -i вариант для избежания нарушения выхода если сигнал происходит. (Спасибо JamesThomasMoon1979 за комментарий.)


инструменты, которые изменяют свой выход в зависимости от того, пишут ли они в трубу или терминал (ls использование цвета и columnized output, например) обнаружит вышеуказанную конструкцию как означающую, что они выводятся в трубу.

есть опции для обеспечения колоризации / колумнизации (например,ls -C --color=always). Обратите внимание, что это приведет к тому, что цветовые коды будут записываться в файл журнала, что делает его меньше читаем.


принятый ответ не сохраняет STDERR как отдельный файловый дескриптор. Это значит

./script.sh >/dev/null

не выводит bar к терминалу, только к файлу журнала, и

./script.sh 2>/dev/null

выведет оба foo и bar к терминалу. Очевидно, что это не поведение, которое обычный пользователь, вероятно, ожидает. Это может быть исправлено с помощью двух отдельных процессов tee, добавляющихся к одному и тому же файл журнала:

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

(обратите внимание, что выше не изначально усечь файл журнала - Если вы хотите, чтобы это поведение, вы должны добавить

>foo.log

в верхней части скрипта.)

на POSIX.Спецификация 1-2008 от tee(1) требует, чтобы выход не был буферизован, т. е. даже не был буферизован линией, поэтому в этом случае возможно, что STDOUT и STDERR могут оказаться в одной строке foo.log, однако это также может произойти на терминал, поэтому файл журнала будет верным отражением того, что мог бы можно увидеть на терминале, если не точное его отражение. Если вы хотите, чтобы строки STDOUT были четко отделены от строк STDERR, рассмотрите возможность использования двух файлов журнала, возможно, с префиксами даты на каждой строке, чтобы разрешить хронологическую сборку позже.


решение для busybox и non-bash оболочек

принятый ответ, безусловно, лучший выбор для bash. Я работаю в среде Busybox без доступа к bash, и он не понимает exec > >(tee log.txt) синтаксис. Это также не делает exec >$PIPE правильно, пытаясь создать обычный файл с тем же именем, что и именованный канал, который терпит неудачу и зависает.

надеюсь, это будет полезно кому-то еще, у кого нет bash.

также, для тех, кто использует именованный канал, безопасно rm $PIPE, потому что это отсоединяет канал от VFS, но процессы, которые его используют, все еще поддерживают счетчик ссылок на него, пока они не закончены.

обратите внимание, что использование $* не обязательно безопасно.

#!/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 without redirected to the named pipe
    SELF_LOGGING=1 sh  $* >$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 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

внутри файла скрипта поместите все команды в круглые скобки, например:

(
echo start
ls -l
echo end
) | tee foo.log

простой способ сделать журнал сценария bash в syslog. Вывод скрипта доступен как через /var/log/syslog и через stderr. syslog добавит полезные метаданные, включая временные метки.

добавьте эту строку вверху:

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

кроме того, отправьте журнал в отдельный файл:

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

для этого требуется moreutils (для ts команда, которая добавляет метки времени).


используя принятый ответ, мой скрипт возвращался исключительно рано (сразу после ' exec > >(tee ...) ') оставляя остальную часть моего сценария в фоновом режиме. Поскольку я не мог заставить это решение работать по-моему, я нашел другое решение/обойти проблему:

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

# Rest of my script

это делает вывод из скрипта идти от процесса, через канал в суб фоновый процесс "tee", который регистрирует все на диск и оригинальный stdout скрипта.

Примечание. это "exec & >" перенаправляет как stdout, так и stderr, мы можем перенаправить их отдельно, если хотим, или изменить на " exec>", если мы просто хотим stdout.

даже еси труба удаляется из файловой системы в начале скрипта он будет продолжать функционировать до завершения процессов. Мы просто не можем ссылаться на него, используя имя файла после rm-line.


Bash 4 имеет coproc команда, которая устанавливает именованный канал для команды и позволяет вам общаться через него.


не могу сказать, что мне комфортно с любым из решений, основанных на старпома. Я предпочитаю использовать tee напрямую, поэтому я заставляю скрипт называть себя tee по запросу:

# 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)  $@ >> log.txt
        TEE=false  $@ 2>&1 | tee --append log.txt
        exit $?
    fi 
}

check_tee_output $@

rest of my script

Это позволяет сделать следующее:

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

вы можете настроить это, например, сделать tee=false по умолчанию, вместо этого сделать TEE держать файл журнала и т. д. Я думаю, что это решение похоже на jbarlow, но проще, возможно, у меня есть ограничения, с которыми я еще не сталкивался.


ни один из них не является идеальным решением, но вот несколько вещей, которые вы могли бы попробовать:

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

или

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

второй оставил бы файл канала, сидящий вокруг, если что-то пойдет не так с вашим скриптом, что может быть или не быть проблемой (т. е., Возможно, вы могли бы rm это в родительской оболочке впоследствии).