Є кілька способів дістатися tail
до виходу:
Поганий підхід: змусити tail
написати ще один рядок
Ви можете змусити tail
записати інший рядок виводу відразу після того, grep
як знайшли збіг та вийшли. Це призведе tail
до отримання значка SIGPIPE
, що призведе до його виходу. Один із способів зробити це - змінити файл, який контролюється tail
після grep
виходів.
Ось приклад коду:
tail -f logfile.log | grep -m 1 "Server Started" | { cat; echo >>logfile.log; }
У цьому прикладі cat
не вийде, поки grep
не закриє свій stdout, тому tail
, швидше за все, не зможе записати на трубу до grep
того, як змогла закрити stdin. cat
використовується для розповсюдження стандартного виходу grep
немодифікованого.
Цей підхід відносно простий, але є кілька недоліків:
- Якщо
grep
закриття stdout перед закриттям stdin, завжди буде умова перегону: grep
закриває stdout, запускаючи cat
вихід, запускаючи echo
, викликаючи tail
виведення рядка. Якщо цей рядок буде надісланий grep
раніше, grep
він мав можливість закрити stdin, 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
результатами 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, можливо, доведеться зробити щось особливе, щоб відключити або регулярно промивати буфери.