How to read one line from `tail -f` through a pipeline, and then terminate?

I would like to follow changes to a file via tail -f and then print out the first line that matches a grep command before terminating. What's the simplest way to do this? So far I've been experimenting with things like:

tail -f foo.log | grep 'bar' | head -1

However, the pipeline just hangs, presumably due to buffering. I also tried

tail -f foo.log | grep --line-buffered 'bar' | head -1

This prints out the line, but the process does not terminate unless I hit ^C, presumably because a second line of input is needed to terminate head -1. What's the best way to solve this problem?


Solution 1:

tail -f foo.log | grep -m 1 bar

if the file foo.log is writen rarily to, you can do:

grep -m 1 bar <( tail -f foo.log )

It should be noted that the tail -f will stay in background until it will get another line to output. If this takes long time it might be problematic. solution in such case is:

grep -m 1 bar <( exec tail -f foo.log ); kill $! 2> /dev/null

kill will kill leftover tail -f process, and we hide errors, because it's possible that the tail will be gone by the time kill will be invoked.

Solution 2:

The <() construct works in bash only. My solution is:

sleep $timeout &
pid=$!
tail -n +0 --pid=$pid -F "$file" | ( grep -q -m 1 "$regexp" && kill -PIPE $pid )
wait $pid

Besides it's working in a POSIX shell, one other advantage is that the waiting period can be limited with a timeout value. Note that the result code of this scriptlet is reversed: 0 means the timeout was reached.

Solution 3:

tail -f waiting for SIGPIPE, which, as already pointed out by depesz, can only be triggered by another write of tail -f to a closed pipe after a successful grep -m 1 match, can be avoided if the tail -f command gets backgrounded and its background pid being sent to the grep subshell, which in turn implements a trap on exit that kills tail -f explicitly.

#(tail -f foo.log & echo ${!} ) |
(tail -f foo.log & echo ${!} && wait ) | 
   (trap 'kill "$tailpid"; trap - EXIT' EXIT; tailpid="$(head -1)"; grep -m 1 bar)