Ruby
knife

クライアントスクリプトのそれっぽい書き方について。

More than 1 year has passed since last update.

警告

この記事は個人用の覚書なんだってば!

目的

最近ちょっとしたスクリプトを書くことが多い。大規模なもんじゃなくて、特定の操作をcrontabで定期的に実行するスクリプトとか、モジュールとかちょっとしたクライアントスクリプトとか。何も考えずにべたっと一枚に書いてもいいんだけど、ちょっと規模が大きくなるとわかりやすく書きたくなってくる。

なので既存のよく使われているスクリプトはどのような風に実行しているか調査してみた。

chef-knife

https://github.com/chef/chef/blob/master/bin/knife

require "chef/application/knife"
Chef::Application::Knife.new.run

ここが実際たたかれるポイント

https://github.com/chef/chef/blob/master/lib/chef/application/knife.rb#L152

knife.rb
 def run
    Mixlib::Log::Formatter.show_time = false
    validate_and_parse_options
    quiet_traps
    Chef::Knife.run(ARGV, options)
    exit 0

ここではconfファイルやら引数で渡されたoptionをパースして実際のknifeの処理をしている部分に渡している

https://github.com/chef/chef/blob/master/lib/chef/knife.rb#L203

self.run
def self.run(args, options = {})
      subcommand_class = subcommand_class_from(args)
      subcommand_class.options = options.merge!(subcommand_class.options)
      subcommand_class.load_deps
      instance = subcommand_class.new(args)
      instance.configure_chef
      instance.run_with_pretty_exceptions
    end

argsはコマンドライン引数から渡されているのでknife node listみたいな引数が渡され、該当するコマンドのクラスのインスタンスが作成され、run_with_retty_exceptionsが呼ばれているような感じがする。

サブコマンドはすべてChef::Knifeクラスを継承しているよう(https://github.com/chef/chef/blob/master/lib/chef/knife/client_list.rb)

run_with_pretty_exceptions でさらに run が呼ばれており
このrun関数の中身は各サブコマンドで定義する感じ

run_with_pretty_exceptions
   def run_with_pretty_exceptions(raise_exception = false)
      Chef::LocalMode.with_server_connectivity do
        run
      end
  end

実際のsub_commandは

client_list.rb
      def run
        output(format_list_for_display(Chef::ApiClientV1.list))
      end

こんな感じで各コマンド毎の処理がわかりやすく記述されている。

command パターン

前置きが長くて何を書きたかったか忘れそうなんだけど、knifeはcommandベースのスクリプトなんで実行をrunに集約して各サブコマンドごとの処理をサブコマンド用のクラスのrun関数に集約できるcommnadパターンぽい書き方はわかりやすくて良いかもしれない。

最小のサンプルプログラム

何らかしらのクライアントプログラムを書くときに参考にできそうな気がするのでコマンドベースのクライアントコードを書くときは多分下のようなファイル構造になるのかな?

例えば、special <subcommand>みたいなプログラムを書きたいとき

special
├── lib
│   ├── application.rb
│   ├── command
│   │   └── ls.rb
│   └── command.rb
└── special.rb

で中身

special.rb
#!/usr/bin/ruby

require_relative 'lib/application'

Application.new.run
lib/application.rb
require_relative 'command'
class Application
  def initialize
  end

  def run
    #ここらへんで渡すoptsとかを取得したりargvのvalidateとかするといいかも
    Command.run(ARGV[0])
  end
end
lib/command.rb
require_relative 'command/ls'

class Command
  def self.run(sub_command, opts ={})
    instance = nil
    if sub_command == 'ls'
      instance = Ls.new
    else
      puts sub_command + ' not found'
      exit 1
    end
    instance.run
  end

  def initialize()
  end
end
lib/command/ls.rb
require_relative '../command'
class Command
  class Ls < Command
    def run()
      puts `ls`
    end
  end
end