Execute command in bash script until output exceeds a certain value

I am using a command that parses video files for specific frames and returns their timecode when found. At this point I need to execute a command, wait until the values ​​printed to stdout reach the desired position, then abort execution with Ctrl+ C.

How am I supposed to monitor the process and interrupt execution at the right time to get the information I want, I thought I could automate this to some extent by creating a bash script.

I'm not sure if this can be done in bash, as I don't know exactly how to abort execution due to the values ​​it writes to stdout.

The command output looks like

0.040000
5.040000
10.040000
15.040000
18.060000
(...)

      

I tried

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

echo "Result: $timecode"

      

or

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

echo "Result: $timecode"

      

which appear to cause the command to run until it finishes, and then the rest of the loop is processed. But I want to evaluate the result while the command is executing and aborting depending on the output.

Additional Information

The command cannot be stopped at a certain point in the flow. It analyzes the entire file and gives results if not stopped. This was my first shot.

The command execution time is very long as the files I am looking at are ~ 2GB. Since I don't want all the frames of the file, but only some of them at a given timecode, I never let it execute until it finishes.

The output of the command varies from file to file, so I cannot find the exact value. If I knew the exact meaning, I probably wouldn't have to look for it.

The destination time code - in the example given by "-gt 30" - is different for every file I will need to parse, so I will have to put it in a command line parameter after the script is running, I would also have to make sure that will return more than the last execution value, but about the last 5 values. For these two, I already have ideas.

I'm completely stuck with this and don't even know what to do on google.

Thanks for your input!

Manuel


With the answers from PSkocik and Kyle Burton, I was able to integrate the proposed solution into my script. It doesn't work and I can't see why.

Here's the complete script, including an external command providing the output:

 #!/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

      

I am not getting any output from "echo $ tc", but ffprobe works - I can see it from above. It works until I stop the script using Ctrl+ C.


Thanks to Kyle for your great efforts in this. I never came to that conclusion. I changed the ffprobe command line to your suggestion

 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

      

and now, I get the results while ffprobe is running. But ... the way you changed the command returns all frames, ffprobe finds and not just keyframes. The original output of the ffprobe command looks like

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

      

0 at the beginning of the line means: this is not a keyframe. The 1 at the beginning of the line means: this is a keyframe.

The script is meant to only provide keyframes around a specific timecode of a video file. The way you changed the command now exposes all frames of the video file, making the result useless as a result. It must be filtered for all zero-based lines to be deleted.

As I don't quite understand why this doesn't work with sed, I can only try to find a solution through try and error, making it easier for various tools to filter the output. But if the filtering itself is causing the problem, we could hit the wall.

+1


source to share


3 answers


My question was finally answered with the help of PSkocik and Kyle Burton's intense support. Thanks to both of you!

I didn't know it was possible to pipe the output of commands executed in a script to a function that belongs to the script. This was the first information needed.

And I didn't know how to properly evaluate the information contained in the channel inside the function, and how to signal from inside the function that the execution of the command generating the values ​​should be completed.

Also, Kyle found that the filtering I did by feeding the original output to sed and the resulting data to a function inside the script prevented the script functions from functioning as requested. I'm still not sure why, but this definitively does.

The original command generating the output is now piped, just like the internal script function. The filtering is done inside the function to avoid the sed problem. Everything now works as expected and I can continue executing the script.



This is the soul working code:

 #!/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"

      

Using:

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

      

where 30 represents the timecode in which to search and return the next found index frame.

0


source


If you have a process a

that outputs material to stdout and processes b

that reads the output material through a pipe:

a | b

      

all b

should normally do to kill a

when a certain element is issued is to close its standard input.



Example b:

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

      

This closing stdin (filedescriptor 0) will cause the producer process to be killed by SIGPIPE the moment it tries to make the next write.

+1


source


I think PSkocik's approach makes sense. I think all you have to do is run your command and insert it into a while loop. If you put the PSkocik code in a file wait-for-max.sh

, then you can run it like:

mycommand | bash wait-for-max.sh

After working with M. Uster in the comments above, we came up with the following solution:

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

# echo "bash cutter.sh rn33.mp4"

# From: https://stackoverflow.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

      

0


source







All Articles