web-dev-qa-db-ja.com

文字列が見つかるまでファイルを監視する

積極的に書き込まれているログファイルを監視するためにtail -fを使用しています。特定の文字列がログファイルに書き込まれたら、監視を終了し、スクリプトの残りの部分を続行します。

現在使用しています:

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

文字列が見つかったら、grepは期待どおりに終了しますが、スクリプトを続行できるようにtailコマンドも終了させる方法を見つける必要があります。

58
Alex Hofsteede

シンプルなPOSIXワンライナー

これは簡単なワンライナーです。 bash特有のものやPOSIX以外のもの、あるいは名前付きパイプさえ必要としません。本当に必要なのは、tailの終了をgrepから切り離すことだけです。このように、grepが終了すると、tailがまだ終了していなくてもスクリプトを続行できます。だから、この簡単な方法はそこにあなたを取得します:

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

grepは、文字列が見つかるまでブロックされ、その後すぐに終了します。 tailをそれ自身のサブシェルから実行させることで、それをバックグラウンドに配置して独立して実行することができます。その間、メインシェルはgrepが終了するとすぐにスクリプトの実行を自由に続けることができます。 tailは、次の行がログファイルに書き込まれるまでそのサブシェル内に残り、その後終了します(おそらくメインスクリプトが終了した後でも)。重要な点は、パイプラインがtailの終了を待たなくなったため、grepが終了するとすぐにパイプラインが終了することです。

いくつかのマイナーな調整:

  • オプション-n0〜tailは、文字列がログファイルの前の方に存在する場合に、ログファイルの現在の最後の行から読み取りを開始します。
  • -fではなくtail -Fを指定することをお勧めします。これはPOSIXではありませんが、待機中にログが交代されてもtailが機能するようにします。
  • -m1ではなく-qオプションを指定すると、grepは最初の出現後に終了しますが、トリガー行は表示されません。 POSIXでもあり、-m1は違います。
32
00prometheus

受け入れられた答えは私のために働いていません、そしてそれは混乱していて、それはログファイルを変えます。

私はこのようなものを使っています:

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

ログ行がパターンと一致する場合は、このスクリプトで開始されたtailをkillしてください。

注:画面にも出力を表示したい場合は、| tee /dev/ttyを実行するか、whileループでテストする前に行をエコーし​​てください。

57
Rob Whelan

Bashを使用している場合(少なくとも、POSIXでは定義されていないようなので、一部のシェルでは欠落している可能性があります)、次の構文を使用できます。

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

これは、既に説明したFIFO解決策とほとんど同じように機能しますが、記述がはるかに簡単です。

15
petch

tailを終了するにはいくつかの方法があります。

不十分なアプローチ:tailに強制的に別の行を書き込む

tailが一致を見つけて終了した直後に、grepに出力の別の行を強制的に書き込むことができます。これにより、tailSIGPIPEを取得し、終了します。これを行う1つの方法は、tailの終了後にgrepによって監視されているファイルを変更することです。

コードの例を次に示します。

tail -f logfile.log | grep -m 1 "Server Started" | { cat; echo >>logfile.log; }

この例では、catがstdoutを閉じるまでgrepは終了しません。そのため、tailがstdinを閉じる前に、grepはパイプに書き込むことができません。 catは、grepの標準出力を変更せずに伝搬するために使用されます。

このアプローチは比較的単純ですが、いくつかの欠点があります。

  • grepがstdinを閉じる前にstdoutを閉じる場合、常に競合状態が発生します。grepはstdoutを閉じ、catをトリガーして終了し、echoをトリガーし、tailをトリガーして行を出力します。 grepがstdinを閉じる前にこの行がgrepに送信された場合、tailは別の行を書き込むまでSIGPIPEを取得しません。
  • ログファイルへの書き込みアクセスが必要です。
  • ログファイルを変更しても問題はありません。
  • 別のプロセスと同時に書き込むと、ログファイルが破損する可能性があります(書き込みがインターリーブされ、ログメッセージの途中に改行が表示されることがあります)。
  • このアプローチはtailに固有であり、他のプログラムでは機能しません。
  • 3番目のパイプラインステージでは、2番目のパイプラインステージのリターンコードへのアクセスが困難になります(bashPIPESTATUS配列などのPOSIX拡張を使用している場合を除く)。この場合、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の管理が複雑になることです。一時ファイル名を安全に生成する必要があり、ユーザーがCtrlを押しても一時FIFOが削除されるようにする必要があります-Cスクリプトの途中。これは、トラップを使用して実行できます。

代替アプローチ:tailを殺すためにメッセージを送信する

tailのような信号を送信することで、SIGTERMパイプラインステージを終了できます。課題は、コード内の同じ場所にある2つのことを確実に知ることです:tailのPIDとgrepが終了したかどうか。

tail -f ... | grep ...のようなパイプラインでは、tailをバックグラウンドにして$!を読み取ることで、変数にtailのPIDを保存する最初のパイプラインステージを簡単に変更できます。 killが終了するときにgrepを実行するように2番目のパイプラインステージを変更するのも簡単です。問題は、パイプラインの2つのステージが(POSIX標準の用語で)別個の「実行環境」で実行されるため、2番目のパイプラインステージが最初のパイプラインステージによって設定された変数を読み取れないことです。シェル変数を使用せずに、2番目のステージでtailが返されたときにtailを殺すことができるように、何らかの理由でgrepのPIDを把握するか、またはgrepが返されたときに最初のステージに通知する必要があります。

2番目の段階ではpgrepを使用してtailのPIDを取得できますが、これは信頼性が低く(間違ったプロセスと一致する可能性があります)、移植性がありません(POSIX標準ではpgrepは指定されていません)。

最初のステージは、PIDをechoingすることで、パイプを介してPIDを2番目のステージに送信できますが、この文字列はtailの出力と混合されます。 tailの出力によっては、2つの逆多重化には複雑なエスケープスキームが必要になる場合があります。

FIFOを使用すると、grepが終了したときに、2番目のパイプラインステージが最初のパイプラインステージに通知することができます。その後、最初の段階で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 stdoutへの書き込み時にfflush()を定期的に呼び出します。 GNU以外のシステムは、バッファーを無効にしたり定期的にフラッシュしたりするために、特別なことをしなければならない場合があります。

14
Richard Hansen

@ 00prometheusの回答(これが最良のものです)を拡張してみましょう。

たぶんあなたは無期限に待つのではなくタイムアウトを使うべきです。

以下の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"
8
Elifarley

それで、いくつかのテストをした後、私はこれを機能させる簡単な1行の方法を見つけました。 grepが終了するとtail -fも終了するようですが、キャッチがあります。ファイルが開閉された場合にのみ起動されます。 grepが一致を見つけたときにファイルに空の文字列を追加することでこれを達成しました。

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

なぜファイルのオープン/クローズがパイプが閉じられていることを認識させるために末尾をトリガーするのかわからないので、この動作には頼りません。しかし、それは今のところうまくいくようです。

それが閉じる理由、-Fフラグと-fフラグを比較してください。

6
Alex Hofsteede

現在、与えられているように、ここのすべてのtail -fソリューションは、以前にログに記録された「Server Started」行をピックアップするリスクを実行します(ログに記録された行の数に応じて、およびログファイルのローテーション/切り捨て)。

bmikeがPerlスニペットで示したように、物事を過度に複雑にするのではなく、よりスマートなtailname__を使用するだけです。最も簡単な解決策はこれです retailNAME _ これはstartおよびstop条件パターンと正規表現サポートを統合しています:

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

これは、その文字列の最初のnewインスタンスが現れるまで通常のtail -fのようにファイルをたどり、終了します。 (-uオプションは、通常の「フォロー」モードの場合、ファイルの最後の10行の既存の行でトリガーされません。)


GNU tailname__(coreutilsから)を使用する場合、次の最も簡単なオプションは--pidとFIFO(名前付きパイプ)を使用することです。

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

PIDを取得して渡すためにプロセスを個別に開始する必要があるため、FIFOが使用されます。 FIFOには、tailname__がタイムリーな書き込みのためにハングアップするという同じ問題が依然としてあります SIGPIPE を使用し、--pidオプションを使用して、tailname__が終了するようにしますgrepname__が終了したことに気付きます(従来はreaderではなくwriterプロセスの監視に使用されていましたが、tailname__は実際には気にしません)。オプション-n 0tailname__と共に使用され、古い行が一致をトリガーしないようにします。


最後に、 stateful tail を使用できます。これにより、現在のファイルオフセットが保存されるため、以降の呼び出しでは新しい行のみが表示されます(ファイルの回転も処理します)。この例では、古いFWTK retailname __ *を使用しています。

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

*同じ名前、前のオプションとは異なるプログラムに注意してください。

CPUを占有するループではなく、ファイルのタイムスタンプを状態ファイル(.${LOGFILE}.off)と比較し、スリープします。必要に応じて「-T」を使用して状態ファイルの場所を指定します。上記では現在のディレクトリを想定しています。その条件をスキップしても構いません。Linuxでは、より効率的なinotifywaitname__を代わりに使用できます。

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

あなたはプロセス制御とシグナリングに入らなければならないので、これは少しトリッキーになります。 PID追跡を使用した2つのスクリプトによる解決策は、より洗練されたものになります。このように名前付きパイプ を使うほうがよいでしょう

どのシェルスクリプトを使用していますか?

すばやく汚い、1つのスクリプトソリューション - 私は File:Tail を使用してPerlスクリプトを作成します。

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ループから抜け出してスクリプトを続行させることができます。

どちらも、あなたが探している監視フロー制御を実装するためのほんの少しの学習を含むべきです。

4
bmike

私はこれよりきれいな解決策を想像することはできません:

#!/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コマンドがあなたが探しているものだと思います。 watchコマンドはファイルの出力を監視し、出力が変更されたときに - gオプションで終了させることができます。

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

アレックス私はこれがあなたに大いに役立つと思います。

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

このコマンドはログファイルにエントリを与えませんが、静かにgrepします...

1
Md. Mohsin Ali

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
phio

これは、ログファイルに書き込む必要のない、はるかに優れた解決策です。これは非常に危険な場合もあれば、不可能な場合もあります。

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

現在のところ副作用は1つだけです。tailプロセスは次の行がログに書き込まれるまでバックグラウンドで残ります。

1
sorin

ここでの他の解決策にはいくつかの問題があります。

  • ロギングプロセスがすでに停止しているか、ループ中に停止した場合、それらは無期限に実行されます。
  • 表示するだけのログを編集する
  • 不必要に追加ファイルを書き込む
  • 追加ロジックを許可しない

例として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
user503391

全部読んでください。 tldr:grepからtailの終端を切り離します。

最も便利な2つの形式は

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

そしてあなたが強打を持っているなら

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

しかし、背景に座っているその尾があなたを悩ませるのであれば、fifoや他の答えよりも良い方法がここにあります。 bashが必要です。

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
1
Ian Kelling

あなたはその行が書かれたらすぐに去りたいが、タイムアウトの後も去りたいです。

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
Adrien

Inotify(inotifywait)を使ってみてください

Inotifywaitを設定してファイルが変更されたら、grepでファイルをチェックし、見つからなかった場合はinotifywaitを再実行し、見つかった場合はループを終了します。

0
Evengard