50

Я использую tail -f для мониторинга файла журнала, в который ведется активная запись. Когда в файл журнала записывается определенная строка, я хочу выйти из режима мониторинга и продолжить работу с остальным сценарием.

В настоящее время я использую:

tail -f logfile.log | grep -m 1 "Server Started"

Когда строка найдена, grep завершает работу, как и ожидалось, но мне нужно найти способ заставить команду tail выйти так, чтобы скрипт мог продолжаться.

18 ответов18

54

Принятый ответ не работает для меня, плюс он сбивает с толку и меняет файл журнала.

Я использую что-то вроде этого:

tail -f logfile.log | while read LOGLINE
do
   [[ "${LOGLINE}" == *"Server Started"* ]] && pkill -P $$ tail
done

Если строка журнала соответствует шаблону, уничтожьте tail запущенный этим сценарием.

Примечание: если вы хотите также просмотреть вывод на экране, либо | tee /dev/tty или повторить строку перед тестированием в цикле while.

26

Простой POSIX с одним вкладышем

Вот простой однострочник. Для этого не нужны специфичные для bash или не POSIX трюки, или даже именованный канал. Все, что вам действительно нужно, это отделить окончание tail от grep . Таким образом, когда grep заканчивается, скрипт может продолжаться, даже если tail еще не закончился. Итак, этот простой метод доставит вас туда:

( tail -f -n0 logfile.log & ) | grep -q "Server Started"

grep будет блокировать, пока не найдет строку, после чего выйдет. Заставив tail работать из его собственной суб-оболочки, мы можем поместить его в фоновом режиме, чтобы он работал независимо. Тем временем основная оболочка может продолжить выполнение скрипта, как только выйдет grep . tail будет задерживаться в своей вложенной оболочке до тех пор, пока в лог-файл не будет записана следующая строка, а затем завершится (возможно, даже после завершения основного сценария). Суть в том, что конвейер больше не ждет завершения tail , поэтому конвейер завершается сразу после выхода grep .

Некоторые незначительные изменения:

  • Опция -n0 для tail заставляет его начать чтение с текущей последней строки файла журнала, если строка существует ранее в файле журнала.
  • Возможно, вы захотите указать tail -F, а не -f. Это не POSIX, но он позволяет tail работать, даже если журнал вращается во время ожидания.
  • Опция -q вместо -m1 заставляет grep выйти после первого появления, но без распечатки строки триггера. И это POSIX, а не -m1.
14

Есть несколько способов получить 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 может потребоваться сделать что-то особенное, чтобы отключить или регулярно очищать буферы.

13

Если вы используете Bash (по крайней мере, но кажется, что он не определен POSIX, поэтому он может отсутствовать в некоторых оболочках), вы можете использовать синтаксис

grep -m 1 "Server Started" <(tail -f logfile.log)

Он работает почти так же, как уже упоминавшиеся решения FIFO, но гораздо проще в написании.

8

Позвольте мне расширить ответ @ 00promeheus (который является лучшим).

Может быть, вы должны использовать тайм-аут, а не ждать бесконечно.

Приведенная ниже функция bash будет блокироваться до тех пор, пока не появится заданное условие поиска или не истечет заданное время ожидания.

Статус выхода будет 0, если строка найдена в течение времени ожидания.

wait_str() {
  local file="$1"; shift
  local search_term="$1"; shift
  local wait_time="${1:-5m}"; shift # 5 minutes as default timeout

  (timeout $wait_time tail -F -n0 "$file" &) | grep -q "$search_term" && return 0

  echo "Timeout of $wait_time reached. Unable to find '$search_term' in '$file'"
  return 1
}

Возможно, файл журнала еще не существует только после запуска вашего сервера. В этом случае вам следует подождать, пока он появится, прежде чем искать строку:

wait_server() {
  echo "Waiting for server..."
  local server_log="$1"; shift
  local wait_time="$1"; shift

  wait_file "$server_log" 10 || { echo "Server log file missing: '$server_log'"; return 1; }

  wait_str "$server_log" "Server Started" "$wait_time"
}

wait_file() {
  local file="$1"; shift
  local wait_seconds="${1:-10}"; shift # 10 seconds as default timeout

  until test $((wait_seconds--)) -eq 0 -o -f "$file" ; do sleep 1; done

  ((++wait_seconds))
}

Вот как вы можете использовать это:

wait_server "/var/log/server.log" 5m && \
echo -e "\n-------------------------- Server READY --------------------------\n"
6

Так что после некоторого тестирования я нашел быстрый способ с 1 строкой сделать эту работу. Похоже, tail -f выйдет, когда выйдет grep, но есть одна загвоздка. Похоже, он срабатывает только в том случае, если файл открыт и закрыт. Я сделал это, добавив пустую строку в файл, когда grep найдет совпадение.

tail -f logfile |grep -m 1 "Server Started" | xargs echo "" >> logfile \;

Я не уверен, почему открытие / закрытие файла вызывает хвост, чтобы понять, что канал закрыт, поэтому я бы не стал полагаться на это поведение. но, похоже, сейчас работает.

Причины закрытия: посмотрите на флаг -F, а не на флаг -f.

6

В настоящее время, как указано, все решения tail -f здесь подвергаются риску обнаружения ранее зарегистрированной строки "Server Started" (которая может или не может быть проблемой в вашем конкретном случае, в зависимости от количества зарегистрированных строк и журнала ротация / усечение файла).

Вместо того, чтобы чрезмерно усложнять вещи, просто используйте более умный tail , как показал bmike с фрагментом perl. Самое простое решение - это retail которую встроена поддержка регулярных выражений с шаблонами условий запуска и остановки :

retail -f -u "Server Started" server.log > /dev/null

Это будет следовать за файлом как обычный tail -f пока не появится первый новый экземпляр этой строки, а затем завершится. (Опция -u не срабатывает на существующие строки в последних 10 строках файла в обычном режиме "follow".)


Если вы используете GNU tail (из coreutils), следующий простейший вариант - использовать --pid и FIFO (именованный канал):

mkfifo ${FIFO:=serverlog.fifo.$$}
grep -q -m 1 "Server Started" ${FIFO}  &
tail -n 0 -f server.log  --pid $! >> ${FIFO}
rm ${FIFO}

FIFO используется, потому что процессы должны запускаться отдельно, чтобы получить и передать PID. ФИФО по- прежнему страдает от тех же проблем торчать для своевременной записи , чтобы вызвать tail получить в SIGPIPE, используйте --pid вариант , так что tail выходит , когда он замечает , что grep окончилась (обычно используются для контроля за процесс , а не писатель читатель, но tail самом деле не волнует). Опция -n 0 используется с tail чтобы старые строки не вызывали совпадения.


Наконец, вы можете использовать хвост с сохранением состояния, при этом будет сохраняться текущее смещение файла, поэтому последующие вызовы будут отображать только новые строки (он также обрабатывает поворот файла). Этот пример использует старую FWTK retail *:

retail "${LOGFILE:=server.log}" > /dev/null   # skip over current content
while true; do
    [ "${LOGFILE}" -nt ".${LOGFILE}.off" ] && 
       retail "${LOGFILE}" | grep -q "Server Started" && break
    sleep 2
done

* Примечание, то же имя, программа отличается от предыдущей опции.

Вместо того, чтобы зацикливаться на процессоре, сравните временную метку файла с файлом состояния (.${LOGFILE}.off) и спать. Используйте « -T », чтобы указать местоположение файла состояния, если требуется, вышеупомянутый предполагает текущий каталог. Не стесняйтесь пропустить это условие, или в Linux вы можете использовать вместо этого более эффективный inotifywait :

retail "${LOGFILE:=server.log}" > /dev/null
while true; do
    inotifywait -qq "${LOGFILE}" && 
       retail "${LOGFILE}" | grep -q "Server Started" && break
done
4

Это будет немного сложнее, так как вам придется войти в управление процессом и сигнализацию. Больше kludgey было бы решением с двумя сценариями, использующим отслеживание PID Лучше бы использовать именованные каналы, как это.

Какой сценарий оболочки вы используете?

Для быстрого и грязного, одного сценария решения - я бы сделал Perl-скрипт, используя File:Tail

use File::Tail;
$file=File::Tail->new(name=>$name, maxinterval=>300, adjustafter=>7);
while (defined($line=$file->read)) {
    last if $line =~ /Server started/;
}

Поэтому вместо того, чтобы печатать внутри цикла while, вы можете отфильтровать совпадения строк и выйти из цикла while, чтобы продолжить выполнение сценария.

Любой из них должен включать в себя лишь небольшое обучение для реализации контроля потока наблюдения, который вы ищете.

2

Я не могу представить себе более чистого решения, чем это:

#!/usr/bin/env bash
# file : untail.sh
# usage: untail.sh logfile.log "Server Started"
(echo $BASHPID; tail -f $1) | while read LINE ; do
    if [ -z $TPID ]; then
        TPID=$LINE # the first line is used to store the previous subshell PID
    else
        echo "$LINE"; [[ "$LINE" == *"${*:2}"* ]] && kill -3 $TPID && break
    fi
done

ок, может быть имя может быть улучшено ...

Преимущества:

  • он не использует никаких специальных утилит
  • он не записывает на диск
  • он грациозно покидает хвост и закрывает трубу
  • это довольно коротко и легко понять
2

дождитесь появления файла

while [ ! -f /path/to/the.file ] 
do sleep 2; done

дождитесь появления строки в файле

while ! grep "the line you're searching for" /path/to/the.file  
do sleep 10; done

https://superuser.com/a/743693/129669

2

Вам не нужен хвост, чтобы сделать это. Я думаю, что команда часов это то, что вы ищете. Команда watch контролирует вывод файла и может быть прервана с помощью опции -g при изменении вывода.

watch -g grep -m 1 "Server Started" logfile.log && Yournextaction
1

Алекс, я думаю, что это поможет тебе.

tail -f logfile |grep -m 1 "Server Started" | xargs echo "" >> /dev/null ;

эта команда никогда не даст запись в лог-файл, но будет молча grep ...

1

Команда tail может быть фоновой, а ее pid отражен в подоболочке grep . В подоболочке grep обработчик прерываний на EXIT может уничтожить команду tail .

( (sleep 1; exec tail -f logfile.log) & echo $! ; wait ) | 
     (trap 'kill "$pid"' EXIT; pid="$(head -1)"; grep -m 1 "Server Started")
1

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

sh -c 'tail -n +0 -f /tmp/foo | { sed "/EOF/ q" && kill $$ ;}'

В настоящее время он имеет только один побочный эффект, tail процесс будет оставаться в фоновом режиме, пока следующая строка не будет записана в журнал.

1

Другие решения здесь имеют несколько проблем:

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

Вот что я придумал, используя tomcat в качестве примера (удалите хэши, если вы хотите видеть журнал во время его запуска):

function startTomcat {
    loggingProcessStartCommand="${CATALINA_HOME}/bin/startup.sh"
    loggingProcessOwner="root"
    loggingProcessCommandLinePattern="${JAVA_HOME}"
    logSearchString="org.apache.catalina.startup.Catalina.start Server startup"
    logFile="${CATALINA_BASE}/log/catalina.out"

    lineNumber="$(( $(wc -l "${logFile}" | awk '{print $1}') + 1 ))"
    ${loggingProcessStartCommand}
    while [[ -z "$(sed -n "${lineNumber}p" "${logFile}" | grep "${logSearchString}")" ]]; do
        [[ -z "$(ps -ef | grep "^${loggingProcessOwner} .* ${loggingProcessCommandLinePattern}" | grep -v grep)" ]] && { echo "[ERROR] Tomcat failed to start"; return 1; }
        [[ $(wc -l "${logFile}" | awk '{print $1}') -lt ${lineNumber} ]] && continue
        #sed -n "${lineNumber}p" "${logFile}"
        let lineNumber++
    done
    #sed -n "${lineNumber}p" "${logFile}"
    echo "[INFO] Tomcat has started"
}
1

Прочитайте их все. tldr: отделить окончание хвоста от grep.

Две наиболее удобные формы

( tail -f logfile.log & ) | grep -q "Server Started"

и если у вас есть Баш

grep -m 1 "Server Started" <(tail -f logfile.log)

Но если этот хвост, сидящий на заднем плане, вас беспокоит, есть более хороший способ, чем пятерка или любой другой ответ здесь. Требуется Баш.

coproc grep -m 1 "Server Started"
tail -F /tmp/x --pid $COPROC_PID >&${COPROC[1]}

Или, если это не хвост, который выводит вещи,

coproc command that outputs
grep -m 1 "Sever Started" ${COPROC[0]}
kill $COPROC_PID
0

Вы хотите уйти, как только строка написана, но вы также хотите уйти после тайм-аута:

if (timeout 15s tail -F -n0 "stdout.log" &) | grep -q "The string that says the startup is successful" ; then
    echo "Application started with success."
else
    echo "Startup failed."
    tail stderr.log stdout.log
    exit 1
fi
0

Попробуйте использовать inotify (inotifywait)

Вы настраиваете inotifywait для любого изменения файла, затем проверяете файл с помощью grep, если он не найден, просто перезапустите inotifywait, если он найден, выйдите из цикла ... Что-то вроде этого

Всё ещё ищете ответ? Посмотрите другие вопросы с метками .