Есть несколько способов добраться tail
до выхода:
Плохой подход: заставить tail
написать еще одну строку
Вы можете принудительно tail
написать еще одну строку вывода сразу после того, grep
как нашли совпадение и вышли. Это приведет tail
к тому SIGPIPE
, что он получит выход. Один из способов сделать это - изменить файл, отслеживаемый tail
после grep
выхода.
Вот пример кода:
tail -f logfile.log | grep -m 1 "Server Started" | { cat; echo >>logfile.log; }
В этом примере cat
не завершит работу, пока grep
не закроет свой стандартный вывод, поэтому tail
вряд ли сможет записать в канал до того, как сможет grep
закрыть свой стандартный вывод. cat
используется для распространения стандартного вывода grep
неизмененного.
Этот подход относительно прост, но есть несколько недостатков:
- Если
grep
закрыть stdout перед закрытием stdin, всегда будет условие гонки: grep
закрытие stdout, запуск cat
для выхода, запуск echo
, запуск tail
для вывода строки. Если эта строка отправлена до того, grep
как grep
она успела закрыть стандартный ввод, tail
она не будет получена, пока не будет SIGPIPE
записана другая строка.
- Требуется доступ для записи в файл журнала.
- Вы должны быть в порядке с изменением файла журнала.
- Вы можете повредить файл журнала, если произойдет запись одновременно с другим процессом (записи могут чередоваться, что приводит к появлению новой строки в середине сообщения журнала).
- Этот подход специфичен для
tail
- он не будет работать с другими программами.
- Этап третий трубопровод делает его трудно получить доступ к коду возврата второго этапа трубопровода (если вы не используете расширение POSIX , таких как
bash
«s PIPESTATUS
массив). Это не имеет большого значения в этом случае, потому grep
что всегда будет возвращать 0, но в целом средняя стадия может быть заменена другой командой, код возврата которой вам небезразличен (например, что-то, что возвращает 0, когда обнаружен «сервер запущен», 1 когда "сервер не запустился" обнаружено).
Следующие подходы позволяют избежать этих ограничений.
Лучший подход: избегайте трубопроводов
Вы можете использовать FIFO, чтобы полностью избежать конвейера, позволяя продолжить выполнение после grep
возврата. Например:
fifo=/tmp/tmpfifo.$$
mkfifo "${fifo}" || exit 1
tail -f logfile.log >${fifo} &
tailpid=$! # optional
grep -m 1 "Server Started" "${fifo}"
kill "${tailpid}" # optional
rm "${fifo}"
Строки, отмеченные комментарием, # optional
могут быть удалены, и программа все равно будет работать; tail
будет просто задерживаться, пока не прочитает другую строку ввода или не будет уничтожен каким-либо другим процессом.
Преимущества этого подхода:
- вам не нужно изменять файл журнала
- подход работает для других утилит, кроме
tail
- не страдает от состояния гонки
- вы можете легко получить возвращаемое значение
grep
(или любую другую альтернативную команду, которую вы используете)
Недостатком этого подхода является сложность, особенно управление FIFO: вам нужно будет безопасно сгенерировать временное имя файла, и вам нужно будет убедиться, что временный FIFO удален, даже если пользователь нажимает Ctrl-C в середине сценарий. Это можно сделать с помощью ловушки.
Альтернативный подход: отправить сообщение в Kill tail
Вы можете получить выход из tail
этапа конвейера, отправив ему сигнал типа SIGTERM
. Задача состоит в том, чтобы точно знать две вещи в одном и том же месте в коде: tail
PID и был ли grep
выход.
При использовании конвейерного типа tail -f ... | grep ...
легко изменить первый этап конвейера, чтобы сохранить tail
PID в переменной с помощью фонового изображения tail
и чтения $!
. Также легко изменить второй этап конвейера для запуска kill
при grep
выходе. Проблема заключается в том, что два этапа конвейера работают в отдельных «средах выполнения» (в терминологии стандарта POSIX), поэтому второй этап конвейера не может считывать переменные, установленные первым этапом конвейера. Без использования переменных оболочки либо второй этап должен каким-то образом вычислять tail
PID, чтобы он мог завершиться tail
при grep
возврате, либо первый этап должен быть каким-то образом уведомлен при grep
возврате.
Второй этап можно использовать pgrep
для получения tail
PID, но это будет ненадежно (вы могли бы соответствовать неправильному процессу) и непереносимо ( pgrep
не указано в стандарте POSIX).
Первый этап может отправить PID на второй этап через канал, echo
используя PID, но эта строка будет смешана с tail
выходными данными. Демультиплексирование двух может потребовать сложной схемы экранирования, в зависимости от вывода tail
.
Вы можете использовать FIFO, чтобы второй этап конвейера уведомлял первый этап конвейера при grep
выходе. Тогда первый этап может убить tail
. Вот пример кода:
fifo=/tmp/notifyfifo.$$
mkfifo "${fifo}" || exit 1
{
# run tail in the background so that the shell can
# kill tail when notified that grep has exited
tail -f logfile.log &
# remember tail's PID
tailpid=$!
# wait for notification that grep has exited
read foo <${fifo}
# grep has exited, time to go
kill "${tailpid}"
} | {
grep -m 1 "Server Started"
# notify the first pipeline stage that grep is done
echo >${fifo}
}
# clean up
rm "${fifo}"
Этот подход имеет все плюсы и минусы предыдущего подхода, за исключением того, что он более сложный.
Предупреждение о буферизации
POSIX позволяет полностью буферизовать потоки stdin и stdout, что означает, что tail
выходные данные могут не обрабатываться grep
сколь угодно долго. В системах GNU не должно быть никаких проблем: GNU grep
использует read()
, что исключает любую буферизацию, а GNU tail -f
делает регулярные вызовы fflush()
при записи в stdout. В системах без GNU может потребоваться сделать что-то особенное, чтобы отключить или регулярно очищать буферы.