Есть несколько способов добраться 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. Задача состоит в том, чтобы точно знать две вещи в одном и том же месте в коде: tailPID и был ли grepвыход.
При использовании конвейерного типа tail -f ... | grep ...легко изменить первый этап конвейера, чтобы сохранить tailPID в переменной с помощью фонового изображения tailи чтения $!. Также легко изменить второй этап конвейера для запуска killпри grepвыходе. Проблема заключается в том, что два этапа конвейера работают в отдельных «средах выполнения» (в терминологии стандарта POSIX), поэтому второй этап конвейера не может считывать переменные, установленные первым этапом конвейера. Без использования переменных оболочки либо второй этап должен каким-то образом вычислять tailPID, чтобы он мог завершиться tailпри grepвозврате, либо первый этап должен быть каким-то образом уведомлен при grepвозврате.
Второй этап можно использовать pgrepдля получения tailPID, но это будет ненадежно (вы могли бы соответствовать неправильному процессу) и непереносимо ( 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 может потребоваться сделать что-то особенное, чтобы отключить или регулярно очищать буферы.