11

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?

jonderry
  • 1,027

5 Answers5

14
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.

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.

1

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)
suro
  • 11
0

Building on answer https://superuser.com/a/275962/37154 , I realized that if the tail would start too late, some instance of the indicated string may have gone by already. The solution is to make tail list the whole file.

grep -m 1 bar <( exec tail -n +1 -f foo.log ); kill $!
clacke
  • 338
  • 1
  • 3
  • 13
-1

I think that applying unbuffer like this,

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

would work. I'm not at a system where I can test it, though, and I can't justify why it would work while grep --line-buffered does not. However, I did try this alternative which does seem to work:

tail -f foo.log | sed -n '/bar/{p;q}'

When the match to bar is found, p prints it and q immediately quits.

garyjohn
  • 36,494