インストール済みのモジュールたちから、perldocを引くためのリストを作る

helm-perldoc を使ってみたりしていて気付いたのだけど、Perlでインストールされているモジュールたちからperldocでドキュメント引くためのリストを作るのってけっこう難しい。。

シェルコマンドで

@INC以下にある*.pmファイル、もしくは*.podファイルから生成。@INCそれぞれからファイルを検索する。カレントディレクト.が含まれてしまうのは困るので除外する。

$ perl -e '$_ ne "." and print "$_\n" for @INC' | xargs -I{} find {} -name '*.pm' -o -name '*.pod'

というかんじでファイルは列挙できるけど、ここからパッケージ名とか抽出していくのが厳しい。全部フルパスで得られるけど探索元がそれぞれ違ったりするから一気に削る、みたいにはできないし。
perl-completion.el では列挙したファイルそれぞれにgrepをかけてpackage ****;の宣言部分を抜き出したりしていたみたい。しかしそれだとpackage宣言されていないpodだけのものとかが抜けてしまうのですよね…。

Perlスクリプト

ExtUtils::Installedを使って列挙する方法。

  • modulesメソッドで一覧を取得できる。カレントディレクトリを除く場合はskip_cwdを指定。
#!/usr/bin/env perl
use strict;
use warnings;

use ExtUtils::Installed;

for my $module (ExtUtils::Installed->new(skip_cwd => 1)->modules) {
    print $module, "\n";
}

で、あっさりモジュール名一覧が取れる…のだけど、これは所謂ディストリビューション名だけになり、それに含まれる各関連パッケージが含まれていなかったりする("Amon2"だけ取得できて"Amon2::Web"とかは含まれない、など)。
なので、各ディストリビューションから.packlistに記述されているファイルたちも取得したい。

use ExtUtils::Installed;

my $ei = ExtUtils::Installed->new(skip_cwd => 1);

my @files = ();
for my $module ($ei->modules) {
    push @files, $ei->files($module);
}
for my $file (@files) {
    print "$file\n";
}

こんなかんじにすると、関連するファイルたちをすべて取得できる。しかしこれだと不要なものもたくさん含まれるので、拡張子で適当にフィルタリングすることにする。

for my $file (@files) {
    next unless $file =~ /\.(?:pm|pods)$/;
    print "$file\n";
}

で、結局フルパスで出てくるのはどうにかしたいので、@INCと一致する部分を削る。@INCをつなげた正規表現で…

my @target_inc    = grep { $_ ne '.' } @INC;
my $re_target_inc = '(' . join('|', map { quotemeta } @target_inc) . ')';

my $ei = ExtUtils::Installed->new(skip_cwd => 1);

my @files = ();
for my $module ($ei->modules) {
    push @files, $ei->files($module);
}
for my $file (@files) {
    next unless $file =~ /\.(?:pm|pods)$/;
    $file =~ s!${re_target_inc}/?!!;
    print "$file\n";
}

ちょっと無理矢理感はあるけど、これでだいたい削れるので、あとはseparatorを置換したり拡張子を除去したりすればOK。
というわけでperldocの対象リストの列挙はこんなかんじになるかな、と。

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;

use ExtUtils::Installed;


my @target_inc    = grep { $_ ne '.' } @INC;
my $re_target_inc = '(' . join('|', map { quotemeta } @target_inc) . ')';

my $ei = ExtUtils::Installed->new(skip_cwd => 1);

my @files = ();
for my $module ($ei->modules) {
    push @files, $ei->files($module);
}
for my $file (@files) {
    next unless $file =~ /\.(?:pm|pods)$/;
    $file =~ s!${re_target_inc}/?!!;
    $file =~ s!/!::!g;
    $file =~ s!\.[^\.]+$!!;
    print "$file\n";
}

ワンライナー

やるなら、こんなかんじかな。

perl -MExtUtils::Installed -e 'my $i = ExtUtils::Installed->new(skip_cwd => 1); print "$_\n" for sort map { s!^(@{[ join("|", map { quotemeta } grep { not /^\./ } @INC) ]})/?!!; s!/!::!g; s!\.(pm|pod)$!!; $_ } grep { /\.(pm|pod)$/ } map { $i->files($_) } $i->modules'

もっと簡単でキレイな方法ないのかなー。