持续监控日志,直到匹配某个模式

27

我想在一个文件上执行tail -F,直到匹配某个模式。我找到了一种使用awk的方法,但是我认为我的命令不够简洁。问题在于,因为一些限制,我必须只能使用一行代码。

tail -n +0 -F /tmp/foo | \
awk -W interactive '{if ($1 == "EOF") exit; print} END {system("echo EOF >> /tmp/foo")}'

尾部命令会一直阻塞,直到文件中出现EOF指示结束。该方式非常有效。必须加上END块,因为awk的exit不会立即退出,而是在退出前执行awkeval END块。由于tail的原因,END块会在读取调用时挂起,所以我需要做的最后一件事情就是在文件中写入另一行,以强制tail退出。

有人知道更好的解决方法吗?


你也可以在http://superuser.com/questions/270529/monitoring-a-file-until-a-string-is-found上找到正确的答案。 - mems
有些相关的答案:https://superuser.com/questions/270529/monitoring-a-file-until-a-string-is-found 和 https://dev59.com/bGYr5IYBdhLWcg3wDWJm - xx1xx
10个回答

43
使用tail命令的--pid选项,当shell终止时,tail会停止运行。不需要对被追踪文件进行额外操作。
sh -c 'tail -n +0 --pid=$$ -f /tmp/foo | { sed "/EOF/ q" && kill $$ ;}'

1
不错的改进!这应该是被接受的答案。 - Richard Neish
有没有办法摆脱“7616 Terminated sh -c ...”这一行,至少当我从脚本中使用它时? - Sampo
2
如果您的tail命令不支持--pid选项,则可以使用sh -i -c 'tail -n +0 -f /tmp/foo | { sed "/EOF/ q" && kill 0 ;}'。_sh -i_创建一个新的进程组,而kill 0则会杀死当前进程组中的所有进程。 - Michal Sojka
注意:这段代码在BusyBox中不可用,因此在Alpine上也无法使用。 - Błażej Michalik
@sampo 你可以使用 grep -v "Terminated sh -c" 命令。它会打印出所有不匹配的行。 - Cadoiz

35

试试这个:

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

一旦在 /tmp/foo 中看到字符串“EOF”,整个命令行都将退出。

有一个副作用: tail 进程将保持运行(在后台),直到有任何内容被写入到 /tmp/foo


1
只是解释一下:"sh -c"用于在子shell中运行管道,并能够检索子shell PID。行末的"$ $"将扩展为此子shell的PID。我的sed脚本应该与您的awk脚本执行相同的操作(即,显示它看到的所有内容,并在遇到字符串“EOF”时退出)。一旦sed找到了“EOF”字符串,“kill”将终止子shell。挂起的“tail”进程将保留,因为它将在/tmp/foo上循环。一旦有东西写入文件,tail将回显它,并在{...}部分已退出后终止。 - jpetazzo
1
请参考@GregBarrett的答案,以解决悬挂尾进程的问题。 - Richard Neish

10

我没有通过这个解决方案得到结果:

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

由于缓冲区的问题,如果文件中没有更多的行被添加,则sed将不会读取输入。因此,经过进一步研究,我得出了以下结论:

sed '/EOF/q' <(tail -n 0 -f /tmp/foo)

这个脚本可以在 https://gist.github.com/2377029 找到。


在搜索了StackOverflow和Unix Stack Exchange之后,这是我在Mac上唯一有效的答案。我正在将其用于Xcode预构建脚本中。 - Luc

5
这是Tcl擅长的事情。如果下面的内容是“tail_until.tcl”,
#!/usr/bin/env tclsh

proc main {filename pattern} {
    set pipe [open "| tail -n +0 -F $filename"]
    set pid [pid $pipe]
    fileevent $pipe readable [list handler $pipe $pattern]
    vwait ::until_found
    catch {exec kill $pid}
}

proc handler {pipe pattern} {
    if {[gets $pipe line] == -1} {
        if {[eof $pipe]} {
            set ::until_found 1
        }
    } else {
        puts $line
        if {[string first $pattern $line] != -1} {
            set ::until_found 1
        }
    }
}

main {*}$argv

那么你可以执行 tail_until.tcl /tmp/foo EOF


甜,除了我必须把它保持成一行代码。 - Sam Alba
@shad:一行代码而不需要额外的文件?-( - Donal Fellows

4
这对你有用吗?
tail -n +0 -F /tmp/foo | sed '/EOF/q'

我假设你要找的是“EOF”模式。当sed命令找到该模式时,它会退出,这意味着tail应该在下一次写入时退出。
我认为如果该模式在文件末尾附近被找到,则有可能tail会等待文件中出现更多输出,而这将永远不会发生。如果这真的是一个问题,那么你可能可以安排杀死它 - 整个管道将在sed终止时终止(除非你使用一个决定这不是正确行为的有趣的shell)。

对Bash的不满

正如所担心的那样,bash(至少在MacOS X上,但可能是任何地方)是一个shell,它认为它需要等待tail完成,即使sed已经退出。有时候 - 比我喜欢的更频繁 - 我更喜欢老式的Bourne shell的行为,因为它没有那么聪明,所以犯错的机会比Bash小。dribbler是一个程序,每秒滴出一个消息(例如“1:Hello”),输出到标准输出。在Bash中,此命令序列会挂起,直到我在单独的窗口中执行'echo pqr >>/tmp/foo'。
date
{ timeout -t 2m dribbler -t -m Hello; echo EOF; } >/tmp/foo &
echo Hi
sleep 1   # Ensure /tmp/foo is created
tail -n +0 -F /tmp/foo | sed '/EOF/q'
date

很遗憾,我没有立即看到控制此行为的选项。我找到了shopt lithist,但这与此问题无关。
万岁Korn Shell
我注意到,当我使用Korn shell运行该脚本时,它按照我的期望工作-留下一个等待被某种方式杀死的tail。在第二个日期命令完成后,'echo pqr >> /tmp/foo'有效。

我真的很喜欢你详细的解释。你的第一个命令本来可能是最好的选择,但是当我没有指定END块时,sed的行为与awk相同。所以我想暂时保留我的awk命令。 - Sam Alba

3
这是Jon解决方案的扩展版,使用sed而不是grep,以便tail的输出进入stdout:
sed -r '/EOF/q' <( exec tail -n +0 -f /tmp/foo ); kill $! 2> /dev/null

这行代码的运行原理是sed在tail之前创建,因此$!保存了tail的进程ID。
相较于sh -c的解决方案,这种方法的主要优势在于杀死sh会在输出中打印一些不受欢迎的信息,如“Terminated”。

我在ksh中没有成功。kill $!实际上杀死了最后一个后台作业,而不是tail命令。 - Scott McIntyre

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

这里的主要问题出在$$.

如果您按原样运行命令,$$将被设置为当前运行命令的shell进程ID而不是sh。

为了使kill命令正常工作,您需要将kill $$更改为kill \$$

之后,您可以安全地摆脱传递给tail命令的--pid=$$

总结一下,以下命令将正常工作:

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

你可以选择使用-n参数在不打扰的情况下执行sed命令 :)


1

准备好用于Tomcat =

sh -c 'tail -f --pid=$$ catalina.out | { grep -i -m 1 "Server startup in" && kill $$ ;}'

对于上述场景:

sh -c 'tail -f   --pid=$$ /tmp/foo | { grep -i -m 1 EOF && kill $$ ;}'

这正是我想要的(检查Tomcat启动),谢谢! - Brian Minton

1
为了杀死悬空的"tail"进程,您可以在(Bash)进程替换上下文中执行"tail"命令,这样它就可以像后台进程一样被终止。(代码取自如何通过管道读取'tail -f'的一行,然后终止?)。
: > /tmp/foo
grep -m 1 EOF <( exec tail -f /tmp/foo ); kill $! 2> /dev/null
echo EOF > /tmp/foo  # terminal window 2

作为替代方案,您可以使用命名管道。

(
: > /tmp/foo
rm -f pidfifo
mkfifo pidfifo
sh -c '(tail -n +0 -f /tmp/foo & echo $! > pidfifo) | 
{ sed "/EOF/ q" && kill $(cat pidfifo) && kill $$ ;}'
)

echo EOF > /tmp/foo  # terminal window 2

-3
tail -f <filename> | grep -q "<pattern>"

这将不会打印任何内容,直到匹配到模式,然后不会停止tail。失败。 - ams

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接