ツナワタリマイライフ

日常ネタから技術ネタ、音楽ネタまで何でも書きます。

capistranoソースコードリーディング Filter編

はじめに

長らく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を見ていきましょう。

github.com

まぁ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に対するパターンマッチです。ここ、めちゃくちゃ難しいですね。

まず、=~ はマッチすればマッチした位置を整数で返します。

=~ (String) - Rubyリファレンス

そしてgという表現は部分式呼び出し(subexpression call)と呼ぶらしい。

正規表現 (Ruby 1.9.3)

これ、,までの英数字でマッチさせる、というのを再帰的に繰り返すことで、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コマンドが実行されたあとどうなるかの流れを追いたいと思います!