1

I want to kill a bash command when I found some string in the output.

To clarify, I want the solution to be similar to a timeout command:

timeout 10s looping_program.sh

Which will execute the script: looping_program.sh and kill the script after 10 seconds of execute.

Instead I want something like:

regexout "^Success$" looping_program.sh

Which will execute the script until it matches a line that just says Success in the stdout of the program. Note that I'm assuming that this looping_program.sh does not exit at the same time it outputs Success for whatever reason, so simply waiting for the program to exit would waste time if I don't care about what happens after that.

So something like:

bash -e looping_program.sh > /tmp/output &
PID="$(ps aux | grep looping_program.sh | head -1 | tr -s ' ' | cut -f 2 -d ' ')"
echo $PID
while :; do
  echo "$(tail -1 /tmp/output)"
  if [[ "$(tail -1 /tmp/output)" == "Success" ]]; then
    kill $PID
    exit 0
  fi
  sleep 1
done

Where looping_program.sh is something like:

echo "Fail"
sleep 1;
echo "Fail"
sleep 1;
echo "Fail"
sleep 1;
echo "Success"
sleep 1;
echo "Fail"
sleep 1;
echo "Fail"
sleep 1;
echo "Fail"
sleep 1;

But that is not very robust (uses a single tmp file... might kill other programs...) and I want it to just be one command. Does something like this exist? I may just write a c program to do it if not.

P.S.: I provided my code as an example of what I wanted the program to do. It does not use good programming practices. Notes from other commenters:

@KamilCuk Do not use temporary file. Use a fifo.

@pjh Note that any approach that involves using kill with a PID in shell code runs the risk of killing the wrong process. Use kill in shell programs only when it is absolutely necessary.

There are more suggestions below from other users, I just wanted to make sure no one came across this and thought it would be good to model their code after.

Grifball
  • 756
  • 1
  • 5
  • 14
  • `PID="$(ps aux | grep looping_program.sh ...` [`?!`](https://unix.stackexchange.com/questions/30370/how-to-get-the-pid-of-the-last-executed-command-in-shell-script) would be more robust – erik258 Feb 15 '23 at 18:17
  • 8
    Do you care about the output preceding `^Success$`? If not, you can write `looping_program.sh | grep -q '^Success$'`. When `grep` finds the first match, it will exit, and your `looping_program.sh` will exit the next time it tries to write to the now-closed pipe. (Or at least, it will receive a SIGPIPE signal, for which the default handler is to exit the program.) – chepner Feb 15 '23 at 18:20
  • @erik258, please no; grepping the whole process tree just to find a child of the current shell is massively fragile and utterly pointless. Sure, the OP was doing that already, but it's certainly not a practice to encourage when one can just use `myproc & myproc_pid=$!` to start a process and then store its PID immediately, no matter if there are other programs running with the `myproc` name or not. – Charles Duffy Feb 15 '23 at 18:56
  • @chepner Oh, that works great too... KamilCuk's answer is nice too if I want more flexibility like if I also want to output to stdout to see all the "Fail" lines. – Grifball Feb 15 '23 at 20:10

3 Answers3

2
looping_program() {
   for i in 1 2 3; do echo $i; sleep 1; done
   echo Success
   yes
}
coproc looping_program
while IFS= read -r line; do
   if [[ "$line" =~ Success ]]; then
      break
   fi
done <&${COPROC[0]}
exec {COPROC[0]}>&- {COPROC[1]}>&-
kill ${COPROC_PID}
wait ${COPROC_PID}

Notes:

  • Do not use temporary file. Use a fifo.
  • Do not use tail -n1 to read last line. Read from the stream in a loop.
  • Do not repeat tail -1 twice. Cache the result.
  • Wait for pid after killing to synchronize.
  • When you're using a coprocess, use COPROC_PID to get the PID
  • When you're not using a coprocess, use $! to get the PID of a background process started from the current shell.
  • When you can't use $! (because the process you're trying to get a PID of was not spawned in the background as a direct child of the current shell), do not use ps aux | grep to get the pid. Use pgrep.
  • Do not use echo $(stuff). Just run the stuff, no echo.
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
KamilCuk
  • 120,984
  • 8
  • 59
  • 111
  • Thanks! This seems to be working great... I don't fully understand all the syntax for coproc though. Also, I had to use stdbuf to ensure the command I was calling would actually flush stdout for me to read. This technique can be read here: https://stackoverflow.com/questions/1429951/force-flushing-of-output-to-a-file-while-bash-script-is-still-running – Grifball Feb 15 '23 at 20:03
1

With

#!/usr/bin/env -S expect -f
set timeout -1
spawn ./looping_program.sh
expect "Success"
send -- "\x03"
expect eof

Call it looping_killer:

$ ./looping_killer
spawn ./looping_program.sh
Fail
Fail
Fail
Success
^C

To pass the program and pattern:

./looping_killer some_program "some pattern"

You'd change the expect script to

#!/usr/bin/env -S expect -f
set timeout -1
spawn [lindex $argv 0]
expect -- [lindex $argv 1]
send -- "\x03"
expect eof
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • Cool! Had to ```apt get expect```... never used it before, but it looks like a powerful tool. Do you know how to modify this script to send arguments to it? Like: replace ./looping_program.sh with ```$1``` and similar for ```"Success"``` ? – Grifball Feb 17 '23 at 20:16
0

Assuming that your looping program exists when it tries to write to a broken pipe, this will print all output up to and including the 'Success' line and then exit:

./looping_program | sed '/^Success$/q'

Note that any approach that involves using kill with a PID in shell code runs the risk of killing the wrong process. Use kill in shell programs only when it is absolutely necessary.

pjh
  • 6,388
  • 2
  • 16
  • 17