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


235

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

exec > foo.log
echo test

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

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

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

script | tee foo.log

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

Я попытался

exec | tee foo.log

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


3
Ваш вопрос плохо сформулирован. Когда вы вызываете 'exec> foo.log', выводом скрипта является файл foo.log. Я думаю , что вы имеете в виду , что вы хотите , чтобы вывод пойти в foo.log и на терминал, так как собирается foo.log это происходит на стандартный вывод.
Уильям Перселл

что я хотел бы сделать, это использовать | на "Exec". это было бы идеально для меня, то есть "exec | tee foo.log", к сожалению, вы не можете использовать перенаправление каналов при вызове exec
Виталий Кушнер

Ответы:


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

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

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


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

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


5
На большинстве систем буферизуется тройник, поэтому выходные данные могут появиться только после завершения сценария. Кроме того, поскольку этот тройник работает в подоболочке, а не в дочернем процессе, ожидание нельзя использовать для синхронизации вывода с вызывающим процессом. То , что вы хотите , это небуферизована версия тройника похожа на bogomips.org/rainbows.git/commit/...

14
@Barry: POSIX указывает, что teeне должен буферизовать свой вывод. Если это делает буфер на большинстве систем, это сломано на большинстве систем. Это проблема teeреализаций, а не моего решения.
DevSolar

3
@Sebastian: execочень мощный, но и очень вовлеченный. Вы можете «создать резервную копию» текущего стандартного вывода в другом дескрипторе файла, а затем восстановить его. Google "bash exec tutorial", есть много продвинутых вещей.
DevSolar

2
@AdamSpiers: Я тоже не знаю, о чем Барри. Bash - execэто документально не начинать новые процессы, >(tee ...)является стандартом по имени подмена труб / процесс, и &в перенаправлении , конечно , не имеет ничего общего с ... :-) фоновый?
DevSolar

11
Я предлагаю перейти -iк tee. В противном случае прерывания сигнала (traps) нарушат стандартный вывод основного сценария. Например, если у вас есть, trap 'echo foo' EXITа затем нажмите ctrl+c, вы не увидите « foo ». Поэтому я бы изменил ответ на exec &> >(tee -ia file).
JamesThomasMoon1979

174

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


По некоторым причинам, в моем случае, когда сценарий выполняется из системного вызова c-program, два подпроцесса tee продолжают существовать даже после выхода из основного сценария. Таким образом, я должен был добавить ловушки как это: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
Я предлагаю перейти -iк tee. В противном случае прерывания сигнала (прерывания) нарушат стандартный вывод в сценарии. Например, если вы trap 'echo foo' EXITи затем ctrl+cнажмете, вы не увидите « foo ». Поэтому я бы изменил ответ на exec > >(tee -ia foo.log).
JamesThomasMoon1979

Я сделал несколько небольших «исходных» скриптов, основанных на этом. Можно использовать их в сценарии, например . logили . log foo.log: sam.nipl.net/sh/log sam.nipl.net/sh/log-a
Сэм Уоткинс,

1
Проблема этого метода заключается в том, что сообщения будут STDOUTпоявляться сначала в виде пакета, а затем будут STDERRпоявляться сообщения. Они не чередуются, как обычно ожидалось.
CMCDragonkai

28

Решение для busybox, macOS bash и не-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 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

Это единственное решение, которое я видел до сих пор, которое работает на Mac
Майк Баглио младший

19

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

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

5
педантично, также можно использовать фигурные скобки ( {})
Гленн Джекман

да, я подумал об этом, но это не перенаправление текущего stdout оболочки, это своего рода обман, вы фактически запускаете subshell и делаете для него обычное перенаправление piper. работает мысль. Я разделен с этим и решением "tail -f foo.log &". подожду немного, чтобы увидеть, может ли быть лучше поверхности. если не возможно, поселиться;)
Виталий Кушнер

8
{} выполняет список в текущей среде оболочки. () выполняет список в подоболочке.

Черт. Спасибо. Принятый там ответ мне не помог, пытаясь запланировать запуск сценария под MingW в системе Windows. Он жаловался, я полагаю, на невыполненную замену процесса. После следующего изменения этот ответ работал отлично, чтобы захватить как stderr, так и stdout: `` `-) | tee foo.log +) 2> & 1 | tee foo.log
Джон Картер

14

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

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

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

В качестве альтернативы, отправьте журнал в отдельный файл:

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

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


10

Используя принятый ответ, мой скрипт возвращался исключительно рано (сразу после '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.


Подобный ответ как вторая идея от Дэвида Z . Посмотрите на его комментарии. +1 ;-)
olibre

Работает хорошо. Я не понимаю $logfileчасть tee < ${logfile}.pipe $logfile &. В частности, я попытался изменить это, чтобы захватить полностью развернутые строки журнала команд (из set -x) в файл, показывая только строки без начального «+» в stdout, изменив на, (tee | grep -v '^+.*$') < ${logfile}.pipe $logfile &но получив сообщение об ошибке, касающееся $logfile. Можете ли вы объяснить teeстроку более подробно?
Крис Джонсон

Я проверил это, и кажется, что этот ответ не сохраняет STDERR (он объединен с STDOUT), поэтому, если вы полагаетесь на отдельные потоки для обнаружения ошибок или другого перенаправления, вы должны посмотреть на ответ Адама.
HeroCC


1

Не могу сказать, что мне удобно какое-либо из решений, основанных на 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

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


-1

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

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потом это сделать в родительской оболочке).


1
Хвост оставит запущенный процесс во втором скрипте, который будет заблокирован, или вам нужно будет запустить его с &, и в этом случае он выйдет из процесса, как в первом.
Виталий Кушнер

@Vitaly: ой, забыл фон tee- я редактировал. Как я уже сказал, ни одно из них не является идеальным решением, но фоновые процессы будут убиты, когда завершится их родительская оболочка, так что вам не придется беспокоиться о том, что они будут затягивать ресурсы вечно.
Дэвид З,

1
Yikes: это выглядит привлекательно, но вывод tail -f также собирается в foo.log. Вы можете исправить это, выполнив tail -f перед exec, но хвост все еще остается запущенным после завершения родительского процесса. Вы должны явно убить его, вероятно, в ловушке 0.
Уильям Перселл

Yeap. Если сценарий является фоновым, он оставляет процессы повсюду.
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.