Есть несколько способов получить 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
закрывает стандартный вывод перед закрытием стандартного ввода, всегда будет условие гонки: grep
закрывает стандартный вывод, вызывая выход cat
, вызывая echo
, вызывая tail
для вывода строки. Если эта строка отправляется в grep
до того, как grep
сможет закрыть stdin, tail
не получит SIGPIPE
пока не напишет другую строку.
- Требуется доступ для записи в файл журнала.
- Вы должны быть в порядке с изменением файла журнала.
- Вы можете повредить файл журнала, если произойдет запись одновременно с другим процессом (записи могут чередоваться, что приводит к появлению новой строки в середине сообщения журнала).
- Этот подход специфичен для
tail
- он не будет работать с другими программами.
- Третий этап конвейера затрудняет получение доступа к коду возврата второго этапа конвейера (если вы не используете расширение POSIX, такое как массив
bash
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
. Задача состоит в том, чтобы точно знать две вещи в одном и том же месте в коде: PID tail
и завершился ли grep
.
С трубопроводом, как tail -f ... | grep ...
это легко модифицировать первый этап трубопровода , чтобы сохранить PID tail
«s в переменной, tail
и фоновый режим чтения $!
, Также легко изменить второй этап конвейера для запуска kill
при выходе из grep
. Проблема заключается в том, что два этапа конвейера работают в отдельных "средах выполнения" (в терминологии стандарта POSIX), поэтому второй этап конвейера не может читать переменные, установленные первым этапом конвейера. Без использования переменных оболочки либо второй этап должен каким-то образом определять PID tail
, чтобы он мог уничтожить tail
при возврате grep
, либо первый этап должен каким-то образом уведомляться при возврате grep
.
Второй этап может использовать pgrep
для получения PID tail
, но это будет ненадежно (вы можете соответствовать неправильному процессу) и непереносимо (pgrep
не определено стандартом POSIX).
Первый этап может отправить PID на вторую ступень через трубу echo
ИНГ на PID, но эта строка будет получить смешанную с выходом tail
«s. Демультиплексирование двух может потребовать сложной схемы экранирования, в зависимости от выхода 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 может потребоваться сделать что-то особенное, чтобы отключить или регулярно очищать буферы.