监视文件直到找到字符串

Ale*_*ede 70 bash grep tail logfiles

我正在使用 tail -f 来监视正在积极写入的日志文件。当某个字符串被写入日志文件时,我想退出监控,并继续我的脚本的其余部分。

目前我正在使用:

tail -f logfile.log | grep -m 1 "Server Started"
Run Code Online (Sandbox Code Playgroud)

找到字符串后,grep 按预期退出,但我需要找到一种方法使 tail 命令也退出,以便脚本可以继续。

小智 65

接受的答案对我不起作用,而且令人困惑并且会更改日志文件。

我正在使用这样的东西:

tail -f logfile.log | while read LOGLINE
do
   [[ "${LOGLINE}" == *"Server Started"* ]] && pkill -P $$ tail
done
Run Code Online (Sandbox Code Playgroud)

如果日志行与模式匹配,则tail终止此脚本启动的。

注意:如果您还想在屏幕上查看输出| tee /dev/tty,请在 while 循环中测试之前回显或回显该行。

  • 这有效,但 POSIX 未指定 `pkill`,并且并非在任何地方都可用。 (2认同)
  • 您不需要 while 循环。使用带有 -g 选项的 watch,您可以省去讨厌的 pkill 命令。 (2认同)

00p*_*eus 59

一个简单的 POSIX one-liner

这是一个简单的单线。它不需要特定于 bash 或非 POSIX 的技巧,甚至不需要命名管道。您真正需要的只是将tailfrom的终止解耦grep。这样,一旦grep结束,即使tail尚未结束,脚本也可以继续。所以这个简单的方法会让你到达那里:

( tail -f -n0 logfile.log & ) | grep -q "Server Started"
Run Code Online (Sandbox Code Playgroud)

grep将阻塞直到找到字符串,然后它将退出。通过tail从它自己的子 shell 运行,我们可以将它置于后台使其独立运行。同时,主 shell 可以在grep退出后立即继续执行脚本。tail将停留在其子 shell 中,直到下一行写入日志文件,然后退出(甚至可能在主脚本终止后)。重点是管道不再等待tail终止,所以管道一退出就grep退出。

一些小的调整:

  • 选项 -n0tail使其从日志文件的当前最后一行开始读取,以防日志文件中较早存在字符串。
  • 您可能想要给出tail-F 而不是 -f。它不是 POSIX,但tail即使在等待时轮换日志,它也可以工作。
  • 选项 -q 而不是 -m1grep在第一次出现后退出,但不打印触发行。它也是 POSIX,而 -m1 不是。

  • 这种方法将使“tail”永远在后台运行。您将如何在后台子外壳中捕获“tail” PID 并将其暴露在主外壳中?我只能通过使用“pkill -s 0 tail”杀死所有会话附加的“tail”进程来提出子可选解决方法。 (6认同)
  • 在大多数用例中,这应该不是问题。您首先这样做的原因是因为您希望将更多行写入日志文件。`tail` 将在尝试写入损坏的管道时立即终止。一旦 `grep` 完成,管道就会中断,因此一旦 `grep` 完成,`tail` 将在日志文件中再增加一行后终止。 (2认同)
  • @Trevor Boyd Smith,是的,这在大多数情况下都有效,但 OP 问题是 grep 在 tail 退出之前不会完成,并且 tail 在日志文件中出现另一行之前不会退出 _after_ grep 退出(当 tail 尝试退出时)馈送被 grep 结尾破坏的管道)。因此,除非您后台跟踪,否则您的脚本将不会继续执行,直到日志文件中出现额外的行,而不是恰好在 grep 捕获的行上。 (2认同)

小智 20

如果您使用的是 Bash(至少,但它似乎不是由 POSIX 定义的,因此在某些 shell 中可能会丢失),您可以使用语法

grep -m 1 "Server Started" <(tail -f logfile.log)
Run Code Online (Sandbox Code Playgroud)

它的工作原理与已经提到的 FIFO 解决方案非常相似,但编写起来要简单得多。

  • @mems,日志文件中的任何附加行都可以。`tail` 将读取它,尝试输出它,然后接收将终止它的 SIGPIPE。所以,原则上你是对的;如果没有任何东西再次写入日志文件,`tail` 可能会无限期运行。在实践中,这对很多人来说可能是一个非常巧妙的解决方案。 (3认同)

Ric*_*sen 15

有几种方法可以tail退出:

糟糕的方法:强制tail写另一行

您可以tailgrep找到匹配项并退出后立即强制写入另一行输出。这将导致tailget a SIGPIPE,导致它退出。一种方法是tailgrep退出后修改正在监视的文件。

下面是一些示例代码:

tail -f logfile.log | grep -m 1 "Server Started" | { cat; echo >>logfile.log; }
Run Code Online (Sandbox Code Playgroud)

在此示例中,在关闭其标准输出cat之前不会退出grep,因此tailgrep有机会关闭其标准输入之前不太可能写入管道。 cat用于传播grep未修改的标准输出。

这种方法比较简单,但有几个缺点:

  • 如果grep在关闭stdin之前关闭stdout,总会有一个竞争条件: grep关闭stdout,触发cat退出,触发echo,触发tail输出一行。如果此行发送到grep之前grep有机会关闭标准输入,tail则在SIGPIPE写入另一行之前不会获得。
  • 它需要对日志文件的写访问权限。
  • 您必须同意修改日志文件。
  • 如果您碰巧与另一个进程同时写入,则可能会损坏日志文件(写入可能是交错的,导致在日志消息中间出现换行符)。
  • 这种方法专门针对 tail- 它不适用于其他程序。
  • 第三个管道阶段很难访问第二个管道阶段的返回代码(除非您使用 POSIX 扩展,例如bashPIPESTATUS数组)。在这种情况下这不是什么大问题,因为它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}"
Run Code Online (Sandbox Code Playgroud)

# optional可以删除标有注释的行,程序仍然可以运行;tail会一直徘徊,直到它读取另一行输入或被其他进程杀死。

这种方法的优点是:

  • 你不需要修改日志文件
  • 该方法适用于除此之外的其他公用事业 tail
  • 它不会受到竞争条件的影响
  • 您可以轻松获得grep(或您正在使用的任何替代命令)的返回值

这种方法的缺点是复杂,尤其是管理 FIFO:您需要安全地生成一个临时文件名,并且您需要确保临时 FIFO 被删除,即使用户在中间按下 Ctrl-C剧本。这可以使用陷阱来完成。

替代方法:发送消息杀死 tail

您可以通过向tail管道发送类似的信号来退出管道阶段SIGTERM。挑战在于可靠地知道代码中同一位置的两件事: tail的 PID 和是否grep已退出。

使用类似 的管道tail -f ... | grep ...,很容易修改第一个管道阶段,tail通过后台处理tail和读取将PID保存在变量中$!。修改第二个管道阶段以killgrep退出时运行也很容易。问题是管道的两个阶段在单独的“执行环境”(POSIX 标准的术语)中运行,因此第二个管道阶段无法读取第一个管道阶段设置的任何变量。如果不使用 shell 变量,要么第二阶段必须以某种方式找出tail的 PID,以便它可以tailgrep返回时终止,要么必须在grep返回时以某种方式通知第一阶段。

第二阶段可以pgrep用来获取tail的 PID,但这将是不可靠的(您可能匹配错误的进程)并且不可移植(pgrepPOSIX 标准未指定)。

第一阶段可以通过管道将 PID 发送到第二阶段echo,但该字符串将与tail的输出混合。将两者解复用可能需要复杂的转义方案,具体取决于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}"
Run Code Online (Sandbox Code Playgroud)

这种方法具有前一种方法的所有优点和缺点,只是它更复杂。

关于缓冲的警告

POSIX 允许完全缓冲 stdin 和 stdout 流,这意味着tail的输出可能不会在grep任意长的时间内被处理。在 GNU 系统上不应该有任何问题:GNUgrep使用read(),它避免了所有缓冲,并且 GNU在写入 stdout 时tail -f会定期调用fflush()。非 GNU 系统可能需要做一些特殊的事情来禁用或定期刷新缓冲区。


Eli*_*ley 9

让我扩展@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
}
Run Code Online (Sandbox Code Playgroud)

也许在启动服务器后日志文件还不存在。在这种情况下,您应该在搜索字符串之前等待它出现:

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))
}
Run Code Online (Sandbox Code Playgroud)

使用方法如下:

wait_server "/var/log/server.log" 5m && \
echo -e "\n-------------------------- Server READY --------------------------\n"
Run Code Online (Sandbox Code Playgroud)


mr.*_*tic 7

目前,正如给定的那样,tail -f这里的所有解决方案都存在选择先前记录的“服务器启动”行的风险(在您的特定情况下,这可能是也可能不是问题,具体取决于记录的行数和日志文件轮换/截断)。

与其让事情过于复杂tail,不如使用更智能的,就像bmike用 perl snippit 展示的那样。最简单的解决方案是retail将正则表达式支持与启动停止条件模式集成在一起:

retail -f -u "Server Started" server.log > /dev/null
Run Code Online (Sandbox Code Playgroud)

这将像正常一样跟随文件,tail -f直到出现该字符串的第一个实例,然后退出。(-u在正常的“跟随”模式下,该选项不会在文件的最后 10 行中的现有行上触发。)


如果您使用 GNU tail(来自coreutils),下一个最简单的选择是使用--pidFIFO(命名管道):

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

使用 FIFO 是因为必须单独启动进程才能获得和传递 PID。FIFO 仍然面临同样的问题,即及时写入导致tail接收SIGPIPE,使用该--pid选项以便tail在它注意到grep终止时退出(通常用于监视写入器进程而不是读取器,但tail不真的不在乎)。Option-n 0与 with 一起使用,tail以便旧行不会触发匹配。


最后,您可以使用有状态的 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
Run Code Online (Sandbox Code Playgroud)

* 注意,同名,与上一个选项不同的程序。

将文件的时间戳与状态文件 ( .${LOGFILE}.off)进行比较,而不是使用 CPU 占用循环,然后睡眠。-T如果需要,使用“ ”来指定状态文件的位置,以上假设为当前目录。随意跳过该条件,或者在 Linux 上,您可以使用更高效的方法inotifywait

retail "${LOGFILE:=server.log}" > /dev/null
while true; do
    inotifywait -qq "${LOGFILE}" && 
       retail "${LOGFILE}" | grep -q "Server Started" && break
done
Run Code Online (Sandbox Code Playgroud)


Ale*_*ede 6

所以在做了一些测试之后,我找到了一种快速的 1 行方式来完成这项工作。当 grep 退出时, tail -f 似乎会退出,但有一个问题。它似乎只有在打开和关闭文件时才会触发。我通过在 grep 找到匹配项时将空字符串附加到文件中来完成此操作。

tail -f logfile |grep -m 1 "Server Started" | xargs echo "" >> logfile \;
Run Code Online (Sandbox Code Playgroud)

我不确定为什么文件的打开/关闭会触发 tail 以意识到管道已关闭,所以我不会依赖这种行为。但它现在似乎有效。

它关闭的原因,请查看 -F 标志与 -f 标志。