4 июня 2016 Как сохранить копию вывода скрипта в лог

Перевод: en

В некоторых ситуациях бывает нужно перенаправить вывод в stdout и stderr из шел-скрипта в лог, сохранив его при этом и в терминале тоже. Обычно в таких случаях я заворачивал интересующие команды в фигурные скобки и перенаправлял полученную составную команду на вход tee. В скрипте ниже более прямой способ добиться того же самого результата с помощью именованных пайпов.

#!/usr/bin/env bash

# stdout и stderr будут перенаправлены в этот файл.
log_file="script.log"

# Завершить скрипт, если какая-либо из команд вернёт ошибку, которая не
# будет обработана.
set -e

# Для создания временного пайпа нет специальной команды, так что заводим
# временную директорию, в которую помещаем пайп.
fifo_dir="$(mktemp -d)"
fifo="${fifo_dir}/fifo"
mkfifo "${fifo}"

# Команда `tee` дублирует все данные, прочитанные из пайпа, и в stdout,
# и в лог-файл.
tee "${log_file}" < "${fifo}" & tee_pid=$!

# Перенаправляем stdout и stderr в пайп.
exec &> "${fifo}"

cleanup() {
    # Запоминаем результат последней команды.
    code=$?

    # Разрешаем функции завершиться, даже если в процессе возникнет ошибка.
    set +e

    echo "Exiting with code ${code}"

    # Нужно закрыть дескрипторы stdout и stderr, чтобы процесс `tee`
    # завершился.
    exec 1>&- 2>&-
    wait ${tee_pid}
    rm -rf "${fifo_dir}"

    # Здесь можно сделать что-нибудь с получившимся лог-файлом, например:
    # cat "${log_file}" | mail -s "${log_file}" user@example.com

    exit ${code}
}

# cleanup() вызовется с правильным кодом возврата даже если скрипт будет
# остановлен преждевременно из-за ошибки (см. `set -e` выше).
trap cleanup EXIT

# Здесь должно быть основное тело скрипта.
# Нужно отметить, что, если процесс скрипта будет в какой-то момент
# замещён командой `exec`, функция cleanup() не вызовется, и временные файлы
# не будут удалены.
$@

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

Есть и другой способ получить аналогичный эффект, используя встроенную в bash команду coproc, который описан, например, здесь и обсуждение на связанную с coproc тему здесь. У него есть определённые преимущества (например, не нужен доступ на запись в директорию для создания временных файлов), но лично я продолжу использовать именованные пайпы, т.к. код с ними выглядит понятнее.