読者です 読者をやめる 読者になる 読者になる

bonar note

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

File::Visitor - ファイルを指定条件で集める

普段の開発やちょっとしたコードを書くときに、特定のディレクトリ以下で指定した条件にマッチしたファイルだけを集めて何か処理をしたいということがよくあります。例えば、

  • 最終更新日が一週間以上前のログファイルを圧縮
  • .conf で終わるファイル名のファイルを読み込む
  • YYYY-MM-DD.txt という形式のファイル名で、且つその日付が1ヶ月以上前のファイルを削除

みたいなものです。

ruby であれば Dir.glob で持ってきてから filter 処理するかもしれません。find コマンドで絞り込んで、その出力をスクリプト言語で拾って、そこからさらに絞り込んだりするかもしれません。ほとんどの場合それで問題ないのですが、「ファイルを持ってきて、特定の条件で絞り込んで、何か処理する」というコードを何度も書いている気がしていたたまれなくなってきたので、この作業を行うための gem を作成しました。

file-visitor
https://github.com/bonar/file-visitor
http://rubygems.org/gems/file-visitor

Install

gem install file-visitor

File::Visitor インスタンスを作成して、そこに必要なフィルタを設定し、visit メソッドにブロックを与えて処理を行います。

require 'file/visitor'

visitor = File::Visitor.new

# files with extension .log
visitor.add_filter(:ext, :log)

# and last modified is more than 30 days ago
visitor.add_filter(:mtime, :passed, 30, :days)

# remove all the matched files
visitor.visit(root_dir) do |path|
  FileUtils.rm(path)
end

これで、「.log という拡張子を持っていて、且つ最終更新日が30日以上前のファイル」を削除する事が出来ます。add_filter できるフィルタは :ext, :name(完全一致、正規表現)、:mtime が用意されていますが、以下のように自分で作成する事も出来ます。

class BigFileFilter

  def initialize(size)
    @size = size
  end

  # filter must implement match?(path)
  def match?(path)
    File.size(path) > @size
  end

end

filter = BigFileFilter(2048)
visitor.add_filter(filter)

match?(path) を持ったインスタンスであれば何でも受け取ります。このようなバッチ処理内の小さな条件は以外とテストされずに残っているものです。バッチ処理が稼働してある程度時間が経ってからバグが発覚し(ファイル名を絞り込む正規表現が間違ってたとか)、自分を呪った事が何度もあります。file-visitor を使うことで、この小さな条件に名前を付けて切り出し テスト可能にする事を後押ししようと考えました。

あと、最終更新日を基準にして何か処理をする事がよくあって、その度に「〜日前」みたいな指定が結構面倒だなと感じていました。それだけであれば find すればいいのですが、find コマンドのオプションが正しいかどうかはテストし辛いので、すべてスクリプト言語で書きたいと考えていました。このモジュールでは以下のように指定出来ます。

# ある日付と比べてどうか、で判別
# add_filter(:mtime, comparetor_sym, time)
visitor.add_filter(:mtime, :equals_to, Time.parse("2013-01-03 04:59"))
visitor.add_filter(:mtime, :is_less_than, Time.parse("2013-01-03 04:59"))

# 現在時刻と比べてどれくらい経っているか、で判別
# add_filter(:mtime, :passed, number, time_unit)
visitor.add_filter(:mtime, :passed, 3, :days)

atime, ctime はプラットフォームによって内容が違うのと、僕のユースケースでは必要無いので入れていません。

フィルタはブロックで指定する事もできます。

visitor.add_filter do |path|
  # return true/false
end

条件が指定出来たら、visit メソッドにブロックを渡して処理します。file_list(dir) で処理を行わずにファイルの一覧を取得することも可能です。

なんとも小粒なモジュールですが、ご意見ご感想があればフィードバックを下さい。