💄

RubyでオプションをJSONファイルとコマンドライン引数の双方から取得する

2020/11/23に公開

Rubyでプログラムを作成している際に、プログラムのオプションを以下の2つのソースから取得したい場合があります。

  1. JSONファイル
  2. コマンドラインオプション

こういう場合に、よく使う実装の方法を共有します。

基本的なコンセプト

  1. オプションは一つのオブジェクトに集める。
    例: class Options
  2. オブジェクト生成時に、JSONファイルからオプションを読み込む。
    例: opt = Options.new('some.json') とするとJSONファイルからオプションを読み込む。
  3. オプションにアクセスするときはオブジェクトの accessor を使う。
    例: opt.option_1 のような感じでアクセスできる。
  4. コマンドライン引数は OptionParser を使ってハッシュに保存しする。
  5. オプションのオブジェクトは、ハッシュから値を取り込むメソッドを持つ。
    これによりコマンドライン引数から生成した設定値のハッシュを取り込む。
    例: opt.import_hash(options) のような感じで使える。

実装例

https://gist.github.com/aikige/3e393a8b04c24b900f4810c5cc55ffa0

#!/usr/bin/env ruby

require "json"

class Options
  # list of supported keys.
  TEMPLATE_SAMPLE = { 'option_1' => 'Bool', 'option_2' => 'String' }

  def initialize(filename = "config.json", template = TEMPLATE_SAMPLE)
    @template = template
    File.exist?(filename) and File.open(filename) do |j|
      import_hash(JSON.load(j))
    end
  end

  def import_hash(hash)
    return unless hash.is_a?(Hash)
    @template.keys.each do |key|
      add_variable(key, hash[key]) if hash.has_key?(key)
    end
  end

  private def add_variable(name, value)
    if (@template[name].downcase == 'bool')
      # 'bool' type needs special handling in ruby.
      raise "value is not bool" if (!!value != value)
    else
      raise "value is not #{@template[name]}" unless (value.is_a?(eval(@template[name])))
    end
    # crate instance variable.
    instance_variable_set("@#{name}", value)
    # add getter method.
    self.class.send(:attr_reader, name) unless respond_to?(name)
  end
end

if $0 == __FILE__
  require 'optparse'

  opt = Options.new('config.json')

  option = {}
  arg = OptionParser.new
  arg.on('-1 OPTION', '--option_1=OPTION', 'Set option 1') { |v| option['option_1'] = v.eql?("true") }
  arg.on('-2 OPTION', '--option_2=OPTION', 'Set option 2') { |v| option['option_2'] = v }
  arg.parse!(ARGV)
  opt.import_hash(option)
  p opt.option_1
  p opt.option_2
end

JSONファイルはこんな感じです。

{
        "option_1": true,
        "option_2": "test"
}

補足

  • 定数 TEMPLATE_SAMPLE が、オプションのテンプレートになっていて、名前と想定する型の組を定義しています。
  • モジュールとして再利用できるようにテンプレートは初期化の引数として指定することもできるようにしてあります。
  • Ruby では Bool 型はありませんが、ないと不便なので Bool キーワードが型として含まれる場合の処理は個別に書いています。

おまけ

Python の場合:
https://zenn.dev/aikige/articles/handle-options-in-python

Discussion