3つのgemからRubyのCLIツールの設計について学びます。
今回コールドリーディングをするのは
- bundler
- itamae
- serverkit
です。
学習対象
bundler
bundler は Ruby のパッケージ管理ツールです
Repository
Code Reading
CLI Framework
処理の流れ
- exe/bundle
コマンドラインからの呼び出しをもとに入力を受付け、
Bundler:CLI.start
に受け渡しています。
# 略 Bundler.with_friendly_errors do require "bundler/cli" # Allow any command to use --help flag to show help for that command help_flags = %w(--help -h) help_flag_used = ARGV.any? {|a| help_flags.include? a } args = help_flag_used ? Bundler::CLI.reformatted_help_args(ARGV) : ARGV Bundler::CLI.start(args, :debug => true) end
- lib/bundler/cli.rb
thor の機能で説明文、オプションが設定されています。
後続の処理に必要となるファイル ( lib/bundler/cli/init.rb
) のみを require し、
Init.new
で実際のコマンドの処理を呼び出しています。
module Bundler class CLI < Thor include Thor::Actions # 中略 desc "init [OPTIONS]", "Generates a Gemfile into the current working directory" long_desc <<-D Init generates a default Gemfile in the current working directory. When adding a Gemfile to a gem with a gemspec, the --gemspec option will automatically add each dependency listed in the gemspec file to the newly created Gemfile. D method_option "gemspec", :type => :string, :banner => "Use the specified .gemspec to create the Gemfile" def init require "lib/bundler/cli/init" Init.new(options.dup).run end
- lib/bundler/cli/init.rb
Gemfileのテンプレートを生成するコードが記述されています
# frozen_string_literal: true module Bundler class CLI::Init attr_reader :options def initialize(options) @options = options end def run # 中略 FileUtils.cp(File.expand_path("../../templates/Gemfile", __FILE__), "Gemfile") end end end
構成
コマンドラインツールとしての基本的な構成に関わる部分のみを抜粋しています。
. ├── exe └── lib └── bundler └── cli
- exe
実際に実行されるコマンドを配置する場所。
- lib/bundler/cli
bundle コマンドで実行する各コマンドがファイル単位で保存されている。
例えば bundle init
は lib/bundler/cli/init.rb
bundle exec
は lib/bundler/cli/exec.rb
itamae
Itamae は軽量な構成管理ツールです。
同様のツールとしてはPuppet,Chef,Ansible, Serverkit などがあります。
Repository
Code Reading
CLI Framework
処理の流れ
- bin/itamae
コマンドラインからの呼び出しをもとに入力を受付け、
Itamae:CLI.start
に受け渡しています。
#!/usr/bin/env ruby require 'itamae/cli' Itamae::CLI.start
- lib/itamae/cli.rb
thor の機能で説明文、オプションが設定されています。
Runner.run(recipe_files, backend_type, options)
で実際のコマンドの処理を呼び出しています。
ツールの特性上、 itamae ssh
/ itamae local
/ itamae docker
など
複数のコマンドで似た処理やオプションを利用するため重複処理をDRYにしたメソッドが作成されています。
※ Itamae::CLI.define_exec_options
や Itamae::CLI#run
など
require 'itamae' require 'thor' module Itamae class CLI < Thor # 略 desc "ssh RECIPE [RECIPE...]", "Run Itamae via ssh" define_exec_options option :host, type: :string, aliases: ['-h'] option :user, type: :string, aliases: ['-u'] option :key, type: :string, aliases: ['-i'] option :port, type: :numeric, aliases: ['-p'] option :ssh_config, type: :string option :vagrant, type: :boolean, default: false option :ask_password, type: :boolean, default: false option :sudo, type: :boolean, default: true def ssh(*recipe_files) if recipe_files.empty? raise "Please specify recipe files." end unless options[:host] || options[:vagrant] raise "Please set '-h <hostname>' or '--vagrant'" end run(recipe_files, :ssh, options) end # 略 desc "init NAME", "Create a new project" def init(name) generator = Generators::Project.new generator.destination_root = name generator.invoke_all end # 略 private # 略 def run(recipe_files, backend_type, options) runner = Runner.run(recipe_files, backend_type, options) if options[:detailed_exitcode] && runner.diff? exit 2 end end end end
- lib/itamae/runner.rb, lib/itamae/generators.rb
runnner.rb には itamae local
, itamae ssh
, itamae docker
の実処理が実装されています。
generators.rb には itamae init
, itamae generate
, itamae destroy
の実処理が実装されています。
構成
コマンドラインツールとしての基本的な構成に関わる部分のみを抜粋しています。
├── bin ├── ci └── lib └── itamae ├── ext ├── generators │ └── templates ├── handler └── resource
- bin
実際に実行されるコマンドを配置する場所。
- lib/itamae
bundle コマンドで実行する各コマンドがファイル単位で保存されている。
例えば itamae local
, itamae ssh
, itamae docker
は lib/itamae/runner.rb
itamae init
, itamae generate
, itamae destroy
は lib/itamae/generators.rb
- lib/generators
ファイル生成関連の個別のファイルに対する処理が保存されている。
└── lib └── itamae └── generators ├── cookbook.rb ├── project.rb └── role.rb
- lib/generators/templates
生成されるファイルのテンプレートが保存されている。
└── lib └── itamae └── generators └── templates ├── cookbook │ ├── default.rb │ ├── files │ └── templates ├── project │ ├── Gemfile │ ├── cookbooks │ └── roles └── role ├── default.rb ├── files └── templates
Serverkit
Serverkit は軽量な構成管理ツールです。
同様のツールとしてはPuppet,Chef,Ansible, Itamae などがあります。
Repository
Code Reading
CLI Framework
処理の流れ
- bin/serverkit
コマンドラインからの呼び出しをもとに入力を受付け、
Serverkit::Command.new
に受け渡しています。
#!/usr/bin/env ruby $LOAD_PATH.unshift(File.expand_path("../../lib", __FILE__)) require "serverkit" Serverkit::Command.new(ARGV).call
- lib/serverkit/command.rb
call 時に第一引数によって処理を振り分けます。
# 略 module Serverkit # 略 class Command # 略 def call setup case action_name when nil raise Errors::MissingActionNameArgumentError when "apply" apply when "check" check when "inspect" _inspect when "validate" validate else raise Errors::UnknownActionNameError, action_name end rescue Errors::Base, Psych::SyntaxError, Slop::MissingArgumentError, Slop::MissingOptionError => exception abort "Error: #{exception}" end # 略 end end
各分岐ごとに呼び出される private method から更に個別の処理が呼び出されます。
# 略 def _inspect Actions::Inspect.new(action_options).call end # 略 def apply Actions::Apply.new(action_options).call end def check Actions::Check.new(action_options).call end def validate Actions::Validate.new(action_options).call end
- lib/serverkit/actions
個別の処理は actions ディレクトリ配下に保存されています。
例えば apply.rb にはレシピを適用する処理が実装されています。
構成
コマンドラインツールとしての基本的な構成に関わる部分のみを抜粋しています。
. ├── bin └── lib └── serverkit ├── serverkit └── errors
- bin
実際に実行されるコマンドを配置する場所。
- lib/serverkit/actions
各コマンドの実処理が保存される場所
- lib/serverkit/errros
各種例外が保存される場所
ひとりごと
こんな設計もあるよ、なんであの gem の設計を紹介しないの?
という意見がある場合、あなたが書いた解説記事も読んでみたいです。
楽しみにしています。