シェル機能をインプリメントしたいなら Term::ShellUI を使うと便利

Web::Scraperのshellをhistoryから補完できるようにしてみました。

shell系は使わないと忘れてしまうので、historyがあったほうがいいかなと思って作ってみました。

~/.scraperhistoryを作っておくと、historyが使えるようになります。

Web::Scraperのshellでhistory補完 - dann's blog - #

おお,たしかに history があると便利。Term::ReadLine ってちょろっと設定するだけでヒストリ保存とかしてくれなかったっけ?と思ったら,別のモジュールでした。

以前*1も使いましたが,Term::ShellUI という CPAN モジュールを使うと高度なシェル機能を簡単にインプリメントすることができます。

特色は,

  • 各コマンドへのサブルーチンリファレンスを指定するだけ
  • エイリアスも指定できる
  • desc を指定しておくと,$term->help_call() 等でヘルプメッセージをだせる
  • args を指定すると,コンテキスト依存な補完ができる(zsh みたいな感じで)
  • history_file を指定すると,履歴をよしなにファイルに保存してくれる
  • 自分で Features を調べる必要はない

これらの機能を実現するために自分の手でコードを書く必要はありません。設定子として書いておくだけです。

使い方は,

  1. Term::ShellUI を設定込みで new() する
    • new() したあとにプロパティとして設定をいじることもできます
  2. $term->run() すると,シェルプロンプトループが開始する
    • EOD とかしない限り,基本的に戻ってきません

です。

diff 形式だと Perl のコードがみづらくなるので,抜粋で。

# ...... snip snip snip ......

use Config;
use Term::ShellUI;
use Data::Dumper;
use HTML::Entities;
use URI;
use Web::Scraper;
use YAML;

# ...... snip snip snip ......

my $stuff   = process_args($ARGV[0])
    or die "Usage: scraper [URI-or-filename]\n";

my $scraper = scraper { exec_shellui($_[0]) };
   $scraper->user_agent->env_proxy;

my $result  = $scraper->scrape($stuff);

# ...... snip snip snip ......

sub exec_shellui {
    my ($tree) = @_;
    local $YAML::UseCode = $YAML::UseCode = 1;
    my $term_setting = Load(<<'END_SHELL_SETTING');
---
  app: scraper

  prompt: !!perl/code |
    { 'scraper[' . shift->{term}->GetHistory() . ']> ' }

  history_file: "~/.scraperhistory"
  history_max: 1000

  commands:
    dump:
      desc: "Dump result"
      proc: !!perl/code |
        { local $Data::Dumper::Indent = 1; warn Dumper result }
    d:
      alias: dump

    yaml:
      desc: "Dump result in YAML"
      proc: !!perl/code |
        { warn Dump result }
    y:
      alias: dump

    source:
      desc: "Show source"
      proc: !!perl/code |
        { $print->($tree->as_HTML(q('"&<>), q(  ), {})); }
    s:
      alias: source

    quit:
      desc: "Exit"
      method: !!perl/code |
        { $_[0]->exit_requested(1); }
    q:
      alias: quit

    code:
      desc: "Dump your processing in perl code"
      proc: !!perl/code |
        {
            if (@_ == 1 && $_[0] eq 'all') {
                print generate_code($source, @stack);
            }
            else {
                print generate_code($source, $stack[-1]);
            }
        }
    c:
      alias: code

    # fallbacks
    "":
      method: !!perl/code |
        {
            my ($term, $param) = @_;
            my $res = eval $param->{rawline};
            warn $@ if $@;
            push @stack, $param->{rawline} unless $@;
        }

    help:
      desc: "Show this help message"
      method: !!perl/code |
        { shift->help_call(undef, @_); }
    h:
      alias: help
    "?":
      alias: help
END_SHELL_SETTING

    my $term = Term::ShellUI->new(%$term_setting);
    print 'Using ', $term->{term}->ReadLine, "\n";
    $term->run();
}

# ...... snip snip snip ......

YAML 形式で設定する必要はないんですが,短く見やすくなるのでそうしました :)

実行すると,

% scraper http://b.hatena.ne.jp/
Using Term::ReadLine::Perl

scraper[0]> ?
                code -- Dump your processing in perl code
                dump -- Dump result
                help -- Show this help message
                quit -- Exit
              source -- Show source
                yaml -- Dump result in YAML

scraper[1]> 

履歴数も表示することができます。