lib/ridl/optparse_ext.rb



#--------------------------------------------------------------------
# optparse_ext.rb - Ruby StdLib OptionParser extensions for RIDL
#
# Author: Martin Corino
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the RIDL LICENSE which is
# included with this program.
#
# Copyright (c) Remedy IT Expertise BV
#--------------------------------------------------------------------
require 'optparse'

# Customize OptionParser RequiredArgument switch class to support
# multi character short switches (single '-' prefix) with (optional)
# arguments.
# These must be defined using a format like '-X<text>' or '-X{text}'
# where 'X' is the common start character for a group of short multichar
# switches.
# Switch arguments should be indicated by either appending '=ARG' or
# ' ARG' giving something like '-X<text>=ARG' or '-X<text> ARG' where
# 'ARG' is an arbitrary non-blank text
class OptionParser::Switch::RequiredArgument
  def initialize(pattern = nil, conv = nil,
                 short = nil, long = nil, arg = nil,
                 desc = ([] if short or long), block = nil, &_block)
    block ||= _block
    super(pattern, conv, short, long, arg, desc, block)
    if (@long.nil? || @long.empty?) && (@arg =~ /^(<.*>|[\{].*[\}])((=|\s).*)?/)
      @multichar_short = true
      @has_arg = (@arg =~ /^(<.*>|[\{].*[\}])(=|\s).*$/ ? true : false)
    end
  end
  alias :_org_parse :parse
  def parse(arg, argv)
    if @multichar_short && @has_arg
      # unless arg included in rest of switch or next arg is not a switch
      unless (arg && arg =~ /.*=.*/) || (argv.first =~ /^-/)
        # concatenate next arg
        arg ||= ''
        arg += "=#{argv.shift}"
      end
    end
    self._org_parse(arg, argv)
  end
end

module IDL
  class OptionList
    class Option
      class Group
        class ParamSet
          class Configurator
            def initialize(set)
              @set = set
            end

            def on_exec(&block)
              ext_klass = class << @set; self; end
              ext_klass.send(:define_method, :_exec, &block)
              ext_klass.send(:protected, :_exec)
            end

            def with(param, options = {})
              @set.define_params({param => options})
            end

            def without(*params)
              params.each { |p| @set.params.delete(p.to_sym) }
            end
          end

          attr_reader :params

          def initialize(options)
            @description = Array === options[:description] ? options[:description] : (options[:description] || '').split('\n')
            @all_params = options[:all_params] == true
            @params = {}
            parms = options[:params] || options[:param]
            define_params(parms) if parms
          end

          def run(param, options, *handler_args)
            key = to_key(param)
            if @all_params || @params.has_key?(key)
              param = key if String === param && @params.has_key?(key)
              param_arg = @params.has_key?(param) ? @params[param][:option_name] : param
              if self.respond_to?(:_exec, true)
                _exec(param_arg, options, *handler_args)
              elsif @params.has_key?(param)
                if @params[param][:option_type] == :list
                  options[@params[param][:option_name]] ||= []
                  options[@params[param][:option_name]] << (handler_args.size == 1 ? handler_args.shift : handler_args)
                elsif @params[param][:option_type] == :noop
                  # do nothing
                else
                  options[@params[param][:option_name]] = handler_args.empty? ?
                      (@params[param].has_key?(:option_value) ? @params[param][:option_value] : true) :
                      (handler_args.size == 1 ? handler_args.shift : handler_args)
                end
              end
              return true
            end
            return false
          end

          def description
            @params.values.inject(@description) { |list, vopt| list.concat(vopt[:description] || []) }
          end

          def define_params(spec = {})
            case spec
            when String, Hash
              define_param(spec)
            when Array
              spec.each { |p| define_param(p) }
            end
          end

          private

          def to_key(param)
            return param if Symbol === param

            # convert empty strings to single space strings before symbolizing
            String === param ? (param.empty? ? ' ' : param).to_sym : nil
          end

          def define_param(spec)
            case spec
            when String
              key = to_key(spec)
              @params[key] = { option_name: key }
            when Hash
              spec.each do |k, v|
                @params[to_key(k)] = (if Hash === v
                  {
                    option_name: to_key(v[:option_name] || k),
                    option_type: v[:type],
                    option_value: v.has_key?(:value) ? v[:value] : true,
                    description: Array === v[:description] ? v[:description] : (v[:description] || '').split('\n')
                  }
                else
                  { option_name: to_key(v) }
                end)
              end
            end
          end
        end # ParamSet

        class Configurator
          def initialize(grp)
            @group = grp
          end

          def on_prepare(&block)
            ext_klass = class << @group; self; end
            ext_klass.send(:define_method, :_prepare, &block)
            ext_klass.send(:protected, :_prepare)
          end

          def define_param_set(id, options = {}, &block)
            id = id.to_sym
            raise "option parameter set [#{id}] already exists" if @group.sets.has_key?(id)

            @group.sets[id] = ParamSet.new(options)
            block.call(ParamSet::Configurator.new(@group.sets[id])) if block_given?
          end
          alias :for_params :define_param_set

          def modify_param_set(id, options = {}, &block)
            id = id.to_sym
            parms = options[:params] ? options.delete(:params) : options.delete(:param)
            @group.sets[id] ||= ParamSet.new(options)
            @group.sets[id].define_params(parms) if parms
            block.call(ParamSet::Configurator.new(@group.sets[id])) if block_given?
          end
          alias :modify_params :modify_param_set
          alias :with_params :modify_param_set

          def define_param(id, options = {}, &block)
            define_param_set("#{id}_set", options) do |pscfg|
              pscfg.with(id)
              pscfg.on_exec(&block)
            end
          end
          alias :for_param :define_param
          alias :with_param :define_param

          def without_param(id)
            @group.sets.delete("#{id}_set")
          end
          alias :without_set :without_param
          alias :without_params :without_param
        end # Configurator

        attr_reader :sets

        def initialize(id, options)
          @test = options[:test] || true
          @description = Array === options[:description] ? options[:description] : (options[:description] || '').split('\n')
          @sets = {}
          if options[:params] && Hash === options[:params]
            @sets[id] = ParamSet.new(params: options[:params])
          end
        end

        def description
          @sets.values.inject(@description.dup) { |desc, a| desc.concat(a.description) }
        end

        def run(arg, options)
          ext_args = []
          if self.respond_to?(:_prepare, true)
            result = _prepare(arg, options)
            return false unless result && !result.empty?

            arg = result.shift
            ext_args = result
          else
            case @test
            when TrueClass
            when Regexp
              return false unless @test =~ arg
            else
              return false unless @test == arg
            end
          end
          return handle_sets(arg, options, *ext_args)
        end

        private

        def handle_sets(param, options, *ext_args)
          @sets.values.inject(false) { |f, s| s.run(param, options, *ext_args) || f }
        end
      end # Group

      class Configurator
        def initialize(opt)
          @option = opt
        end

        def define_group(id, options = {}, &block)
          id = id.to_sym
          raise "option group [#{id}] already exists" if @option.groups.has_key?(id)

          @option.groups[id] = Group.new(id, options)
          block.call(Group::Configurator.new(@option.groups[id])) if block_given?
        end
        alias :for_group :define_group

        def modify_group(id, options = {}, &block)
          id = id.to_sym
          parms = options[:params] ? options.delete(:params) : options.delete(:param)
          @option.groups[id] ||= Group.new(id, options)
          grpcfg = Group::Configurator.new(@option.groups[id])
          grpcfg.modify_param_set(id, params: parms) if parms
          block.call(grpcfg) if block_given?
        end
        alias :with_group :modify_group

        def undefine_group(id)
          @option.groups.delete(id.to_sym)
        end

        def define_param_set(id, options = {}, &block)
          modify_group :default, {test: true} do |grpcfg|
            grpcfg.define_param_set(id, options, &block)
          end
        end
        alias :for_set :define_param_set
        alias :for_params :define_param_set

        def on_exec(options = {}, &block)
          modify_group :default, {test: true} do |grpcfg|
            grpcfg.modify_param_set(:default, options.merge({all_params: true})) do |pscfg|
              pscfg.on_exec(&block)
            end
          end
        end

        def define_param(id, options = {}, &block)
          modify_group :default, {test: true} do |grpcfg|
            grpcfg.define_param_set("#{id}_set", options) do |pscfg|
              pscfg.with(id)
              pscfg.on_exec(&block)
            end
          end
        end
        alias :for_param :define_param

        def without_param(id)
          if @option.groups.has_key?(:default)
            modify_group :default do |grpcfg|
              grpcfg.without_set("#{id}_set")
            end
          end
        end
        alias :without_set :without_param
        alias :without_params :without_param
      end

      attr_reader :switch, :type, :separator, :groups

      def initialize(switch, options)
        @switch = switch
        @type = options[:type] || TrueClass
        @separator = options[:separator] == true
        @description = Array === options[:description] ? options[:description] : (options[:description] ? options[:description].split('\n') : [''])
        @groups = {}
      end

      def description(indent = "")
        @groups.values.inject(@description.dup) { |desc, h| desc.concat(h.description.collect { |desc| "\r#{indent}  #{desc}" }) }
      end

      def run(arg, options)
        unless @groups.values.inject(false) { |f, h| h.run(arg, options) || f }
          raise ArgumentError, "unknown option [#{arg}] for switch '#{@switch}'"
        end
      end
    end # Option

    def initialize
      @options = {}
    end

    def define_switch(switch, options = {}, &block)
      switch = switch.to_s
      raise "switch types mismatch" if @options.has_key?(switch) && options[:type] && options[:type] != @options[switch].type

      @options[switch] ||= Option.new(switch, options)
      block.call(Option::Configurator.new(@options[switch])) if block_given?
    end
    alias :for_switch :define_switch
    alias :switch :define_switch

    def undefine_switch(switch)
      switch = switch.to_s
      @options.delete(switch)
    end

    def to_option_parser(optp, option_holder)
      @options.each do |sw, op|
        (arg_list = [sw]) << op.type
        arg_list.concat(op.description(optp.summary_indent))
        optp.on(*arg_list) do |v|
          op.run(v, option_holder.options)
        end
        optp.separator '' if op.separator
      end
    end
  end # OptionList
end # IDL