積極的に書き込まれているログファイルを監視するためにtail -fを使用しています。特定の文字列がログファイルに書き込まれたら、監視を終了し、スクリプトの残りの部分を続行します。
現在使用しています:
tail -f logfile.log | grep -m 1 "Server Started"
文字列が見つかったら、grepは期待どおりに終了しますが、スクリプトを続行できるようにtailコマンドも終了させる方法を見つける必要があります。
これは簡単なワンライナーです。 bash特有のものやPOSIX以外のもの、あるいは名前付きパイプさえ必要としません。本当に必要なのは、tail
の終了をgrep
から切り離すことだけです。このように、grep
が終了すると、tail
がまだ終了していなくてもスクリプトを続行できます。だから、この簡単な方法はそこにあなたを取得します:
( tail -f -n0 logfile.log & ) | grep -q "Server Started"
grep
は、文字列が見つかるまでブロックされ、その後すぐに終了します。 tail
をそれ自身のサブシェルから実行させることで、それをバックグラウンドに配置して独立して実行することができます。その間、メインシェルはgrep
が終了するとすぐにスクリプトの実行を自由に続けることができます。 tail
は、次の行がログファイルに書き込まれるまでそのサブシェル内に残り、その後終了します(おそらくメインスクリプトが終了した後でも)。重要な点は、パイプラインがtail
の終了を待たなくなったため、grep
が終了するとすぐにパイプラインが終了することです。
いくつかのマイナーな調整:
tail
は、文字列がログファイルの前の方に存在する場合に、ログファイルの現在の最後の行から読み取りを開始します。tail
-Fを指定することをお勧めします。これはPOSIXではありませんが、待機中にログが交代されてもtail
が機能するようにします。grep
は最初の出現後に終了しますが、トリガー行は表示されません。 POSIXでもあり、-m1は違います。受け入れられた答えは私のために働いていません、そしてそれは混乱していて、それはログファイルを変えます。
私はこのようなものを使っています:
tail -f logfile.log | while read LOGLINE
do
[[ "${LOGLINE}" == *"Server Started"* ]] && pkill -P $$ tail
done
ログ行がパターンと一致する場合は、このスクリプトで開始されたtail
をkillしてください。
注:画面にも出力を表示したい場合は、| tee /dev/tty
を実行するか、whileループでテストする前に行をエコーしてください。
Bashを使用している場合(少なくとも、POSIXでは定義されていないようなので、一部のシェルでは欠落している可能性があります)、次の構文を使用できます。
grep -m 1 "Server Started" <(tail -f logfile.log)
これは、既に説明したFIFO解決策とほとんど同じように機能しますが、記述がはるかに簡単です。
tail
を終了するにはいくつかの方法があります。
tail
に強制的に別の行を書き込むtail
が一致を見つけて終了した直後に、grep
に出力の別の行を強制的に書き込むことができます。これにより、tail
がSIGPIPE
を取得し、終了します。これを行う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
に固有であり、他のプログラムでは機能しません。bash
のPIPESTATUS
配列などの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をecho
ingすることで、パイプを介して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以外のシステムは、バッファーを無効にしたり定期的にフラッシュしたりするために、特別なことをしなければならない場合があります。
@ 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"
それで、いくつかのテストをした後、私はこれを機能させる簡単な1行の方法を見つけました。 grepが終了するとtail -fも終了するようですが、キャッチがあります。ファイルが開閉された場合にのみ起動されます。 grepが一致を見つけたときにファイルに空の文字列を追加することでこれを達成しました。
tail -f logfile |grep -m 1 "Server Started" | xargs echo "" >> logfile \;
なぜファイルのオープン/クローズがパイプが閉じられていることを認識させるために末尾をトリガーするのかわからないので、この動作には頼りません。しかし、それは今のところうまくいくようです。
それが閉じる理由、-Fフラグと-fフラグを比較してください。
現在、与えられているように、ここのすべてのtail -f
ソリューションは、以前にログに記録された「Server Started」行をピックアップするリスクを実行します(ログに記録された行の数に応じて、およびログファイルのローテーション/切り捨て)。
bmikeがPerlスニペットで示したように、物事を過度に複雑にするのではなく、よりスマートなtail
name__を使用するだけです。最も簡単な解決策はこれです retail
NAME _ これはstartおよびstop条件パターンと正規表現サポートを統合しています:
retail -f -u "Server Started" server.log > /dev/null
これは、その文字列の最初のnewインスタンスが現れるまで通常のtail -f
のようにファイルをたどり、終了します。 (-u
オプションは、通常の「フォロー」モードの場合、ファイルの最後の10行の既存の行でトリガーされません。)
GNU tail
name__(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には、tail
name__がタイムリーな書き込みのためにハングアップするという同じ問題が依然としてあります SIGPIPE を使用し、--pid
オプションを使用して、tail
name__が終了するようにしますgrep
name__が終了したことに気付きます(従来はreaderではなくwriterプロセスの監視に使用されていましたが、tail
name__は実際には気にしません)。オプション-n 0
はtail
name__と共に使用され、古い行が一致をトリガーしないようにします。
最後に、 stateful tail を使用できます。これにより、現在のファイルオフセットが保存されるため、以降の呼び出しでは新しい行のみが表示されます(ファイルの回転も処理します)。この例では、古いFWTK retail
name __ *を使用しています。
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
name__を代わりに使用できます。
retail "${LOGFILE:=server.log}" > /dev/null
while true; do
inotifywait -qq "${LOGFILE}" &&
retail "${LOGFILE}" | grep -q "Server Started" && break
done
あなたはプロセス制御とシグナリングに入らなければならないので、これは少しトリッキーになります。 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ループから抜け出してスクリプトを続行させることができます。
どちらも、あなたが探している監視フロー制御を実装するためのほんの少しの学習を含むべきです。
私はこれよりきれいな解決策を想像することはできません:
#!/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
わかりました、多分名前は改良を受けることができます….
利点:
ファイルが現れるのを待つ
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
あなたはそれをする必要はありません。 watchコマンドがあなたが探しているものだと思います。 watchコマンドはファイルの出力を監視し、出力が変更されたときに - gオプションで終了させることができます。
watch -g grep -m 1 "Server Started" logfile.log && Yournextaction
アレックス私はこれがあなたに大いに役立つと思います。
tail -f logfile |grep -m 1 "Server Started" | xargs echo "" >> /dev/null ;
このコマンドはログファイルにエントリを与えませんが、静かにgrepします...
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")
これは、ログファイルに書き込む必要のない、はるかに優れた解決策です。これは非常に危険な場合もあれば、不可能な場合もあります。
sh -c 'tail -n +0 -f /tmp/foo | { sed "/EOF/ q" && kill $$ ;}'
現在のところ副作用は1つだけです。tail
プロセスは次の行がログに書き込まれるまでバックグラウンドで残ります。
ここでの他の解決策にはいくつかの問題があります。
例として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"
}
全部読んでください。 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
あなたはその行が書かれたらすぐに去りたいが、タイムアウトの後も去りたいです。
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
Inotify(inotifywait)を使ってみてください
Inotifywaitを設定してファイルが変更されたら、grepでファイルをチェックし、見つからなかった場合はinotifywaitを再実行し、見つかった場合はループを終了します。