RakeFileUtils の :noop, :verbose オプション

!注意!現在のRake(0.8.1.1)には、この記事のとおりにやっても一部うまく行かない問題があります。修正は後ほど公開します。id:ke-k:20080212:rakefileutils_fix で公開しています。

前にも書きましたが、Rakeではモジュール FileUtils(正確にはRakeFileUtils) がincludeされています。 FileUtils には、cp, mkdir, mv 等々のメソッドが定義されており、

file 'some-directory' do |t|
  mkdir t.name
end

のようにシェルのコマンドのように各メソッドを使うことができます。FileUtils について詳しくは以下のページを参照してください。

また、Rakeではメソッド sh, ruby も定義されており、シェルコマンドの実行やrubyコマンドの呼び出しも簡単にできるようになっています。

task :hoge do |t|
  sh 'echo hoge'
  ruby '-e', 'puts "hoge"'
end

これらのメソッドには、オプションを指定することができます。

mkdir t.name, :verbose => true, :noop => true

ほとんどのメソッドは、:noop, :verbose を指定できるようになっています。

  • :noop
    • コマンドを実際には実行しない
  • :verbose

cd 等の実行されないとその後のコマンドに影響を与えるコマンドには :noop オプションは無いようです。オプションについて詳しくはマニュアルに書いてあります。あと、コマンドごとに受け付けるオプションの一覧は、 FileUtils::OPT_TABLE にハッシュで入っているようです。

ただ、 :noop, :verbose などを実行するのは通常コマンドの実行内容を確認したいときであり、全体もしくは部分で指定できるとうれしいと思います。これを実現するのが、モジュール RakeFileUtils です。 FileUtils をincludeしており、以下の部分でラッパを生成しています。

module RakeFileUtils
  # ...
  FileUtils::OPT_TABLE.each do |name, opts|
    default_options = []
    if opts.include?('verbose')
      default_options << ':verbose => RakeFileUtils.verbose_flag'
    end
    if opts.include?('noop')
      default_options << ':noop => RakeFileUtils.nowrite_flag'
    end

    next if default_options.empty?
    module_eval(<<-EOS, __FILE__, __LINE__ + 1)
    def #{name}( *args, &block )
      super(
        *rake_merge_option(args,
          #{default_options.join(', ')}
          ), &block)
    end
    EOS
  end

前述のとおり、 FileUtils::OPT_TABLE には、各メソッドとそのメソッドが受け付けるオプションが入っています。これを見て、 :noop と :verbose を受け付けるメソッドには、デフォルト値を簡単に変更できるように、module_eval で同名のラッパメソッドを生成しているのが上記の部分です。デフォルト値は、 verbose, nowrite の各メソッドで設定します。

noop = nowrite # 現在の :noop デフォルト値を取得
v = verbose    # 現在の :verbose デフォルト値を取得
noop = nowrite(true)     # :noop デフォルト値を設定、noop には設定前のデフォルト値が入る
v = verbose(true)        # :verbose デフォルト値を設定、v には設定前のデフォルト値が入る
sh "gcc hoge.c -o hoge"  # sh "gcc hoge.c -o hoge", :noop => true, :verbose => true と同じ。コマンドが出力され、実際には実行されない
verbose(v)               # 元に戻す
nowrite(noop)            # 元に戻す

また、範囲で実行する場合には、ブロックで指定することもできます。

nowrite(true) do
  verbose(true) do
    sh "gcc hoge.c -o hoge"
  end
end

ブロックを抜けると、もとの値に戻ります。例外が起きた場合もちゃんと戻してくれます。

rake に --dry-run を付けて実行したときには、

module Rake
  # ...
  class Application
    # ...
    # Do the option defined by +opt+ and +value+.
    def do_option(opt, value)
      case opt
      # ...
      when '--dry-run'
        verbose(true)
        nowrite(true)
        options.dryrun = true
        options.trace = true

verbose(true), nowrite(true) が実行されています。 ただ、 options.dryrun = true もセットされているので、

module Rake
  # ...
  class Task
    # ...
    # Execute the actions associated with this task.
    def execute(args)
      if application.options.dryrun
        puts "** Execute (dry run) #{name}"
        return
      end
      ...

タスクの実行の部分も "** Execute (dry run) #{name}" と表示されるだけで、タスクのブロックそのものが実行はされないのですが。実行コマンドの確認もしたいので、verbose(true), nowrite(true) はするけど、options.dryrun = true はしないオプションが欲しいです。現在は、確認するときだけRakefileの先頭で nowrite(true) を入れるのが現実的な解でしょうか。
--verbose オプションでは verbose(true) が、 --quiet オプションでは verbose(false) が実行されてます。

さて、ここまで書いてきておいてなんですが、現在のバージョンには nowrite, verbose によって設定されるデフォルト値が sh, ruby 以外のメソッドに適用されない問題があります。対策について後ほど書きます。id:ke-k:20080212:rakefileutils_fix で書いています。

directory

directory メソッドでディレクトリ作成タスクを生成できます。

directory 'a/b/c'

ディレクトリがネストしている場合は、それぞれのディレクトリについてタスクを生成してくれます。

file 'a' do |task|
  mkdir task.name
end
file 'a/b' do |task|
  mkdir task.name
end
file 'a/b/c' do |task|
  mkdir task.name
end

ということですね。素晴らしいです。directory に限っては、依存関係やブロックを渡せないので、これらは file メソッドで別に定義する必要があるようです。

directory 'a/b/c'
file 'a/b' => ['hogehoge.yaml'] do |task|
  ...
end

pathmap

Rake の rule では、

rule '.o' => ['%X.c'] do |t|
  ...
end

のように、依存先のルールに'%' を入れると、pathmapを使って変換されるようです。pathmapについての情報があまりなかったのですが、新しい機能なのか、使う人がいないのか。pathmapについてまとめてみます。と言っても、リファレンスマニュアルほとんどそのままですが。

Rakeでは、 String#pathmap が定義されています。pathmapはprintfのように書式指定子を使うことでパスの変形を行えます。

  • %p -- 完全なパス
  • %f -- パスからディレクトリを取り除いたファイル名
  • %n -- 拡張子を取り除いたファイル名
  • %d -- パスのディレクトリ部
  • %x -- 拡張子(ない場合は空文字列)
  • %X -- 拡張子を取り除いたパス
  • %s -- File::ALT_SEPARATOR(あれば。なければFile::SEPARATOR)
  • %% -- '%'そのもの
'a/b/c/d/file.txt'.pathmap('%p') # => a/b/c/d/file.txt
'a/b/c/d/file.txt'.pathmap('%f') # => file.txt
'a/b/c/d/file.txt'.pathmap('%n') # => file
'a/b/c/d/file.txt'.pathmap('%d') # => a/b/c/d
'a/b/c/d/file.txt'.pathmap('%x') # => .txt
'a/b/c/d/file.txt'.pathmap('%X') # => a/b/c/d/file

%s, %% 以外の6つ(p, f, n, d, x, X)は正規表現による置換を行えます。

  • %{old,new}d のように記述
    • %d の old (正規表現)にマッチする部分が new (文字列)に置換される
  • %{old,*}{...}
    • new の部分を'*'一文字にすると、ブロックを渡せる
  • %{old,new;src,bin}d
    • セミコロンで区切って複数のパターンを記述できる

たとえば、

"src/org/onestepback/proj/A.java".pathmap("%{^src,bin}X.class") # => bin/org/onestepback/proj/A.class

では、 ^src が正規表現コンパイルされて、%X 中でマッチする部分がbinに置換されます。

"src/org/onestepback/proj/A".sub(/^src/, "bin")

のように動くようです。gsubではなくsubです。

ブロックを渡す場合は、

"/path/to/file.TXT".pathmap("%X%{.*,*}x") { |ext| ext.downcase } # => /path/to/file.txt
  • %d は %2d のように数値を指定すると、階層数を指定できる。
'a/b/c/d/file.txt'.pathmap("%2d")  # => 'a/b'
'a/b/c/d/file.txt'.pathmap("%-2d") # => 'c/d'

独自フォーマットでなかなか覚えるのが大変そうなので、正規表現で済ましてしまいそうな予感です。

Rakeメモ - rule

ruleは、Makeの型ルールをもっと高度にしたものですね。

rule '.o' => ['.c'] do |task|
  sh "gcc #{task.source} #{task.name}"
end
  • 依存先が'.'で始まる場合は、いわゆるサフィックスルール(古いタイプの)
    • task.nameがターゲット名(file, taskでも使える)
    • task.sourceが依存先ファイル(の1つめ)(file, taskでは使えない)
    • task.sourcesが全依存先ファイルの配列(Array)(file, taskでは使えない)
    • task.prerequisitesが全依存先ファイルのFileList(?)(file, taskでも使える)
      • FileListはto_sがjoin(' ')してくれる
      • sourcesとprerequisitesの違いは?クラスだけ??
  • '.c' は proc{|task_name| task_name.ext('.c')} と等価

sourcesとprerequisitesの違いがわかりません…全部prerequisitesでいいんじゃないかと思うのですが。

rule '.o' => [proc{|task_name| task_name.sub(/\.o$/, '.c')}] do |task| ...
  • Procで依存先を解決
  • ターゲットのファイル名(String)が引数に渡されるので、依存先のファイル名を返す
  • 依存先の配列は最終的にflattenされるので、ネストしてもOK
    • つまり、複数ファイルを返せる

これを使えば、どんな複雑なルールでもかけそうです。

rule '.o' => ['../filename.ext'] do |task| ...
  • スラッシュを含む場合はファイル名の変換しない
    • この場合は'../filename.ext'そのまま

ディレクトリをまたぐ場合は、ファイル名固定のようです。正直何のためにあるのかわかりません。

rule '.o' => ['%X.c'] do |task| ...
  • '%'を含む場合はpathmapを使って変換
  • '%X.c' は proc{|task_name| task_name.pathmap('%X.c')} と等価
    • pathmapについては後述
rule '.o' => ['HOGEHOGE'] do |task| ...
  • '%''/'を含まず、'.'で始まらない場合は、ファイル名そのまま(この場合'HOGEHOGE')
rule '.o' => ['.c', proc{|task_name| (1..5).map{|n| "#{task_name.sub(/\.o$/, '')}.#{n}"}}, 'HOGEHOGE'] do |task| ...
  • もちろん組み合わせもOK
rule(/\.o$/ => ['.c']) do |task| ...
  • ターゲットに正規表現OK
    • Rubyの文法上の制約から、メソッド呼び出しの括弧を省略できない
    • rule '.o' ... は rule(/\.o$/ ...) と等価

Rakeメモ - task

Makeで言う.PHONYターゲットはtaskメソッドで定義します。

  • 依存先のタイムスタンプに関係なく必ず実行する。

あとはfileと同じのようです。実装的にはtaskがベースのようですが。タスク名はStringもしくはSymbolどちらでも良いようですが、公式マニュアルのサンプルではSymbolで書いてありますね。fileはString、taskはSymbolで書くとわかりやすい、ということでしょうか。

  • デフォルトターゲットは:defaultタスクで指定する。
    • Makeとちがってデフォルトターゲットは明示的に定義しないとダメ。
  • 生成ファイル全消去は:clobber推奨
    • :cleanは中間ファイルのみ削除
    • require 'rake/clean' すると簡単に定義できる(らしい)

Rakeメモ - file

Makeでいうルールの定義がfileですね。

file 'hoge' => ['hoge.c', 'hoge.h'] do |task|
  sh "gcc hoge.c -o hoge"
end
  • ファイル 'hoge' が 'hoge.c', 'hoge.h' に依存していることを表す。
  • 'hoge'を生成するのに、ブロックの中のsh "..."を実行する。
    • 引数taskには、Taskクラスのインスタンスが渡される。(Taskクラスは後述)
    • shメソッドによってシェルを呼び出す。
      • FileUtilsを継承したRakeFileUtilsモジュールに定義されており、includeされている。
      • FileUtilsのメソッドと同じく{:verbose => true, :noop => true}でオプションを渡せる。
      • verboseオプションに合わせて、標準エラー出力に実行コマンドを出すなど、execやsystemよりRake向けになっている。
      • ソースを見たところMakeのSHELL変数のようにシェルを変更することはできないようだ。
    • mkdir, rm等はFileUtilsに定義されており、includeされているので、通常のシェルに近い感覚で使える。
    • ブロックは省略可能。
  • ターゲットのファイルが、依存先のファイルよりも新しければ、実行しない。

fileメソッドに{'hoge' => ['hoge.c', 'hoge.h']}なHashとブロックを渡してるだけで単なるメソッド呼び出しなのですが、Makefileと同じような構文になっていて読みやすいです。

file 'hoge' => ['hoge.c']
file 'hoge' => ['hoge.h']
  • 複数回実行した場合は、上書きではなく依存先リストに追加される。

Rake - Ruby Make

Makeはコンパイル用のツールと思われがちですが、コンパイル後のコマンドの実行まで自動で行ってくれる非常に便利なツールです。パラメータを変えて、大量にコマンドを実行、ファイル生成をするときなどは自前のスクリプトを使うよりも簡単かつ柔軟に書けて重宝します。しかし、使っていると型ルールが意外と使いにくいことから、Makeより柔軟性が高く、簡単に記述できるMakeツールを探していました。

Ruby版Make の Rake がRuby on Railsなどでよく使われているようで、かなり便利そうな感じです。

  • Makeに対するMakefileと同様に、"Rakefile"を書いておけばよい
    • rakeコマンドで1発実行
    • rake hoge みたいなのもOK
    • rake -n でdry run
  • RakefileRubyスクリプト
    • task メソッドや rule メソッドで依存関係を書いていく
  • 依存関係に正規表現OK
  • 依存関係の解決にprocを渡せる

うーん、便利そうです。

ここからダウンロードしてインストール。rubygemsでもOKらしいです。開発は続いているみたいですね。バージョンは0.8.1でした。

この辺を読めばわかるかな。