I sometimes use tail -f
to watch a file,
and also grep
the output for a particular phrase.
For instance, I might look for a URL in an nginx log file. I might
start a screen session, send the output to a log, and filter for
phrases in a long running command. Or use script
, which I
recently learned about, to generate a real time log of
a shell session.
In a perfect world, that's all I need. But sometimes things
go wrong. The web server stops. The long running command
gets killed. And tail -f | grep
don't help me
realize that the output has stopped.
$ tail -f /var/log/nginx/access.log | grep special 1.2.3.4 - - [29/Aug/2018 -0400] "GET /special"… █
$ screen -L $ script -F /tmp/out $ echo "Read me back the last line"
$ tail -f screenlog.0 | grep Read Read me back the last line █
$ tail -f /tmp/out | grep Read Read me back the last line █
What would be nice would be a spinner to indicate that output is still being generated.
It would also be nice if the spinner stops spinning if there is no output.
So, here's a little program to accomplish that. I called it tailgrep
.
I don't reimplement tail
-- I'm too lazy to think about opening and seeking
to the end of a file -- I just use make a Proc::Async
object
and then watch stdout.
There are some subtleties -- whenever
looks like a loop but isn't.
It sets up callbacks. And react
declares an event loop. start
puts it in a separate thread.
Also you have to get the order right: Make the Proc::Async
object,
then call .stdout
to create a Supply
. Set up your the callbacks on your event loop
with whenever
s.
Then start
the process.
It returns a Promise
which you can await
.
What else.
shell "tput 'civis'";
(and cnorm
)
make the cursor invisible (or visible). signal(SIGINT)
is another Supply
that is called whenever ^C
is pressed (i.e. a sigint signal is received).
< >
splits up a string into an array.
<<...>>
interpolates too. $++
maintains
a stateful variable and (post-)increments it.
#!/usr/bin/env perl6 sub spinner() { <\ - | - / ->[$++ % 6] } sub MAIN($expr, $filename) { shell "tput 'civis'"; my $proc = Proc::Async.new: <<tail -f $filename>>; my $out = $proc.stdout; start react { whenever $out.lines.grep( / "$expr" / ) { .say } whenever $out.lines { print spinner() ~ "\r"; } whenever signal(SIGINT) { shell 'tput cnorm'; exit; } } await $proc.start; }
$ tailgrep special /var/log/nginx/access.log
1.2.3.4 - - [29/Aug/2018 -0400] "GET /special"…
Oh, yeah, MAIN($expr,$filename)
declares both the
parameters and the command line arguments, and also generates
a usage message which is displayed when you start the
program with -h
.
$ ./tailgrep -h
Usage:
./tailgrep <expr> <filename>
Not bad for 25 lines of code, but let's do a bit more.
First I want to ensure that the file exists. To do this,
I add a constraint on the $filename
parameter.
sub MAIN($expr, $filename where *.IO.e) {
Also let's write a message if there hasn't been output for a few seconds.
We make another Supply
that calls a little routine
every second to check for that.
my $last-seen = DateTime.now; ... whenever $out.lines { print spinner() ~ "\r"; $last-seen = DateTime.now; } whenever Supply.interval(1) { if DateTime.now - $last-seen > $wait { say "--no lines for $wait seconds--"; } }
And by "a few seconds" I mean whatever numeric value is given on the command line, but let's default to 2.
sub MAIN( $expr, #= what to search for $filename where *.IO.e, #= a filename to grep Numeric :$wait = 2, #= when to notify ) {
Did I mention that inline comments attached to the parameters become messages in the help output?
Or that named
parameters (which start with a :
) become named
command line arguments?
./tailgrep -h Usage: ./tailgrep [--wait=<Numeric>] <expr> <filename> <expr> what to search for <filename> a filename to grep --wait=<Numeric> when to notify
That about wraps it up, but just for fun, I also decided to
look for some nicer unicode spinners and add some color.
For these features I needed to use a few modules that
may need to be installed separately.
(zef install JSON::Fast Terminal::ANSIColor
).
So, I found some spinners here, with a nice JSON file, so we just download that and store it locally.
#!/usr/bin/env perl6 use JSON::Fast; use Terminal::ANSIColor; my @frames = < / - | - \ - >; sub spinner { @frames[$++ % +@frames]; } sub download-spinners { my $store = "{ %*ENV<HOME> }/.spinners.json"; unless $store.IO.e { say "downloading spinners"; my $url='https://raw.githubusercontent.com' ~ '/sindresorhus/cli-spinners' ~ '/HEAD/spinners.json'; shell "curl -s $url > $store"; } from-json( $store.IO.slurp ); }
Also multi-dispatch is handy -- a named boolean argument for another dispatch candidate can show a list of all the spinners.
multi MAIN( Bool :$list-spinners!, #= list spinners ) { say download-spinners.keys.sort.join("\t"); } ...
Here's the final program with the fancy unicode spinners. And here's the less fancy one (no dependencies).
curl -L https://git.io/fA8MV > ~/bin/tg-fancy
curl -L https://git.io/fA8MM > ~/bin/tailgrep
Just chmod +x
and there you are.
$ tg-fancy -h Usage: tg-fancy --list-spinners tg-fancy [--wait=<Numeric>] [--spinner=<Any>] <expr> <filename> --list-spinners list spinners <expr> what to search for <filename> a filename to grep --wait=<Numeric> when to notify --spinner=<Any> which spinner
$ tg-fancy --spinner=pong tailgrep access.log
$ tg-fancy --spinner=weather tailgrep access.log
edit: 2018-09-07: updated animations