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.
source to share
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.
source to share
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.
source to share
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
source to share