Перенаправить копирование стандартного вывода в файл журнала изнутри самого скрипта bash

Я знаю, как перенаправить стандартный вывод в файл:

exec > foo.log
echo test

это поместит "тест" в файл foo.log.

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

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

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 например, использование цветов и столбцов вывода) обнаружит вышеупомянутую конструкцию как означающую, что они выводят в канал.

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

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

./script.sh >/dev/null

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

./script.sh 2>/dev/null

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

#!/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-2008tee(1) требует, чтобы вывод был небуферизованным, то есть даже не буферизованным строкой, поэтому в этом случае возможно, что STDOUT и STDERR могут оказаться в одной строке foo.log; однако это также может произойти на терминале, поэтому файл журнала будет точным отражением того, что можно увидеть на терминале, если не будет точным отражением этого. Если вы хотите, чтобы строки STDOUT были четко отделены от строк STDERR, рассмотрите возможность использования двух файлов журналов, возможно, с префиксами отметок даты в каждой строке, чтобы впоследствии разрешить хронологическую сборку.

Решение для занятых и небашевых оболочек

Принятый ответ, безусловно, лучший выбор для 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 $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 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 в системном журнале. Вывод скрипта доступен как через /var/log/syslog и через stderr. Системный журнал добавит полезные метаданные, включая метки времени.

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

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', который записывает все на диск и в исходный стандартный вывод скрипта.

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

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

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

Не могу сказать, что мне удобно любое из решений, основанных на exec. Я предпочитаю использовать 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) $0 $@ >> log.txt
        TEE=false $0 $@ 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

Вы можете настроить это, например, вместо этого сделать по умолчанию make 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

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

Другие вопросы по тегам