はじめに
長らくCapistranoを使ってます。もう随分慣れました。
そして最近バージョンが3.5にあがり、ログフォーマットが激変しました。ちょっと3.5の勉強がてら、ソースを読んでみます。
インストール
3.4.1からアップグレードしました。
take@MacBook-Air ~> sudo gem install capistrano Fetching: sshkit-1.11.1.gem (100%) Successfully installed sshkit-1.11.1 Fetching: airbrussh-1.0.2.gem (100%) Successfully installed airbrussh-1.0.2 Fetching: capistrano-harrow-0.5.2.gem (100%) ___ _ ___ ___ ___ _____ ___ _ _ _ ___ / __| /_\ | _ \_ _/ __|_ _| _ \ /_\ | \| |/ _ \ | (__ / _ \| _/| |\__ \ | | | / / _ \| .` | (_) | \___/_/ \_\_| |___|___/ |_| |_|_\/_/ \_\_|\_|\___/ Learn about our web-based collaboration and automation platform for Capistrano: hrw.io/auto-cap Successfully installed capistrano-harrow-0.5.2 Fetching: capistrano-3.5.0.gem (100%) Successfully installed capistrano-3.5.0 Parsing documentation for sshkit-1.11.1 Installing ri documentation for sshkit-1.11.1 Parsing documentation for airbrussh-1.0.2 Installing ri documentation for airbrussh-1.0.2 Parsing documentation for capistrano-harrow-0.5.2 Installing ri documentation for capistrano-harrow-0.5.2 Parsing documentation for capistrano-3.5.0 Installing ri documentation for capistrano-3.5.0 Done installing documentation for sshkit, airbrussh, capistrano-harrow, capistrano after 2 seconds 4 gems installed take@MacBook-Air ~> cap --version Capistrano Version: 3.5.0 (Rake Version: 10.4.2)
sshkitの内部実装も読まないとな。。。
ソースコードの読み方
v3.5を見ていきましょう。
まぁgrepしたりするかもしれないので自分のところに落としておいたほうがいいですね。ディレクトリ構造はこんな感じ。
take@MacBook-Air ~/capistrano-3.5.0> tree -L 2 . ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DEVELOPMENT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── RELEASING.md ├── Rakefile ├── bin │ ├── cap │ └── capify ├── capistrano.gemspec ├── features │ ├── configuration.feature │ ├── deploy.feature │ ├── deploy_failure.feature │ ├── doctor.feature │ ├── installation.feature │ ├── remote_file_task.feature │ ├── sshconnect.feature │ ├── step_definitions │ └── support ├── issue_template.md ├── lib │ ├── Capfile │ ├── capistrano │ └── capistrano.rb └── spec ├── integration ├── integration_spec_helper.rb ├── lib ├── spec_helper.rb └── support
featureとspecはテストなのでとりあえず除外。binはその名の通り実行ファイルっぽいので、読むのはlib配下ですね。
lib以下を見てみると
take@MacBook-Air ~/c/lib> tree
.
├── Capfile
├── capistrano
│ ├── all.rb
│ ├── application.rb
│ ├── configuration
│ │ ├── empty_filter.rb
│ │ ├── filter.rb
│ │ ├── host_filter.rb
│ │ ├── null_filter.rb
│ │ ├── plugin_installer.rb
│ │ ├── question.rb
│ │ ├── role_filter.rb
│ │ ├── server.rb
│ │ ├── servers.rb
│ │ └── variables.rb
│ ├── configuration.rb
│ ├── console.rb
│ ├── defaults.rb
│ ├── deploy.rb
│ ├── doctor
│ │ ├── environment_doctor.rb
│ │ ├── gems_doctor.rb
│ │ ├── output_helpers.rb
│ │ └── variables_doctor.rb
│ ├── doctor.rb
│ ├── dotfile.rb
│ ├── dsl
│ │ ├── env.rb
│ │ ├── paths.rb
│ │ ├── stages.rb
│ │ └── task_enhancements.rb
│ ├── dsl.rb
│ ├── framework.rb
│ ├── git.rb
│ ├── hg.rb
│ ├── i18n.rb
│ ├── immutable_task.rb
│ ├── install.rb
│ ├── plugin.rb
│ ├── scm.rb
│ ├── setup.rb
│ ├── svn.rb
│ ├── tasks
│ │ ├── console.rake
│ │ ├── deploy.rake
│ │ ├── doctor.rake
│ │ ├── framework.rake
│ │ ├── git.rake
│ │ ├── hg.rake
│ │ ├── install.rake
│ │ └── svn.rake
│ ├── templates
│ │ ├── Capfile
│ │ ├── deploy.rb.erb
│ │ └── stage.rb.erb
│ ├── upload_task.rb
│ ├── version.rb
│ └── version_validator.rb
└── capistrano.rb
filterという文字が見えますね。configuration以下のコードを読んでいきましょう。
filter.rb
capistrano/filter.rb at v3.5.0 · capistrano/capistrano · GitHub
まずここがfilterの親元です。他のfilterをrequireしてます。
呼び出し元がどこかまだ分かってませんが、typeとvalueが与えられて呼ばれるようです。typeはfilterのtypeだと思って間違いないでしょう。
- valueが空ならEmptyFilter
- valueがallのsymbolもしくはstringならNullFilter
- valueが:hostならHostFilter
- valueが:roleならRoleFilter
- それ以外はNullFilter
filterメソッドではここで決められた@strategyに入ってるfilterが実行されることになります。
ちなみにここのallのsymbolかstringかって話はv3.5のrelease noteに書いてますね。
capistrano/CHANGELOG.md at master · capistrano/capistrano · GitHub
Allow use "all" as string for server filtering (@theist)
元々はsymbolのみだったのがstringを許すようになりました。
さて1つずつFilterを見ていきましょう。
EmptyFilter
capistrano/empty_filter.rb at v3.5.0 · capistrano/capistrano · GitHub
module Capistrano class Configuration class EmptyFilter def filter(_servers) [] end end end end
何のために作ったんかな?これ、どんなserverをいれても空配列を返すんですね。
むしろvalueがないならraiseして怒ってほしいぐらいなんですが、ここは律儀に空配列を返してあげるのね。
NullFilter
capistrano/null_filter.rb at v3.5.0 · capistrano/capistrano · GitHub
module Capistrano class Configuration class NullFilter def filter(servers) servers end end end end
filterしないってことなのね。そのまま返すのね。まぁallのときにここに入るようになるからその通りなんだろうけど、その他もっていうところだよね。Filterが不適切でもエラーを返さないのはelseでここに入っているからだったわけか。
HostFilter
capistrano/host_filter.rb at v3.5.0 · capistrano/capistrano · GitHub
module Capistrano class Configuration class HostFilter def initialize(values) av = Array(values).dup av.map! { |v| (v.is_a?(String) && v =~ /^(?<name>[-A-Za-z0-9.]+)(,\g<name>)*$/) ? v.split(",") : v } av.flatten! @rex = regex_matcher(av) end def filter(servers) Array(servers).select { |s| @rex.match s.to_s } end private def regex_matcher(values) values.map! do |v| case v when Regexp then v else vs = v.to_s vs =~ /^[-A-Za-z0-9.]+$/ ? vs : Regexp.new(vs) end end Regexp.union values end end end end
3.4から正規表現を許すようになったのがこのhost filter。そのおかげで完全一致が部分一致になったんですけどね。。。
キモは以下の1行。
av.map! { |v| (v.is_a?(String) && v =~ /^(?<name>[-A-Za-z0-9.]+)(,\g<name>)*$/) ? v.split(",") : v }
まず、mapは要素分繰り返して、ブロック内で評価したものを返すメソッド。
is_a?でオブジェクトのクラスを判定しています。まずはStringであるかどうか。
そして =~ はStringに対するパターンマッチです。ここ、めちゃくちゃ難しいですね。
まず、=~ はマッチすればマッチした位置を整数で返します。
そしてg
これ、,までの英数字でマッチさせる、というのを再帰的に繰り返すことで、host filterの,区切り形式かどうかを見ているんですね。
HOSTS='server1,server2,server3'という形式であればok。これを,でsplitして分割して返してあげてますね。
ただこれ、avに元々正規表現を含んでいると、マッチしないのでvをそのまま返すほうのルートに入ります。
通常だとマッチする
irb(main):032:0> str = "hoge1,poge2,page5" => "hoge1,poge2,page5" irb(main):033:0> str =~ /^(?<name>[-A-Za-z0-9.]+)(,\g<name>)*$/ => 0
正規表現を少しでも使うとマッチしない
irb(main):034:0> str = "hoge*,poge*" => "hoge*,poge*" irb(main):035:0> str =~ /^(?<name>[-A-Za-z0-9.]+)(,\g<name>)*$/ => nil
つまり3.4以前の,区切り表現と、今回新たに採用した正規表現の互換を保とうとしてこういう処理になったんですね。そしてそれらは排他ですと。納得。
RoleFilter
capistrano/role_filter.rb at master · capistrano/capistrano · GitHub
module Capistrano class Configuration class RoleFilter def initialize(values) av = Array(values).dup av.map! { |v| v.is_a?(String) ? v.split(",") : v } av.flatten! @rex = regex_matcher(av) end def filter(servers) Array(servers).select { |s| s.is_a?(String) ? false : s.roles.any? { |r| @rex.match r } } end private def regex_matcher(values) values.map! do |v| case v when Regexp then v else vs = v.to_s vs =~ %r{^/(.+)/$} ? Regexp.new($1) : /^#{vs}$/ end end Regexp.union values end end end end
フィルターとして来た値をカンマでsplitします。
regex_matcherではvsの中に正規表現が含まれていなければRegexpでnewして、そうでなければそのままRegexpを返却。つまりロールは正規表現と,区切りが同時に使えるんですね。HOST FILTERとは違う点だ。
最後にfilterメソッドですが、ServerのArrayからマッチするroleを持つものをselectします。最初にServerがStringかを確認しているのがちょっと分からないですね。おそらくいろんな情報を持つServerのオブジェクトだと思いますので通常はfalseに入らないはず。そして何かroleを持っていればそのroleでループをまわし、単純にマッチするかどうかを見ています。
おわりに
OSSのソースをはじめて読みました。分からない表現ばかりで、そのたび調べて動かすことができるので、ひとのコードを読むのは勉強になるというのは本当ですね。
次回はbin以下、capコマンドが実行されたあとどうなるかの流れを追いたいと思います!