【问题标题】:Using tail in a subshell in conjunction with while/break does not exit the loop在子 shell 中使用 tail 和 while/break 不会退出循环
【发布时间】:2012-12-05 18:33:03
【问题描述】:

我在使用 shell 脚本时遇到了一个非常特殊的问题。

这里是场景

Script1(在后台生成)--> Script2

Script2 有如下代码

function check_log()
{
    logfile=$1
    tail -5f ${logfile} | while read line
    do
      echo $line
      if echo $line|grep "${triggerword}";then
        echo "Logout completion detected"
        start_leaks_detection
        triggerwordfound=true
        echo "Leaks detection complete"
      fi
      if $triggerwordfound;then
        echo "Trigger word found and processing complete.Exiting"
        break
      fi

    done
        echo "Outside loop"
        exit 0

}

check_log "/tmp/somefile.log" "Logout detected"

现在 while 循环中的中断在这里没有帮助。我可以看到“检测到注销完成”以及“泄漏检测完成”在标准输出上回显,但不是字符串“外部循环”

我假设这与 tail -f 创建子外壳有关。我想要做的是,退出该子外壳并退出 Script2 以将控制权返回给 Script1。

有人可以解释一下如何做到这一点吗?

【问题讨论】:

  • 当你管道某些东西时,你隐含地创建了一个无法修改父环境的叉子(一个孩子)......看看stackoverflow.com/questions/13763942/…
  • 错误链接@F——这与stackoverflow.com/questions/7178888/…相关得多
  • @Neeraj 您可能希望使用case 语句将触发词与输入行进行比较,而不是为每个输入行分叉grepecho 管道。
  • @GregA.Woods 能否请您添加一个使用带有尾部的 case 语句的示例 sn-p 的答案? (我是菜鸟,因此要求)
  • 看看我的答案的编辑版本。

标签: bash shell ksh


【解决方案1】:

试试这个,虽然不太一样(它不会在启动时跳过日志文件的开头):

triggerwordfound=
while [ -z "$triggerwordfound" ]; do
    while read line; do
        echo $line
        if echo $line|grep "${triggerword}";then
            echo "Logout completion detected"
            start_leaks_detection
            triggerwordfound=true
            echo "Leaks detection complete"
        fi
    done
done < "$logfile"
echo "Outside loop"

双循环实际上与tail -f 做同样的事情。

【讨论】:

  • 这应该可行,但最好使用case 语句而不是分叉echogrep 的管道。
  • @GregA.Woods 确实如此。我的第一个方法是if [[ $line =~ $triggerword ]],但后来我记得他没有使用 bash,所以我决定不修改他的代码,因为双循环部分是最有趣的。其余的当然可以改进。
  • 当然你的意思是[ -z "$triggerword" ]test -z "$triggerword"
  • @charles:实际上,我想我的意思一定是[ -z "$triggerwordfound" ],但是我不记得关于这个问题的任何思考过程。
【解决方案2】:

您的函数在某种意义上是有效的,但是直到找到触发词之后将另一行写入文件之前,您不会注意到它这样做了。这是因为tail -5 -f 通常可以在一次write() 调用中将文件的最后五行全部写入管道,并在一次调用中继续写入新行,因此不会发送SIGPIPE 信号直到它尝试在while 循环退出后写入管道。

因此,如果您的文件定期增长,那么应该没有问题,但如果您的文件在写入触发字后立即停止增长更为常见,那么您的观察程序脚本也会挂起,直到任何新的输出被写入文件。

SIGPIPE 不会在管道关闭时立即发送,即使其中缓冲了未读取的数据,但只有在管道上尝试后续 write() 时才会发送。

这可以很简单地证明。此命令不会退出(前提是文件的尾部小于管道大小的缓冲区),直到您手动中断它,或者向文件再写入一个字节:

tail -f some_large_file | read one

但是,如果您强制 tail 对管道进行多次写入并确保读取器在最终写入之前退出,那么一切都会按预期工作:

tail -c 1000000 some_large_file | read one

不幸的是,在给定的系统上发现管道缓冲区的大小并不总是那么容易,也不总是可能只在文件中的数据已经超过管道缓冲区的价值时才开始读取文件,并且触发器word 已经在文件中,并且距离文件末尾至少有一个管道缓冲区的大小字节。

不幸的是,tail -F(您可能应该使用它而不是 -f)也不会尝试每 5 秒写入零字节,否则这可能会以更有效的方式解决您的问题。

另外,如果您要坚持使用tail,那么-1 可能就足够了,至少对于检测任何未来 事件而言。

顺便说一句,这里有一个稍微改进的实现,仍然使用tail,因为我认为这可能是你最好的选择(你总是可以使用cron或类似的方式在日志中添加一个周期性标记行(大多数syslogd实现有一个内置 mark 功能)以保证您的函数将在标记期间返回):

check_log ()
{
        tail -1 -F "$1" | while read line; do
                case "$line" in
                *"${2:-SOMETHING_IMPOSSIBLE_THAT_CANNOT_MATCH}"*)
                        echo "Found trigger word"
                        break
                        ;;
                esac
        done
}

echo 语句替换为读取触发短语时需要执行的任何处理。

【讨论】:

  • 当然,它是一种bashism,但是bash 是可移植的,并且可以在几乎所有服务器环境中工作(必须针对日志文件工作)。对于这种日志文件处理,perl 通常是更好的选择。
【解决方案3】:

不要使用管道进入您的 while 循环,而是使用以下格式:

while read line
do
   # put loop body here
done < <(tail -5f ${logfile})

【讨论】:

  • 双大于号“
  • 有一个空格。 &lt;(command) 称为进程替换,&lt; &lt;(command) 表示将进程的输出重定向到标准输入。
  • 当我尝试这个时,我得到以下错误 ./Automation/check_success.sh: line 20: syntax error near unexpected token &lt;' ./Automation/check_success.sh: line 20: done
  • 这通常是一个比较混乱的形式,它并不能解决OP的问题。
  • @GregA.Woods 是:如果父进程关闭&lt;() 打开的未命名管道,则tail 收到SIGHUP 而不是关闭。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-09-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-04-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多