Перенаправить копирование стандартного вывода в файл журнала изнутри самого скрипта 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
это в родительской оболочке впоследствии).