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
に別の行の出力を書かせることができます。これは tail
が SIGPIPE
を取得して終了する原因となります。これを行う一つの方法は、grep
が終了した後にtail
が監視しているファイルを変更することです
以下にコード例を示します
tail -f logfile.log | grep -m 1 "Server Started" | { cat; echo >>logfile.log; }
この例では、cat
は grep
が標準出力を閉じるまで終了しないので、tail
は grep
が標準出力を閉じる前にパイプに書き込むことはできないでしょう。cat
は grep
の標準出力を変更されていない状態で伝搬するために使われます
このアプローチは比較的シンプルですが、いくつかのデメリットがあります
grep
が標準入力を閉じる前に標準出力を閉じた場合、常に競合状態になる。grep
が stdout を閉じ、cat
が終了し、echo
が終了し、tail
が行を出力する。grep
が stdin を閉じる前にこの行がgrep
に送られた場合、tail
は別の行を書くまでSIGPIPE
を取得しません- ログファイルへの書き込みアクセスが必要です
- ログファイルを変更しても問題ありません
- たまたま他のプロセスと同時に書き込みを行った場合、ログファイルが破損する可能性があります(書き込みがインターリーブされ、ログメッセージの途中で改行が表示される可能性があります)
- このアプローチは
tail
に特有のもので、他のプログラムでは動作しません - 3番目のパイプラインステージは、2番目のパイプラインステージのリターンコードにアクセスすることを困難にします (
bash
のPIPESTATUS
配列のような 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 grep
はread()
を使っているので、バッファリングはすべて回避されていますし、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 0
は tail
と一緒に使われ、古い行がマッチしないようにします
最後に、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
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