OptionParser

Ruby標準ライブラリの一部で、基本的なコマンドライン引数解析タスクのためのシンプルな選択肢です。

rubyclicommand-linestdlib

フレームワーク

OptionParser

概要

OptionParserは、Ruby標準ライブラリの一部で、基本的なコマンドライン引数解析タスクのためのシンプルな選択肢です。標準ライブラリとして、追加の依存関係なしで使用できるため、シンプルなスクリプトで引き続き使用されています。学習コストが低く、Rubyの基本的なCLIツール開発に適しています。

詳細

OptionParserは、Rubyに標準で含まれているコマンドライン引数解析ライブラリです。外部gemを追加することなく使用でき、基本的なオプション解析機能を提供します。シンプルなAPIと軽量な実装により、小規模なCLIツールの開発に適しています。

主な特徴

  • 標準ライブラリ: 追加のgemインストール不要
  • 軽量: 最小限のオーバーヘッド
  • 基本機能: 必要十分なオプション解析機能
  • 自動ヘルプ生成: ヘルプメッセージの自動生成
  • 型変換: 文字列、数値、配列への自動変換
  • バリデーション: 基本的な値の検証
  • 短縮形・長形式: -v と --verbose の両方をサポート

メリット・デメリット

メリット

  • 依存関係なし: 標準ライブラリなので追加インストール不要
  • 学習コストが低い: シンプルで理解しやすいAPI
  • 軽量: メモリ使用量とパフォーマンスのオーバーヘッドが少ない
  • 安定性: Ruby標準ライブラリとして長期サポート
  • 十分な機能: 基本的なCLIツールには必要十分

デメリット

  • 機能制限: 高度な機能は他のライブラリに劣る
  • サブコマンド非対応: 複雑なCLI構造には不向き
  • 拡張性: カスタマイズ性に限界がある
  • モダンでない: 最新のCLI設計パターンには対応していない

主要リンク

書き方の例

基本的な使用例

#!/usr/bin/env ruby

require 'optparse'

# オプションを格納するハッシュ
options = {}

# OptionParserインスタンスを作成
opt_parser = OptionParser.new do |opts|
  opts.banner = "使用法: #{$0} [オプション] ファイル"

  # 文字列オプション
  opts.on('-n', '--name NAME', '名前を指定') do |name|
    options[:name] = name
  end

  # 数値オプション
  opts.on('-c', '--count COUNT', Integer, '繰り返し回数') do |count|
    options[:count] = count
  end

  # フラグオプション
  opts.on('-v', '--verbose', '詳細出力') do
    options[:verbose] = true
  end

  # ヘルプオプション
  opts.on('-h', '--help', 'ヘルプを表示') do
    puts opts
    exit
  end
end

# 引数を解析
begin
  opt_parser.parse!(ARGV)
rescue OptionParser::InvalidOption => e
  puts "エラー: #{e.message}"
  puts opt_parser
  exit 1
end

# 残りの引数(ファイル名など)
files = ARGV

# オプションの使用
name = options[:name] || 'World'
count = options[:count] || 1
verbose = options[:verbose]

# 実行
count.times do |i|
  puts "こんにちは、#{name}さん!"
  puts "実行回数: #{i + 1}" if verbose
end

if files.any?
  puts "処理ファイル: #{files.join(', ')}"
end

より高度な例

#!/usr/bin/env ruby

require 'optparse'
require 'ostruct'

# オプションを格納するオブジェクト
options = OpenStruct.new
options.verbose = false
options.format = 'txt'
options.output = nil
options.force = false

opt_parser = OptionParser.new do |opts|
  opts.banner = "ファイル処理ツール v1.0\n使用法: #{$0} [オプション] 入力ファイル..."

  opts.separator ""
  opts.separator "基本オプション:"

  # 必須オプション(後でチェック)
  opts.on('-o', '--output FILE', '出力ファイル名') do |output|
    options.output = output
  end

  # 選択肢からの選択
  opts.on('-f', '--format FORMAT', %w[txt csv json xml], 
          '出力形式 (txt, csv, json, xml)') do |format|
    options.format = format
  end

  # 数値レンジ
  opts.on('-l', '--limit LIMIT', Integer, '処理制限数 (1-1000)') do |limit|
    if limit < 1 || limit > 1000
      raise OptionParser::InvalidArgument, "制限数は1-1000の範囲で指定してください"
    end
    options.limit = limit
  end

  # 配列オプション
  opts.on('-t', '--tags TAG1,TAG2,TAG3', Array, 'タグ一覧(カンマ区切り)') do |tags|
    options.tags = tags
  end

  # 正規表現オプション
  opts.on('-p', '--pattern REGEX', Regexp, 'フィルタパターン(正規表現)') do |pattern|
    options.pattern = pattern
  end

  opts.separator ""
  opts.separator "フラグオプション:"

  opts.on('-v', '--verbose', '詳細出力') do
    options.verbose = true
  end

  opts.on('-q', '--quiet', '静寂モード') do
    options.quiet = true
  end

  opts.on('--force', '強制実行') do
    options.force = true
  end

  opts.on('--dry-run', '実際には実行しない') do
    options.dry_run = true
  end

  opts.separator ""
  opts.separator "その他:"

  opts.on_tail('-h', '--help', 'このヘルプを表示') do
    puts opts
    exit
  end

  opts.on_tail('--version', 'バージョンを表示') do
    puts "ファイル処理ツール v1.0"
    exit
  end
end

# 引数解析とエラー処理
begin
  opt_parser.parse!(ARGV)
rescue OptionParser::InvalidOption => e
  STDERR.puts "エラー: #{e.message}"
  STDERR.puts opt_parser
  exit 1
rescue OptionParser::InvalidArgument => e
  STDERR.puts "エラー: #{e.message}"
  exit 1
end

# 必須パラメータのチェック
if ARGV.empty?
  STDERR.puts "エラー: 入力ファイルが指定されていません"
  STDERR.puts opt_parser
  exit 1
end

# 相互排他オプションのチェック
if options.verbose && options.quiet
  STDERR.puts "エラー: --verbose と --quiet は同時に指定できません"
  exit 1
end

# 設定の表示
unless options.quiet
  puts "処理設定:"
  puts "  入力ファイル: #{ARGV.join(', ')}"
  puts "  出力ファイル: #{options.output || '標準出力'}"
  puts "  出力形式: #{options.format}"
  puts "  制限数: #{options.limit || '無制限'}"
  puts "  タグ: #{options.tags&.join(', ') || 'なし'}"
  puts "  パターン: #{options.pattern || 'なし'}"
  puts "  詳細出力: #{options.verbose ? 'あり' : 'なし'}"
  puts "  ドライラン: #{options.dry_run ? 'あり' : 'なし'}"
  puts
end

# ファイル処理の実行
ARGV.each do |input_file|
  unless File.exist?(input_file)
    STDERR.puts "警告: ファイルが見つかりません: #{input_file}"
    next
  end

  puts "処理中: #{input_file}" if options.verbose

  # ファイル処理のシミュレーション
  unless options.dry_run
    lines = File.readlines(input_file)
    
    # パターンフィルタリング
    if options.pattern
      lines = lines.select { |line| line.match?(options.pattern) }
    end

    # 制限数の適用
    if options.limit
      lines = lines.first(options.limit)
    end

    # 出力処理
    output_content = case options.format
                     when 'csv'
                       lines.map { |line| "\"#{line.chomp}\"" }.join(",\n")
                     when 'json'
                       require 'json'
                       lines.map(&:chomp).to_json
                     when 'xml'
                       "<lines>\n" + lines.map { |line| "  <line>#{line.chomp}</line>" }.join("\n") + "\n</lines>"
                     else
                       lines.join
                     end

    # 出力
    if options.output
      File.write(options.output, output_content)
      puts "出力完了: #{options.output}" unless options.quiet
    else
      puts output_content
    end
  end
end

puts "処理完了" unless options.quiet

設定ファイル読み込みとの組み合わせ

#!/usr/bin/env ruby

require 'optparse'
require 'yaml'
require 'json'

class ConfigurableApp
  def initialize
    @options = {
      config_file: nil,
      host: 'localhost',
      port: 8080,
      debug: false,
      log_level: 'info'
    }
  end

  def parse_options
    opt_parser = OptionParser.new do |opts|
      opts.banner = "設定可能アプリケーション\n使用法: #{$0} [オプション]"

      opts.on('-c', '--config FILE', '設定ファイル (YAML/JSON)') do |file|
        @options[:config_file] = file
      end

      opts.on('-H', '--host HOST', 'ホスト名') do |host|
        @options[:host] = host
      end

      opts.on('-p', '--port PORT', Integer, 'ポート番号') do |port|
        @options[:port] = port
      end

      opts.on('-d', '--debug', 'デバッグモード') do
        @options[:debug] = true
      end

      opts.on('-l', '--log-level LEVEL', %w[debug info warn error],
              'ログレベル (debug, info, warn, error)') do |level|
        @options[:log_level] = level
      end

      opts.on('-h', '--help', 'ヘルプを表示') do
        puts opts
        exit
      end
    end

    opt_parser.parse!
  end

  def load_config
    return unless @options[:config_file]

    unless File.exist?(@options[:config_file])
      STDERR.puts "エラー: 設定ファイルが見つかりません: #{@options[:config_file]}"
      exit 1
    end

    begin
      config_content = File.read(@options[:config_file])
      
      # ファイル拡張子で形式を判定
      config_data = case File.extname(@options[:config_file]).downcase
                    when '.yml', '.yaml'
                      YAML.safe_load(config_content)
                    when '.json'
                      JSON.parse(config_content)
                    else
                      STDERR.puts "エラー: 未対応の設定ファイル形式"
                      exit 1
                    end

      # 設定ファイルの値をデフォルトとして使用(コマンドライン引数が優先)
      config_data.each do |key, value|
        sym_key = key.to_sym
        if @options.key?(sym_key) && @options[sym_key] == default_value(sym_key)
          @options[sym_key] = value
        end
      end

    rescue YAML::SyntaxError, JSON::ParserError => e
      STDERR.puts "エラー: 設定ファイルの解析に失敗しました: #{e.message}"
      exit 1
    end
  end

  def run
    parse_options
    load_config

    puts "アプリケーション設定:"
    puts "  ホスト: #{@options[:host]}"
    puts "  ポート: #{@options[:port]}"
    puts "  デバッグ: #{@options[:debug] ? '有効' : '無効'}"
    puts "  ログレベル: #{@options[:log_level]}"
    puts "  設定ファイル: #{@options[:config_file] || 'なし'}"

    # アプリケーションのメイン処理
    start_server
  end

  private

  def default_value(key)
    defaults = {
      host: 'localhost',
      port: 8080,
      debug: false,
      log_level: 'info'
    }
    defaults[key]
  end

  def start_server
    puts "\nサーバーを開始中..."
    puts "http://#{@options[:host]}:#{@options[:port]}"
    
    if @options[:debug]
      puts "デバッグモード: 詳細ログが有効です"
    end

    # サーバー起動のシミュレーション
    puts "サーバーが正常に起動しました (Ctrl+C で停止)"
    
    begin
      loop do
        sleep 1
      end
    rescue Interrupt
      puts "\nサーバーを停止中..."
      puts "サーバーが停止しました"
    end
  end
end

# アプリケーション実行
app = ConfigurableApp.new
app.run

バリデーションとエラーハンドリングの例

#!/usr/bin/env ruby

require 'optparse'

class ValidatedApp
  def initialize
    @options = {}
    @errors = []
  end

  def parse_options
    opt_parser = OptionParser.new do |opts|
      opts.banner = "バリデーション付きアプリケーション"

      opts.on('-e', '--email EMAIL', 'メールアドレス') do |email|
        @options[:email] = email
      end

      opts.on('-u', '--url URL', 'URL') do |url|
        @options[:url] = url
      end

      opts.on('-f', '--file FILE', 'ファイルパス') do |file|
        @options[:file] = file
      end

      opts.on('-n', '--number NUMBER', Integer, '数値 (1-100)') do |number|
        @options[:number] = number
      end

      opts.on('-d', '--date DATE', '日付 (YYYY-MM-DD)') do |date|
        @options[:date] = date
      end

      opts.on('-h', '--help', 'ヘルプを表示') do
        puts opts
        exit
      end
    end

    begin
      opt_parser.parse!
    rescue OptionParser::InvalidOption, OptionParser::InvalidArgument => e
      @errors << e.message
    end

    validate_options
  end

  def validate_options
    # メールアドレスのバリデーション
    if @options[:email]
      email_regex = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
      unless @options[:email].match?(email_regex)
        @errors << "無効なメールアドレス形式: #{@options[:email]}"
      end
    end

    # URLのバリデーション
    if @options[:url]
      url_regex = /\Ahttps?:\/\/[\S]+\z/
      unless @options[:url].match?(url_regex)
        @errors << "無効なURL形式: #{@options[:url]}"
      end
    end

    # ファイル存在チェック
    if @options[:file]
      unless File.exist?(@options[:file])
        @errors << "ファイルが見つかりません: #{@options[:file]}"
      end
    end

    # 数値範囲チェック
    if @options[:number]
      unless (1..100).include?(@options[:number])
        @errors << "数値は1-100の範囲で指定してください: #{@options[:number]}"
      end
    end

    # 日付形式チェック
    if @options[:date]
      begin
        require 'date'
        Date.strptime(@options[:date], '%Y-%m-%d')
      rescue Date::Error
        @errors << "無効な日付形式: #{@options[:date]} (YYYY-MM-DD形式で入力してください)"
      end
    end
  end

  def run
    parse_options

    if @errors.any?
      STDERR.puts "バリデーションエラー:"
      @errors.each { |error| STDERR.puts "  - #{error}" }
      exit 1
    end

    puts "バリデーション成功!"
    puts "設定値:"
    @options.each do |key, value|
      puts "  #{key}: #{value}"
    end
  end
end

# アプリケーション実行
app = ValidatedApp.new
app.run

サンプル設定ファイル

# config.yml
host: "0.0.0.0"
port: 3000
debug: true
log_level: "debug"
{
  "host": "0.0.0.0",
  "port": 3000,
  "debug": true,
  "log_level": "debug"
}

実行例

# 基本的な使用
ruby script.rb --name "Ruby" --count 3 --verbose file1.txt file2.txt

# 設定ファイルと組み合わせ
ruby configurable_app.rb --config config.yml --port 9000

# バリデーション例
ruby validated_app.rb --email test@example.com --url https://example.com --number 50