Rakeを徹底解剖 - その4 "タスクの実行"
過去3回のソースリーディングに続き、今回はついにタスクの実行について調べていきます。
def run standard_exception_handling do init load_rakefile top_level ## 今回のキモになる部分 end end
なお、まだその1, その2、その3を読んでない方はこちらから。
タスクの実行
それでは、まず top_level
の中を見てみましょう。
## rake/application.rb def top_level run_with_threads do if options.show_tasks display_tasks_and_comments elsif options.show_prereqs display_prerequisites else top_level_tasks.each { |task_name| invoke_task(task_name) } end end end
最初の if
と elsif
に関しては、オプションによって それぞれ -T
、-p
を指定したときのもので、どちらも実行をせずにタスクの詳細を表示するオプションになります。
今回は実行を見ていくので、else
に指定されたケースを見ていくことになります。
なので、invoke_task
についてみていきましょう。
## rake/application.rb def invoke_task(task_string) # :nodoc: name, args = parse_task_string(task_string) t = self[name] t.invoke(*args) end def parse_task_string(string) # :nodoc: /^([^\[]+)(?:\[(.*)\])$/ =~ string.to_s name = $1 remaining_args = $2 return string, [] unless name return name, [] if remaining_args.empty? args = [] begin /((?:[^\\,]|\\.)*?)\s*(?:,\s*(.*))?$/ =~ remaining_args remaining_args = $2 args << $1.gsub(/\\(.)/, '\1') end while remaining_args return name, args end
rake
は、 task[arg1,arg2]
というようにタスク名のあとに、引数を設定することができます。
parse_task_string
は、まさに task[arg1,arg2]
という文字列からタスク名と引数にパースしている箇所になります。
余談ですが、筆者はあまりこのタスクに引数を与える使い方をしてません。
どうようのことは、ENV
を介してやるようにしています。
なんとも、扱いづらいんですよね。 文字列上に引数が設定されてるんで、shell でちょうどその task の引数の位置に移動できなかったりするんで。
一方で、 -T
実行時に引数が必要なことが明示できないというデメリットがあるのですが、 desc
にその旨を書くということで対処してます。
さて、ソースリーディングに戻ります。
パースされた、文字列は args
として展開され、Task#invoke
へ渡されます。
## rake/task.rb # Invoke the task if it is needed. Prerequisites are invoked first. def invoke(*args) task_args = TaskArguments.new(arg_names, args) invoke_with_call_chain(task_args, InvocationChain::EMPTY) end
引数は、TaskArguments.new(arg_names, args)
によって、 Hash
のような構造体へと置き換えられます。
そして、 タスクの実行部分invoke_with_call_chain
へと引数として渡されていきます。
## rake/task.rb def invoke_with_call_chain(task_args, invocation_chain) # :nodoc: new_chain = InvocationChain.append(self, invocation_chain) @lock.synchronize do if application.options.trace application.trace "** Invoke #{name} #{format_trace_flags}" end return if @already_invoked @already_invoked = true invoke_prerequisites(task_args, new_chain) execute(task_args) if needed? end rescue Exception => ex add_chain_to(ex, new_chain) raise ex end
InvocationChain.append(self, invocation_chain)
については、 実行するタスクを保持しているのだが、読み解いていった結果、append
するときに、タスクの循環依存が起きてないか確認してる以外実行自体にはどうやら関係なさそう?
次の@lock.synchronize
については、おそらく並列処理によるスレッド実行のためのものと思われる。
今回は、タスクの単純な実行だけに焦点をあてるので割愛。
application.options.trace
や、 二重実行を防ぐ、@already_invoked
のチェックを終えると、invoke_prerequisites
が実行される。
これは単純に、dependecies
として登録されたタスクを実行するものである。
## rake/task.rb def invoke_prerequisites(task_args, invocation_chain) # :nodoc: if application.options.always_multitask invoke_prerequisites_concurrently(task_args, invocation_chain) else prerequisite_tasks.each { |p| prereq_args = task_args.new_scope(p.arg_names) p.invoke_with_call_chain(prereq_args, invocation_chain) } end end
そして、最後に呼ばれる execute
。
## rake/task.rb def execute(args=nil) args ||= EMPTY_TASK_ARGS if application.options.dryrun application.trace "** Execute (dry run) #{name}" return end application.trace "** Execute #{name}" if application.options.trace application.enhance_with_matching_rule(name) if @actions.empty? @actions.each do |act| case act.arity when 1 act.call(self) else act.call(self, args) end end end
application.enhance_with_matching_rule(name)
については、 rule
という記述で定義されたタスクで活躍する。
今回の単純なタスクのケースにはあまり関係ないので、割愛。
すると、残すは、@actions
の部分だが、中身はタスクを定義したときに渡しているブロック文である。
act.call(self, args)
を呼ぶことで、ブロックにタスクと引数(TaskArguments
)が渡ってくる。
少し気になったところとして、なんで actions
なんだというところだが、どうやら下記のような動作をするらしい。
task :hoge do puts "hoge1" end task :hoge do puts "hoge2" end
$ bundle exec rake hoge hoge1 hoge2
てっきり、Rubyのメソッド定義のようにあとで書いたものでオーバライドされるのかと思ってたら、どうやら登録された順に追加実行されるようだ。 動作の補足しづらくなるし、正直あまりお勧めしないし。というかこの書き方が起きないように防ぐべきなきがする。
まとめ
今回もあわせて、全4回にわけて rake
のソースリーディングを行ってきたのだが、やってみた感想としては、一見単純そうなものでも沢山のクラスで構成されていた。
特に文字列解析や、オプションによる分岐が必要になる箇所は、厚めにクラス分けが行われており、読んでいて感心した。
特に、CLIのツールを0から読み解くのは初めてだったこともあり、コマンドパーサの解析や、オプションによる処理分岐、ファイル読み込み、実行処理といった流れや分け方はかなり勉強になった。
ソースリーディングとしては、以上だが時間に余裕があったら、ここで得た知識をもとにrakeもどきでも作ってみようと思う。