tailgrep

楽土

tailgrep

跟随一个文件, 过滤行, 观察 spinner.

我有时会使用 tail -f 来观察文件,还可以使用 grep 输出特定的短语。

$ tail -f /var/log/nginx/access.log | grep special
1.2.3.4 - - [29/Aug/2018 -0400] "GET /special"…
█

例如,我可能会在 nginx 日志文件中查找 URL。我可能会启动一个屏幕会话,将输出结果发送到日志中,然后在一个长时间运行的命令中过滤短语。或者使用我最近了解到的脚本来生成 shell 会话的实时日志。

$ 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
█

在一个完美的世界里,这就是我所需要的一切。但有时事情会出错。 Web 服务器停止了。长时间运行的命令被杀死。而 tail -f | grep 并不能帮我意识到输出已经停止。

$ tail -f /tmp/out | grep Read
Read me back the last line
█

如果能有一个旋转器来显示输出仍在产生, 那就更好了。

如果在没有输出的情况下,旋转器停止旋转也会很好。

/

所以,这是有一个小程序来实现这一目标。我把它叫做 tailgrep

我没有重新实现 tail - 我懒得考虑打开并搜索文件的结尾 - 我只是制作了一个 Proc::Async 对象然后看标准输出。

有一些微妙之处 - whenever 看起来像一个循环, 但不是。它设置了回调。而 react 声明了一个事件循环。 start 把它放在一个单独的线程中。

另外,你还得把顺序搞对。先创建 Proc::Async 对象,然后调用 .stdout 来创建一个 Supply。使用 whenever 在事件循环上设置回调。然后启动(start)这个过程。

它返回一个你可以等待(await)的 Promise

还有什么。

shell "tput 'civis'"; shell "tput 'cnorm'"; 使光标不可见(或可见)。 signal(SIGINT)是每当按下 ^C 时调用的另一个 Supply(即接收到 sigint 信号)。

< > 将字符串拆分为数组。 << ... >> 进行插值。 $++ 维护一个状态变量并(后)增加它。

#!/usr/bin/env raku
 
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"…

img

哦,对了,MAIN($expr,$filename) 声明了参数和命令行参数,并且还生成一个使用信息,当你使用 -h 添加启动程序时, 这个信息就会显示出来。

$ ./tailgrep -h
Usage:
  ./tailgrep <expr> <filename>

对于 25 行代码来说还不错,但让我们再多做一点。

首先,我想确保文件已存在。要做到这一点,我在 $filename 参数上添加了一个约束。

sub MAIN($expr, $filename where *.IO.e) {

如果几秒钟内没有输出,我们也写一条消息。

我们再做一个 Supply, 每秒钟调用一个小的例程来检查。

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--";
    }
  }

我们所说的"几秒钟"指的是命令行上给出的任何数值,但是默认为 2。

sub MAIN(
  $expr,                   #= what to search for
  $filename where *.IO.e,  #= a filename to grep
  Numeric :$wait = 2,      #= when to notify
) {

我有没有说过, 附加在参数上的内联注释会成为帮助输出中的消息?

或者说命名的参数(以 : 开头的参数)会变成命名的命令行参数?

./tailgrep -h
Usage:
  ./tailgrep [--wait=<Numeric>] <expr> <filename>

    <expr>              what to search for
    <filename>          a filename to grep
    --wait=<Numeric>    when to notify

这就是我的想法,但为了好玩,我还决定寻找一些更好的 unicode spinners,并添加一些颜色。对于这些功能,我需要使用一些可能需要单独安装的模块。

zef install JSON::Fast Terminal::ANSIColor

所以,我在这里找到了一些 spinners,带有一个很好的 JSON 文件,所以我们只需下载它并将其存储在本地。

#!/usr/bin/e nü
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 );
}

另外多重分派也很方便 - 另一个分派候选者的命名布尔参数可以显示所有 spinners 的列表。

multi MAIN(
    Bool :$list-spinners!, #= list spinners
) {
    say download-spinners.keys.sort.join("\t");
}
...

这里是带有花哨的 unicode spinners 的最终程序。这里 是一个不那么花哨的程序(没有依赖)。

如果你想尝试一下,这里有一些方便的网址供 curling。

curl -L https://git.io/fA8MV > ~/bin/tg-fancy
curl -L https://git.io/fA8MM > ~/bin/tailgrep

执行 chmod +x, 完了。

img

结论

  • 混合和匹配你的 CLI 实用工具以获得乐趣,以改善你的生活质量。
  • 线程、Promise 和 Supplies 是异步编程的方便工具。
  • Spinners 不只是用于烦躁不安。有时他们很有用。

comments powered by Disqus