Переадресовать COPY stdout в файл журнала из bash script

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

exec > foo.log
echo test

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

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

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

script | tee foo.log

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

Я пробовал

exec | tee foo.log

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

Ответ 1

#!/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. Если вы вызываете script с помощью sh myscript.sh, вы получите сообщение об ошибке в строке syntax error near unexpected token '>'.

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


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

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

Ответ 2

Принятый ответ не сохраняет 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

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

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

Ответ 3

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

Ответ 4

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

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

Ответ 5

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

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

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

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

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

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

Ответ 6

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

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

# Rest of my script

Это делает вывод из script переходом из процесса через трубку в вспомогательный фоновый процесс 'tee', который записывает все на диск и в исходное stdout script.

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

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

Ответ 7

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

Ответ 8

Не могу сказать, что мне нравится любое решение на основе 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 [email protected] >> log.txt
        TEE=false $0 [email protected] 2>&1 | tee --append log.txt
        exit $?
    fi 
}

check_tee_output [email protected]

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

Ответ 9

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

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

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