How can I kill a process when a specific string is

2020-03-01 18:07发布

I need to start a process, lets say foo. I would like to see the stdout/stderr as normal, but grep the stderr for string bar. Once bar is found in the stderr foo should be killed.

Is this possible?

标签: bash grep stderr
5条回答
贪生不怕死
2楼-- · 2020-03-01 18:13

I actually managed to figure out a way to do this without PID files or co-routines and in a way that should work in all POSIX-compatible shells (I've tried bash and dash). At least on systems that support /dev/fd/, but that should be pretty much all of them.

It is a bit convoluted, though, so I'm not sure if it is to your liking.

(                               # A
    (                           # B
        ( /tmp/foo 2>&1 1>&3 & echo $! >&4 ) |              # C
        ( tee /dev/fd/2 | ( grep -q bar && echo fin >&4 ) ) # D and E
    ) 4>&1 | (                  # F
        read CHILD
        read STATUS
        if [ "$STATUS" = fin ]; then
            kill $CHILD
        fi
    )
) 3>&1

To explain the numerous subshells used herein:

The body of A runs with the normal stdout duplicated to fd 3. It runs the subshells B and F with the stdout of B piped to the stdin of F.

The body of B runs with the pipe from A duplicated on fd 4.

C runs your actual foo command, with its stderr connected to a pipe from C to D and its stdout duplicated from fd 3; that is, restored to the global stdout. It then writes the PID of foo to fd 4; that is, to the pipe that subshell F has on its stdin.

D runs a tee command receiving, from the pipe, whatever foo prints on its stderr. It copies that output to both /dev/fd/2 (in order to have it displayed on the global stderr) and to a pipe connected to subshell E.

E greps for bar and then, when found, writes fin on fd 4, that is, to the pipe that F has on its stdin. Note the &&, making sure that no fin is written if grep encounters EOF without having found bar.

F, then, reads the PID from C and the fin terminator from E. If the fin terminator was properly output, it kills foo.

EDIT: Fixed the missing tee to copy foo's stderr to the real stderr.

查看更多
【Aperson】
3楼-- · 2020-03-01 18:19

I initially wrote a way to do this that involved stream swizzling, but it wasn't very good. Some of the comments relate to that version. Check the history if you're curious.

Here's a way to do this:

(PIDFILE=$(mktemp /tmp/foo.XXXXXX) && trap "rm $PIDFILE" 0 \
   && { foo \
           2> >(tee >(grep -q bar && kill $(cat $PIDFILE)) >&2) \
        & PID=$! && echo $PID >$PIDFILE ; wait $PID || true; })

Good old-fashioned nightmare fuel. What's happening here?

  1. The outermost parentheses put the whole thing in a subshell; this constrains the scope of variables, for purposes of hygeine
  2. We create a temporary file, using a syntax which works with both GNU and BSD mktemp, and call it PIDFILE
  3. We set up a catch-all exit trap (which runs when the outermost subshell exits) to remove the file named by PIDFILE, again for hygeine
  4. We run foo; this is done in a compound statement so that & binds to foo and not to the whole preceding pipeline
  5. We redirect foo's standard error into a process substitution which waits for bar to appear and then kills foo (of which more later)
  6. We capture foo's PID into a variable, write it to the file named by PIDFILE, then wait for it, so that the whole command waits for foo to exit before itself exiting; the || true discards the error exit status of foo when that happens.

The code inside the process substitution works as follows:

  1. First, tee the input (foo's standard error), redirecting tee's standard output to standard error, so that foo's standard error does indeed appear on standard error
  2. Send the copy of the input going to a file going to another process substitution (a process substitution within a process substitution)
  3. Within the deeper process substitution, firstly run grep -q on the input, which looks for the specified pattern, and exits as soon as it finds it (or when it reaches the end of the stream), without printing anything, after which (if it found the string and exited successfully) the shell goes on to ...
  4. kill the process whose PID is captured in the file named by PIDFILE, namely foo
查看更多
兄弟一词,经得起流年.
4楼-- · 2020-03-01 18:20

Tom Anderson’s answer is quite good, but the kill $(cat $PIDFILE) will only happen on my system if foo terminated on its own, or through Ctrl-C. The following solution works for me

while read g
do
  if [[ $g =~ bar ]]
  then
    kill $!
  fi
done < <(
  exec foo 2> >(tee /dev/tty)
)
查看更多
来,给爷笑一个
5楼-- · 2020-03-01 18:36

Use Expect to Monitor Standard Error

Expect is designed for taking actions based on output from a process. The simplest solution is to simply let Expect start the process, then exit when it sees the expected output. For example:

expect -c 'set msg {Saw "foo" on stderr. Exiting process.}
           spawn /bin/bash -c "echo foo >&2; sleep 10"
           expect "foo" { puts $msg; exit }'

If the spawned process ends normally (e.g. before "foo" is seen), then the Expect script will exit, too.

查看更多
相关推荐>>
6楼-- · 2020-03-01 18:37

Just as an alternative to the other answer, one way would be to use bash's coproc facility:

{coproc FOO { foo; } 2>&1 1>&3; } 3>&1
CHILD=$!
while read line <&${FOO[0]}; do
    if echo "$line" | grep -q bar; then
        kill $CHILD
    else
        echo "$line"
    fi
done

That's clearly bash-specific, though.

查看更多
登录 后发表回答