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

bash grep logfiles tail

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

現在は使用しています

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

文字列が見つかると予想通り grep は終了しますが、スクリプトを続行できるように tail コマンドも終了させる方法を見つける必要があります

  66  Alex Hofsteede  2011-04-13


ベストアンサー

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

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

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

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

いくつかの微調整

  • オプション -n0 から tail を指定すると、文字列がログファイル内に以前に存在していた場合、現在のログファイルの最終行から読み込みを開始します
  • tailに-fではなく-fを与えた方がいいかもしれません。これはPOSIXではありませんが、tailが待機中にログを回転させても動作するようにしてくれます
  • m1ではなく-qオプションを指定すると、grepは最初に発生した後に終了しますが、トリガ行は出力されません。また、これはPOSIXであり、-m1はPOSIXではない

48  00prometheus  2015-04-10


受け入れられた答えは私には効かないし、混乱するし、ログファイルを変更してしまう

こんな感じのものを使っています

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

ログ行がパターンと一致した場合、このスクリプトによって開始されたtailをkillします

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

62  Rob Whelan  2012-07-16


Bashを使っている場合(少なくともですが、POSIXでは定義されていないようなので、シェルによっては欠落しているかもしれません)は、構文を使うことができます

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

これは、すでに述べたFIFOソリューションとほぼ同じように動作しますが、よりシンプルに書くことができます

18  petch  2014-09-09


tailを終了させる方法はいくつかあります

アプローチが悪い。tailを強制的に別の行を書かせる

grepがマッチを見つけて終了した直後に、tailに別の行の出力を書かせることができます。これは tailSIGPIPE を取得して終了する原因となります。これを行う一つの方法は、grepが終了した後にtailが監視しているファイルを変更することです

以下にコード例を示します

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

この例では、catgrep が標準出力を閉じるまで終了しないので、tailgrep が標準出力を閉じる前にパイプに書き込むことはできないでしょう。catgrep の標準出力を変更されていない状態で伝搬するために使われます

このアプローチは比較的シンプルですが、いくつかのデメリットがあります

  • grep が標準入力を閉じる前に標準出力を閉じた場合、常に競合状態になる。grep が stdout を閉じ、cat が終了し、echo が終了し、tail が行を出力する。grep が stdin を閉じる前にこの行が grep に送られた場合、tail は別の行を書くまで SIGPIPE を取得しません
  • ログファイルへの書き込みアクセスが必要です
  • ログファイルを変更しても問題ありません
  • たまたま他のプロセスと同時に書き込みを行った場合、ログファイルが破損する可能性があります(書き込みがインターリーブされ、ログメッセージの途中で改行が表示される可能性があります)
  • このアプローチはtailに特有のもので、他のプログラムでは動作しません
  • 3番目のパイプラインステージは、2番目のパイプラインステージのリターンコードにアクセスすることを困難にします (bashPIPESTATUS 配列のような POSIX 拡張を使用している場合を除いて)。grep は常に 0 を返すので、この場合は大したことではありませんが、一般的には、中間のステージは気になるリターンコードを持つ別のコマンドに置き換えられるかもしれません (例えば、”server started” が検出されたときに 0 を返し、”server failed to start” が検出されたときに 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-C を押した場合でも一時的な FIFO が削除されるようにする必要があります。これはトラップを使用して行うことができます

オルタナティブ・アプローチtailを殺すためにメッセージを送る

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

tail -f ... | grep ...のようなパイプラインでは、tailをバックグラウンドにして$!を読み込むことで、tailのPIDを変数に保存するように最初のパイプラインステージを変更するのは簡単です。また、grepが終了したときにkillを実行するように第2のパイプラインステージを修正するのも簡単です。問題は、パイプラインの2つのステージが別々の「実行環境」(POSIX標準の用語では)で実行されるため、2つ目のパイプラインステージは1つ目のパイプラインステージによって設定された変数を読むことができないということです。シェル変数を使わずに、grepが戻ってきたときにtailを殺すことができるように、二番目のステージがtailのPIDをどうにかして見つけ出さなければならないか、もしくはgrepが戻ってきたときに最初のステージがどうにかして通知されなければなりません

第二段階ではpgrepを使ってtailのPIDを取得することができますが、これは信頼性が低く(間違ったプロセスにマッチする可能性があります)、移植性がありません(pgrepはPOSIX標準では指定されていません)

第一段階はechoでPIDをパイプ経由で第二段階に送ることができましたが、この文字列はtailの出力と混ざってしまいます。2つをデマルチプレックスするには、tailの出力に応じて複雑なエスケープスキームが必要になるかもしれません

FIFOを使用して、grepが終了したときに第2のパイプラインステージが第1のパイプラインステージに通知するようにすることができます。そうすると、最初のステージは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は標準入力と標準出力のストリームを完全にバッファリングできるようにしています。これは、tailの出力がgrepによって任意の長い間処理されないかもしれないことを意味します。GNUシステム上では何の問題もないはずです。GNU grepread()を使っているので、バッファリングはすべて回避されていますし、GNU tail -fは標準出力に書き込むときにfflush()を定期的に呼び出します。GNU以外のシステムでは、バッファを無効にしたり、定期的にフラッシュしたりするために特別なことをしなければならないかもしれません

15  Richard Hansen  2013-02-08


@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"

9  Elifarley  2015-05-20


そこで、いくつかのテストをした後、これを動作させるための簡単な1行の方法を見つけました。tail -f は grep が終了したときに終了するように見えるのですが、ちょっと問題があります。これは、ファイルを開いて閉じたときにのみ起動するように見えます。私は、grepがマッチを見つけたときに空の文字列をファイルに追加することで、これを実現しました

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

ファイルの開閉でパイプが閉じていることをテールが認識する理由がよくわからないので、この動作に頼ることはないと思いますが、今のところはうまくいっているようです

閉じてしまう理由は、-F フラグと -f フラグを比較してみてください

6  Alex Hofsteede  2011-04-14


現在のところ、与えられたように、ここにある tail -f のすべての解決策は、以前にログに記録された “Server Started” 行を拾ってしまう危険性があります (ログに記録された行数やログファイルの回転/切り捨てによって、あなたの特定のケースで問題になるかどうかは変わります)

複雑にしすぎるのではなく、bmikeがperlのスニペットで示したように、よりスマートなtailを使えばいいのです。最もシンプルな解決策は、このretailで、開始条件と停止条件のパターンを含む統合された正規表現をサポートしています

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

これは、その文字列の最初の新しいインスタンスが現れるまで、通常のtail -fのようにファイルを追いかけ、終了します。(-u オプションは、通常の “follow” モードの場合、ファイルの最後の 10 行にある既存の行には適用されません)


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を取得して渡すためにプロセスを別々に起動しなければならないからです。FIFOは、a SIGPIPEを受け取るために、tailがタイムリーな書き込みをするためにぶらつくという同じ問題にまだ悩まされています。オプション -n 0tail と一緒に使われ、古い行がマッチしないようにします


最後に、stateful 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

* 注意、同名、前のオプションとは異なるプログラム

CPUを消費するループをさせるのではなく、ファイルのタイムスタンプと状態ファイル(.${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

6  mr.spuratic  2014-07-29


これは、プロセス制御とシグナリングに入る必要があるので、少しトリッキーになります。より厄介なのは、PIDトラッキングを使用して2つのスクリプトソリューションになります。より良いのは、like this. のような名前付きパイプを使用することでしょう

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

手っ取り早くて汚い、一つのスクリプトで解決するには、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  2011-04-13


ファイルが表示されるのを待ちます

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
Attention Required! | Cloudflare

2  Mike  2014-08-04


これ以上にすっきりとした解決策は思いつきません

#!/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

よし、名前は改良の余地があるかもしれない

Advantages:

  • 特別なユーティリティは使用していません
  • ディスクに書き込みません
  • それは優雅に尾を引き、パイプを閉じます
  • かなり短くてわかりやすいです

2  Giancarlo Sportelli  2014-08-12


そのためにテイルは必要ありません。watchコマンドはあなたが探しているものだと思います。watchコマンドはファイルの出力を監視し、出力が変わったときに-gオプションで終了させることができます

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

2  l1zard  2015-02-17


tldr: tailの終端をgrepから切り離す

最も便利なのは2つの形態です

( 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]}

あるいは、何かを出力しているのがtailではない場合

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

2  Ian Kelling  2016-05-09


アレックス……私はこれがあなたの多くを助けると思います

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

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

1  Md. Mohsin Ali  2011-06-19


ここには、ログファイルに書き込む必要のない、非常に危険な、あるいは場合によっては不可能な、はるかに良い解決策があります

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

現在のところ、tailプロセスは次の行がログに書き込まれるまでバックグラウンドで待機します

1  sorin  2013-04-05


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

  • ループ中にロギングプロセスがすでにダウンしていたり、ダウンしていたりした場合、それらは無期限に実行されます
  • 見なければならないログを編集する
  • 不必要に追加ファイルを書き込んでしまいます
  • 追加のロジックを許さない

以下は 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  2015-09-30


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  2016-03-13


inotify(inotifywait)を使ってみます

ファイルの変更を inotifywait で設定して、grep でファイルをチェックして、見つからなければ inotifywait を再実行して、見つかったらループを終了する…。そんな感じです

0  Evengard  2011-04-13


行が書かれたらすぐに退出したいが、タイムアウトしてから退出したいというのもあります

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  2017-06-07


auth.logに行が出た後、ログファイルに行を書く必要がありました

上の答えに触発されて

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

次は私のために働いた

tail -f -n0 /var/log/auth.log | while read LOGLINE;
do [[ "${LOGLINE}" == *"Removed session"* ]] && pkill -P $$ tail;
echo "$(date "+%d-%m-%Y_%H:%M:%S") $(whoami) Screen_Locked poweroff" >>
/var/log/lock-unlock-user.log;
done

0  stefan bambuleac  2020-03-03


これはどうですか?

while true; do if [ ! -z $(grep “myRegEx” myLog.log)] ]; then break; fi ; done

-2  Ather  2019-05-09


タイトルとURLをコピーしました