在 bash 脚本中执行命令,直到输出超过某个值

Posted

技术标签:

【中文标题】在 bash 脚本中执行命令,直到输出超过某个值【英文标题】:execute command in bash script until output exceeds certain value 【发布时间】:2017-07-25 13:04:05 【问题描述】:

我使用一个命令来解析某些帧的视频文件,并在找到时返回它们的时间码。目前,我必须执行命令,等待,直到打印到 stdout 的值到达所需位置,然后使用 Ctrl+C 中止执行。

由于我必须观察该过程并在适当的时候中止执行以获取所需的信息,我想,我可以通过创建一个 bash 脚本在某种程度上自动执行此操作。

我不确定是否可以在 bash 中完成,因为我不完全知道,如何中止与它写入标准输出的值相关的执行。

命令的输出看起来像

0.040000
5.040000
10.040000
15.040000
18.060000
(...)

我试过了

until [[ "$timecode" -gt 30 ]]; do
  timecode=$(mycommand)
  sleep 0.1
done

echo "Result: $timecode"

while [[ "$timecode" -le 30 ]]; do
  timecode=$(mycommand)
  sleep 0.1
done

echo "Result: $timecode"

这似乎都导致命令被执行,直到它完成,然后循环的其余部分被处理。但我想在命令执行时评估输出,并根据输出中断执行。

附加信息

该命令无法在流中的某个点停止。它解析整个文件并给出结果,除非发出停止信号。这是我的第一枪。

该命令的执行时间很长,因为我解析的文件约为 2GB。由于我不需要文件的所有帧,而只需要给定时间码附近的几个帧,因此在完成之前我从不让它执行。

命令的输出因文件而异,因此我无法找到确切的值。如果我知道确切的值,我可能就不必寻找它了。

目标时间码 - 在示例中由“-gt 30”指定 - 对于我必须解析的每个文件都是不同的,因此一旦脚本工作,我必须将其放入命令行参数中。我还必须确保返回的值超过执行的最后一个值,但大约是最后 5 个值。对于这两个我已经有想法了。

我完全被那个卡住了,甚至不知道要谷歌搜索什么。

感谢您的意见!

曼努埃尔


通过 PSkocik 和 Kyle Burton 的回答,我能够将建议的解决方案集成到我的脚本中。它不起作用,我不明白,为什么。

这里是完整的脚本,包括提供输出的外部命令:

 #!/usr/bin/env bash
 set -eu -o pipefail

 parser () 
   local max="$1"
   local max_int

   max_int="$max%.*"

   while read tc;
     do
       local tc_int
       tc_int="$tc%.*"
       echo $tc

       if (( "$tc_int" >= "$max_int" )); then
         echo "Over 30: $tc";
         exec 0>&-
         return 0
       fi

     done
 

 ffprobe "$1" -hide_banner -select_streams v -show_entries frame=key_frame,best_effort_timestamp_time -of csv=nk=1:p=0:s="|" -v quiet | sed -ne "s/^1|//p" | parser 30

我没有从“echo $tc”得到任何输出,但 ffprobe 正在运行 - 我可以在顶部看到它。它一直运行到我使用 Ctrl+C 停止脚本。


感谢凯尔为此付出的巨大努力。我永远不会得出这样的结论。我将 ffprobe 的命令行更改为您的建议

 ffprobe "$1" -hide_banner -select_streams v -show_entries frame=key_frame,best_effort_timestamp_time -of csv=nk=1:p=0:s="|" -v quiet | cut -f2 -d\| | parser 30

现在,我在 ffprobe 运行时得到了结果。但是...您更改命令的方式返回所有帧,ffprobe 发现而不仅仅是关键帧。 ffprobe 命令的原始输出如下所示

 1|0.000000
 0|0.040000
 0|0.080000
 0|0.120000
 0|0.160000
 0|0.200000
 (...)

行首的 0 表示:这不是关键帧。 行首的 1 表示:这是一个关键帧。

该脚本旨在仅提供视频文件特定时间码周围的关键帧。您更改命令的方式现在提供了视频文件的所有帧,这使得结果输出无用。必须过滤掉所有以零开头的行。

由于我不完全理解,为什么这不适用于 sed,我只能尝试通过尝试和错误来找到解决方案,以便使用不同的工具来过滤输出。但如果过滤本身导致问题,我们可能在这里碰壁了。

【问题讨论】:

我想知道读取是否有效。如果您将echo $tc 更改为echo tc=$tc,您会看到类似tc= 的行吗?您可以运行ffprobe <<with those args>> | sed -ne "s/^1|//p" | head 并分享该输出吗?我现在想知道为什么读取失败以及 ffprobe 是否正在打印行 - 所以我试图缩小错误可能出现的位置。 echo $tc 更改为echo "Read: $tc",但没有打印任何内容。 # ffprobe Input.avi -hide_banner -select_streams v -show_entries frame=key_frame,best_effort_timestamp_time -of csv=nk=1:p=0:s="|" -v quiet | sed -ne "s/^1|//p" 打印 0.040000 5.040000 10.040000 15.040000 18.060000 21.980000 26.980000 啊! ffprobe 的输出是空格而不是换行符吗?让我调整我的示例,看看我是否可以将空格转换为换行符并更接近您的需要。我更新了答案以包括使用tr - 试试看? 已经想到你可能会这样做。返回值已被评论系统放入一行。每个值都在一行中。尝试了您的更新,但没有任何改变。当使用包含函数生成值的版本时,我会取回这些值。 好的,这就是我认为当前的事实是关于ffprobe:它每行发出一个浮点数,我们似乎无法逐行读取这些数字。我们确定它不会超过标准错误?我们如何才能将事情缩小到 ffprobe 正在输出什么以及它是如何输出的?如果您要获取上面复制的输出并将其放入文本文件中,那么读取循环将起作用,那么是什么使它无法正常工作呢?你是否可以在某个地方发布一个(小)视频文件,我可以测试这个脚本和ffprobe我自己? 【参考方案1】:

如果您有进程a 将内容输出到标准输出,而进程b 通过管道读取输出内容:

a | b

所有b通常必须在输出某个项目时杀死a 是关闭它的标准输入。

样本b:

b()

    while read w;
        do case $w in some_pattern)exec 0>&-;; esac; 
        echo $w
    done

stdin (filedescriptor 0) 的关闭将导致生产者进程在尝试进行下一次写入时被 SIGPIPE 杀死。

【讨论】:

我目前无法验证这一点,因为我不知道如何将外部执行程序的输出通过管道传输到内部 bash 脚本过程。产生输出的命令是从与应该评估其结果并中断其执行的过程相同的 bash 脚本内部启动的。而且我已经切换到 if 语句,因为 case 是评估模式而不是数值,这使得一切变得更加复杂。【参考方案2】:

我认为 PSkocik 的方法是有道理的。我认为您需要做的就是运行您的 mycommand 并将其通过管道传输到您的 while 循环中。如果您将 PSkocik 的代码放在文件 wait-for-max.sh 中,那么您应该能够将其运行为:

mycommand | bash wait-for-max.sh

在上面的 cmets 中与 M. Uster 合作后,我们提出了以下解决方案:

#!/usr/bin/env bash
set -eu -o pipefail

# echo "bash cutter.sh rn33.mp4"

# From: https://***.com/questions/45304233/execute-command-in-bash-script-until-output-exceeds-certain-value
# test -f stack_overflow_q45304233.tar ||  curl -k -O https://84.19.186.119/stack_overflow_q45304233.tar
# test -f stack_overflow_q45304233.tar ||  curl -k -O https://84.19.186.119/stack_overflow_q45304233.tar
# test -f rn33.mp4 || curl -k -O https://84.19.186.119/rn33.mp4

function parser () 
  local max="$1"
  local max_int

  # NB: this removes everything after the decimal point
  max_int="$max%.*"

  # I added a line number so I could match up the ouptut from this function
  # with the output captured by the 'tee' command
  local lnum="0"
  while read -r tc;
    do

      lnum="$(( 1 + lnum ))"

      # if a blank line is read, just ignore it and continue
     if [ -z "$tc" ]; then
       continue
     fi

     local tc_int
     # NB: this removes everything after the decimal point
     tc_int="$tc%.*"
     echo "Read[$lnum]: $tc"

     if (( "$tc_int" >= "$max_int" )); then
       echo "Over 30: $tc";
       # This closes stdin on this process, which will cause an EOF on the
       # process writing to us across the pipe
       exec 0>&-
       return 0
     fi

    done


# echo "bash version:    $BASH_VERSION"
# echo "ffprobe version: $(ffprobe -version | head -n1)"
# echo "sed version:     $(sed --version | head -n1)"

# NB: by adding in the 'tee ffprobe.out' into the pipeline I was able to see
# that it was producing lines like:
#
# 0|28.520000
# 1|28.560000
#
#
# changing the sed to look for any single digit and a pipe fixed the script
# another option is to use cut, see below, which is probalby more robust.

# ffprobe "$1" \
#   -hide_banner \
#   -select_streams v \
#   -show_entries frame=key_frame,best_effort_timestamp_time \
#   -of csv=nk=1:p=0:s="|" \
#   -v quiet 2>&1 | \
#   tee ffprobe.out |
#   sed -ne "s/^[0-9]|//p" | \
#   parser 30


ffprobe "$1" \
    -hide_banner \
    -select_streams v \
    -show_entries frame=key_frame,best_effort_timestamp_time \
    -of csv=nk=1:p=0:s="|" \
    -v quiet 2>&1 | \
    cut -f2 -d\| | \
    parser 30

【讨论】:

谢谢你的例子。我设法将它集成到我的脚本中,但它还不起作用。我没有从“echo $timecode”中得到任何输出,所以读数似乎没有按预期工作。我现在更新问题。 请注意,在最后一行,您需要将parse-video-frames 替换为您的问题中的mycommand。是否有可能打印到 stderr 而不是 stdout? 现在我更新了问题,包括整个脚本。你可以看到,我按照你的建议做了,没有任何东西被重定向到 stderr。我用它的名字来调用脚本并提供一个文件名作为参数。【参考方案3】:

在 PSkocik 的帮助和 Kyle Burton 的大力支持下,我的问题终于找到了答案。谢谢你们!

我不知道,可以将在脚本中执行的命令的输出通过管道传输到属于该脚本的函数。这是第一条必要的信息。

而且我不知道如何正确评估函数内部的管道信息以及如何从函数内部发出信号,应该终止生成值的命令的执行。

此外,Kyle 发现,我通过将原始输出传输到 sed 并将结果数据传输到脚本内的函数进行的过滤会阻止脚本按设计运行。我仍然不确定,为什么 - 但它确实如此。

生成输出的原始命令现在正按原样传送到脚本的内部函数。过滤是在函数内部完成的,以避免 sed 的问题。现在一切正常,我可以继续完成脚本。

这是灵魂的工作代码:

 #!/usr/bin/env bash
 set -eu -o pipefail

 function parser () 
   local max="$1"
   local max_int

   max_int="$max%.*"

   while read tc;
     do

      #If line is empty, continue
      if [ -z "$tc" ]; then
        continue
      fi

      #If first char is 0 (=non-Index Frame), continue
      local iskey="$tc:0:1";

      if [ $iskey == "0" ]; then
        continue
      fi

      #Return timecode if intended maximum has been reached
      local val="$tc:2:10"
      local tc_int
      tc_int="$val%.*"

      if (( "$tc_int" >= "$max_int" )); then
        echo "First index frame at/after given Timecode: $tc";
        exec 0>&-
        return 0
      fi

     done
 

 ffprobe "$1" -hide_banner -select_streams v -show_entries frame=key_frame,best_effort_timestamp_time -of csv=nk=1:p=0:s="|" -v quiet | parser "$2"

用法:

 ./script.sh "Name of Movie.avi" 30

其中 30 表示搜索并返回下一个找到的索引帧的时间码。

【讨论】:

以上是关于在 bash 脚本中执行命令,直到输出超过某个值的主要内容,如果未能解决你的问题,请参考以下文章

使用bash脚本,编写一个实现将命令行的多个参数逆序输出的程序。

在脚本中使用`until`和`/ usr / bin / timeout`

bash 脚本编程七 将命令输出保存到变量中

bash 脚本编程七 将命令输出保存到变量中

Shell、CMD、PowerShell、Bash

如何从 bash 获取 telnet 命令的输出?