技あり! gemspec中でファイル一覧を取得する
タグ: ruby / 初版公開: 2014-08-26

はじめに

gemspecファイルはRubyのGemファイルの生成元となるファイルである。gemspecファイルでは、Gemに含むファイルの一覧をGem::Specification#filesに設定して、Gemファイルに含むファイル一覧を定義する。

ファイル数が少ない場合は手書きしても良いのだが、ファイル数が多い場合は1つ1つ書くのが馬鹿馬鹿しいため、普通は何らかの方法でファイル一覧をプログラム的に取得して設定する。

このファイル一覧の取得方法は標準化されているとは言いがたく、有名どころのGemであっても、ファイル一覧の取得方法は様々である。このエントリではいくつか目に止まったGemについて、ファイル一覧の取得・設定方法を見てみたい。

なおメジャーなGemを調べるには、拙作のBestGems.orgが便利である。(宣伝)

rack

rackは合計ダウンロードランキング2位である。 言わずと知れたWebサーバインタフェースはどのようにファイル一覧を取得しているのか。 以下がrack.gemspecからの抜粋である。

s.files = Dir['{bin/*,contrib/*,example/*,lib/**/*,test/**/*}'] +
%w(COPYING KNOWN-ISSUES rack.gemspec Rakefile README.rdoc SPEC)

いきなりなんか難しい感じだ。Dir.[]Dir.globである。Dir.globはワイルドカード展開を行いパターンにマッチしたファイル名を文字列の配列として返すメソッドだ。

{bin/*,contrib/*,example/*,lib/**/*,test/**/*}が難しいので構成要素を分解すると、{}は組み合わせの展開、*は任意の文字列とのマッチ、***/の0回以上の繰り返しとなっている。

良く読むと、これでbin直下のファイルや、lib以下の全ファイルなどを、一覧していることがわかる。意外とアナログである。

thor

thorは合計ダウンロードランキング3位である。 サードパーティのコマンドラインオプションのパーサとしては最も人気がある。 以下がthor.gemspecからの抜粋である。

spec.files = %w[.document CHANGELOG.md LICENSE.md README.md Thorfile thor.gemspec]
spec.files += Dir.glob("bin/**/*")
spec.files += Dir.glob("lib/**/*.rb")
spec.files += Dir.glob("spec/**/*")

これは読みやすい。使っているのはDir.globでrackと変わらない。あまりトリッキーなことはせずに、可読性を重視しているように見える。Array#+で配列を連結できることをうまく活用していると言えるだろう。

active-support

active-supportは合計ダウンロードランキング4位である。 みんな大好きactive-supportは説明するまでもなく、Railsで使われている便利なクラスの詰め合わせだ。 以下がactive-support.gemspecの抜粋である。

s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'lib/**/*']

…なんだかDir.[]だとかDir.globばかりでつまらないが、こいつはDir.[]に引数を複数渡している点がこれまでに紹介したGemとは異なる。Dir.[]は複数引数が取れるのである。組み合わせ展開なんて要らないのである。

なおrails系統のGemは同じように書かれていることを確認した。

json

jsonは合計ダウンロードランキング6位である。 jsonはその名の示す通りJSONのパースと生成を行うライブラリだ。 さてjson.gemspecの抜粋は以下である。

s.files = ["./tests/test_json.rb", "./tests/test_json_addition.rb", 以下略

数えたら115ファイル分ベタ書きされていたため、都合により省略させてもらった。 jsonはプログラムによるファイル一覧の生成には頼らず、すべてのファイルを手書きするというアプローチを取っていた。 まさか手書きとは思えないのだが、こういう形式のgemspecを出力するツールがあるのだろうか?

builder

builderは合計ダウンロードランキング9位である。 XML生成を行うライブラリである。

builderにはgemspecファイルは存在せず、rakeタスクでGemファイルを生成するようになっている。 このためRakefileからGem::Specification#filesを設定する箇所を抜粋する。

PKG_FILES = FileList[
'[A-Z]*',
'doc/**/*',
'lib/**/*.rb',
'test/**/*.rb',
'rakelib/**/*'
]
s.files = PKG_FILES.to_a

FileListはrakeタスク中で使用できるファイル格納用の配列だ。FileList.[]は遅延評価であることを除いてDir.[]と基本的には同じである。配列が必要なため最後にFileList.to_aしている。

tzinfo

tzinfoは合計ダウンロードランキング13位となっている。 異なるタイムゾーン間での時刻の相互変換を行うためのライブラリである。 以下はtzinfo.gemspecからの抜粋だ。

s.files = %w(CHANGES.md LICENSE Rakefile README.md tzinfo.gemspec .yardopts) +
          Dir['lib/**/*.rb'].delete_if {|f| f.include?('.svn')} +
          Dir['test/**/*.rb'].delete_if {|f| f.include?('.svn')} +
          Dir['test/zoneinfo/**/*'].delete_if {|f| f.include?('.svn') || File.symlink?(f)}

内容自体はもはやつまらないものだが、特筆すべきはSubversionの管理ファイルを除外している点だろう。以前はSubversionで管理していたのだろうか。現在はGitHubで管理されているのでもはや不要なコードのように見える。

tilt

tiltは合計ダウンロードランキング16位だ。 解説によると複数のテンプレートエンジンの汎用インタフェースとある。 tilt.gemspecを抜粋する。

s.files = %w[
  CHANGELOG.md
  COPYING
  Gemfile
  省略
]

こいつもベタ書きだった。%記法を使ってコンマを省略している点がjsonと比べて新しいと言えば新しい。

polyglot

polyglotは合計ダウンロードランキング19位だ。 良く知らないので説明は省略。

polyglotもgemspecは無くRakefileでGemを生成する系統である。ようやくJewelerが出てきた。

Jeweler::Tasks.new do |gem|
  省略
end

20位まで見て疲れたので最後にもう1パターンだけ紹介して、このエントリを終わりにする。

jekyll

jekyllは合計ダウンロードランキング449位となっている。 GitHubでも使われていて静的サイトジェネレータとして広く知られている。 以下がjekyll.gemspecからの抜粋である。

all_files = `git ls-files -z`.split("\x0")
s.files = all_files.grep(%r{^(bin|lib)/})

もはや自力でディレクトリを辿ってファイル一覧を生成するのを諦め、gitコマンドを実行してしまっている。なおbundlerも似たようなgemspecを生成する。

git ls-filesはGitで管理しているファイル一覧を出力するコマンド。-zは区切りをヌル文字とするオプション。ここに技あり! 改行ではなくヌル文字を使うことで安全にパスを切り出せるというわけだ。

ファイル一覧から必要なファイルだけを取り出すのにEnumerable#grepを使っているのもエレガントである。Enumerable#grepはパターンと===でマッチする要素を含んだ配列を返すメソッドだ。

この例ではgit ls-filesの結果のうち、binまたはlibからはじまるパスだけを、filesに格納していることがわかる。

おわりに

似ているものは省略したがGemの合計ダウンロードランキング1〜20位とjekyllについて、gemspecでファイル一覧を取得する方法について調べた。

何かテンプレートがあってひな形は自動生成されているような気もするのだが、そちらについてはあまり明るくないので特に書かないことにする。

想像以上にファイル一覧の取得・設定方法がばらついており、色々と生々しいものを見てしまった気分だ。たまにはこういう観点でGemを読んでみるのも面白い。もうやりたくないが…。