bonar note

京都のエンジニア bonar の技術的なことや技術的でない日常のブログです。

[API][perl] Google Code Search を ack 風にコマンドラインから使う


Google Code Search の API がシンプルでかなり便利っぽい。

Google Code Search Data API
http://code.google.com/apis/codesearch/reference.html#Parameters

普通にサイトに行って調べてもいいのですが、基本この機能を使いたい時ってCUIな環境でコードを書いてることが多いわけで、コマンドラインで使いたい。かつ ack みたいに XTermColor なエスケープシーケンス付きだと素敵だね、ってことで書いてみました。

#!/usr/bin/perl

use strict;
use warnings;

use Getopt::Long;
use Term::ANSIColor qw(BOLD ON_YELLOW RED GREEN MAGENTA CYAN YELLOW RESET);

use URI::Escape    'uri_escape';
use HTML::Entities 'decode_entities';
use LWP::UserAgent;
use XML::LibXML;

use constant {
    URL_BASE => 'http://www.google.com/codesearch/feeds/search',
    TIMEOUT  => 3,
    DEFAULT_START_INDEX => 1,
    DEFAULT_MAX_RESULT  => 5,
    DEFAULT_MAX_MATCH   => 3,
};

sub cleanup_content {
    my ($str, $keyword) = @_;

    $str =~ s/^<pre>//g;
    $str =~ s/<?/pre>$//g;
    decode_entities($str);

    my $hilighted = BOLD . ON_YELLOW . RED . $keyword . RESET;
    $str =~ s/<b>$keyword<?/b>/$hilighted/ig;

    return $str;
}
my (%opt);
Getopt::Long::GetOptions(
    'lang=s'  => ?$opt{'lang'},
    "start=i" => ?$opt{'start-index'},
    "num=i"   => ?$opt{'max-results'},
    "match=i" => ?$opt{'max-match'},
);
$opt{'start-index'} ||= DEFAULT_START_INDEX;
$opt{'max-results'} ||= DEFAULT_MAX_RESULT;
$opt{'max-match'}   ||= DEFAULT_MAX_MATCH;

my $keyword = $ARGV[0];
if (!$keyword) {
    die "specify keyword!";
}

my ($query);
{ # create query string
    my $lang = (exists $opt{'lang'} && $opt{'lang'}
        ? ' lang:' . $opt{'lang'} : '');
    my $option = join '&', map { sprintf("%s=%s", $_, $opt{$_}) }
        qw/start-index max-results/;
    $query = sprintf("%s?q=%s&%s", URL_BASE
        , uri_escape($keyword . $lang), $option);
}

{ # get and parse search result from google.
    my $ua = new LWP::UserAgent();
    $ua->timeout(TIMEOUT);
    my $res = $ua->get($query);
   
    if (!$res->is_success()) {
        die "search failed. request uri=[$query]";
    }
    # parse response XML
    my ($xmldoc);
    eval { $xmldoc = XML::LibXML->new()->parse_string($res->content()); };
    if ($@ || !$xmldoc) {
        die "parse failed. request uri=[$query] [$@]";
    }
    my @entry_nodes = $xmldoc->findnodes('//*[local-name()=?'entry?']');
    my $match_count = (scalar @entry_nodes);
    if (0 == $match_count) {
        print "result is empty.?n";
        exit;
    }

    # print lines
    foreach my $entry_node (@entry_nodes) {
        my $title   = $entry_node->findvalue(
            '*[local-name()=?'title?']/text()');
        my $updated = $entry_node->findvalue(
            '*[local-name()=?'updated?']/text()');
        my $alt_href = $entry_node->findvalue(
            '*[local-name()=?'link?']/@href');

        print GREEN . BOLD, "$title"   . RESET
            . GREEN . ' - ' . $updated . RESET . "?n"
            . CYAN  . "$alt_href"      . RESET . "?n"
            ;
        my @match = $entry_node->findnodes('*[local-name()=?'match?']');
        my $print_count = 0;

        MATCH_LOOP:
        foreach my $match (@match) {
            last MATCH_LOOP if ++$print_count > $opt{'max-match'};

            my $content = $match->textContent() || "";
            my $line    = $match->find('@lineNumber');
            print MAGENTA . "line $line" . RESET . "?n"
                . cleanup_content($content, $keyword) . "?n?n"
                ;
        }
    }
}

__END__

=head1 NAME

  google_code_search.pl - script to search google_code_search

=head1 SYNOPSYS

  google_code_search.pl strcmp
  google_code_search.pl --lang=perl --num=3 --start=10 uri_escape

=end

#バックスラッシュが入力できない。。。どうやるんだろう。。
使い方はこんな感じで、

  google_code_search.pl strcmp
  google_code_search.pl --lang=perl --num=3 --start=10 uri_escape

オプションはこんな感じです。

--lang= 言語指定
--start= 検索結果の開始位置を指定(10なら10件目から表示)
--num= 結果件数の最大値を指定
--match= 結果内のマッチ箇所の最大値を指定

と本当にAPIそのままの素朴なツールなのですが、実行すると以下のような感じになります。

http://farm2.static.flickr.com/1326/804820054_c166fbd7b6.jpg

単にAPIをたたいて表示してるだけですが、結構便利かも。欲を言えばvimスクリプトでカーソル位置の単語で検索した結果をvnewして出してくれちゃったりするとかなり最高な予感ですが、これをvimスクリプトだけで書くのはかなり骨が折れそうですね。。特に通信部分とXMLのパースが。

このコマンドをどこかにおいておいて vnew + コマンド実行 するような map を書く方が楽ですね。ちょっとインチキっぽいですけど。

こんな素敵な google code search ですが、欲を言えば

  • 結果の前後の行数指定をしたい。現状だと前後が無さすぎて文脈がまったくわからない。
  • 無理な注文ですが、特にperlとかは、ネームスペースを理解した検索結果が欲しい。URI::Escape::uri_escape で検索しても uri_escape がマッチして欲しい。

といった辺りがなんとかなると言う事なしなんだけどなあ。