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)