class Consoler::Matcher

Argument/Options matcher

Given a list of arguments and a list option try to match them

Public Class Methods

new(arguments, options) click to toggle source

Create a matcher

@param [Consoler::Arguments] arguments List of arguments @param [Consoler::Options] options List of options

# File lib/consoler/matcher.rb, line 12
def initialize(arguments, options)
  @arguments = arguments
  @options = options

  @index = 0
  @matched_options = {}
  @argument_values = []
end

Public Instance Methods

match() click to toggle source

Match arguments against options

@return [Hash, nil] Matched information, or nil is returned when there was no match

# File lib/consoler/matcher.rb, line 24
def match
  parse_options = true

  _loop_args do |arg|
    unless parse_options
      @argument_values.push arg
      next
    end

    # when "argument" is --, then stop parsing the rest of the arguments
    # and treat the rest as regular arguments
    if arg == '--'
      parse_options = false
      next
    end

    analyzed = _analyze arg

    if analyzed.nil?
      return nil
    end
  end

  remaining = _match_arguments
  _fill_defaults

  if @matched_options.size == @options.size
    @matched_options['remaining'] = remaining

    # make sure all aliases are also filled
    @options.each do |option|
      option.aliases.each do |alias_|
        @matched_options[alias_.name] = @matched_options[option.name]
      end
    end

    return @matched_options
  end

  nil
end

Private Instance Methods

_analyze(arg) click to toggle source

Analyze a single argument

@param [String] arg Single argument @return [true, nil] true on success, nil on failure

# File lib/consoler/matcher.rb, line 72
def _analyze(arg)
  is_long = false
  is_short = false
  name = nil

  if arg[0..1] == '--'
    is_long = true
    name = arg[2..-1]
  elsif arg[0] == '-'
    is_short = true
    name = arg[1..-1]
  end

  # arg is not a long/short option, add to arguments values
  unless is_long || is_short
    @argument_values.push arg
    return true
  end

  unless name.nil?
    # get the name of the option, short options use the first character
    option_name = if is_short
                    name[0]
                  else
                    name
                  end

    option, matched = @options.get_with_alias option_name

    # no option by this name in options
    return nil if option.nil?

    # see if the type if right, short or long
    if matched.is_long && !is_long
      return nil
    elsif matched.is_short && !is_short
      return nil
    end

    if is_long
      if option.is_value
        # is_value needs a next argument for its value
        return nil if _peek_next.nil?

        @matched_options[option.name] = _peek_next
        _skip_next
      else
        option_value! option
      end
    end

    if is_short
      if name.size == 1 && option.is_value
        # is_value needs a next argument for its value
        return nil if _peek_next.nil?

        @matched_options[option.name] = _peek_next
        _skip_next
      else
        # for every character (short option) increment the option value
        name.split('').each do |n|
          short_option = @options.get n
          return nil if short_option.nil?

          option_value! short_option
        end
      end
    end
  end

  true
end
_each_optional_before_sorted() { |optionals[item| ... } click to toggle source

Loop through the optionals before map

Sorted by number of optionals in a group

@return [Consoler::Matcher]

# File lib/consoler/matcher.rb, line 335
def _each_optional_before_sorted
  @optionals_before.each do |_, optionals|
    tmp = []
    optionals.each do |optional_index, before|
      tmp.push(
        count: before.size,
        index: optional_index,
      )
    end

    tmp.sort! { |a, b| b[:count] - a[:count] }.each do |item|
      yield optionals[item[:index]]
    end
  end

  self
end
_fill_defaults() click to toggle source

Give all unmatched optional options there default value

@return [Consoler::Matcher]

# File lib/consoler/matcher.rb, line 318
def _fill_defaults
  @options.each do |option|
    next unless option.is_optional

    unless @matched_options.key? option.name
      @matched_options[option.name] = option.default_value
    end
  end

  self
end
_loop_args() { |args| ... } click to toggle source

Loop through the arguments

@yield [String] An argument @return [Consoler::Matcher]

# File lib/consoler/matcher.rb, line 166
def _loop_args
  @index = 0
  size = @arguments.args.size

  # use an incrementing index, to be able to peek to the next in the list
  # and to skip an item
  while @index < size
    yield @arguments.args[@index]

    _skip_next
  end

  self
end
_match_arguments() click to toggle source

Match arguments to defined option arguments

@return [Array<String>, nil] The remaining args,

or <tt>nil</tt> if there are not enough arguments
# File lib/consoler/matcher.rb, line 206
def _match_arguments
  @optionals_before = {}
  @optionals_before_has_remaining = false

  total_argument_values = @argument_values.size
  argument_values_index = 0

  _match_arguments_optionals_before

  @optionals_before.each do |mandatory_arg_name, optionals|
    # fill the optional argument option with a value if there are enough
    # arguments supplied (info available from optionals map)
    optionals.each do |_, optional|
      optional.each do |before|
        if before[:included]
          return nil if argument_values_index >= total_argument_values

          @matched_options[before[:name]] = @argument_values[argument_values_index]
          argument_values_index += 1
        end
      end
    end

    # only fill mandatory argument if its not the :REMAINING key
    if mandatory_arg_name != :REMAINING
      return nil if argument_values_index >= total_argument_values

      @matched_options[mandatory_arg_name] = @argument_values[argument_values_index]
      argument_values_index += 1
    end
  end

  remaining = []

  # left over arguments
  while argument_values_index < @argument_values.size
    remaining.push @argument_values[argument_values_index]
    argument_values_index += 1
  end

  remaining
end
_match_arguments_optionals_before() click to toggle source

Create a map of all optionals and before which mandatory argument they appear

@return [Consoler::Matcher]

# File lib/consoler/matcher.rb, line 252
def _match_arguments_optionals_before
  @optionals_before = {}
  tracker = {}

  @options.each do |option, _key|
    next unless option.is_argument

    if option.is_optional
      # setup tracker for optional group
      tracker[option.is_optional] = [] if tracker[option.is_optional].nil?

      # mark all optionals as not-included
      tracker[option.is_optional].push(
        included: false,
        name: option.name,
      )
    else
      @optionals_before[option.name] = tracker
      tracker = {}
    end
  end

  # make sure all optionals are accounted for in the map
  if tracker != {}
    # use a special key so we can handle it differently in the filling process
    @optionals_before[:REMAINING] = tracker
    @optionals_before_has_remaining = true
  end

  _match_arguments_options_before_matcher

  self
end
_match_arguments_options_before_matcher() click to toggle source

Match remaining args against the optionals map

@return [Consoler::Matcher]

# File lib/consoler/matcher.rb, line 289
def _match_arguments_options_before_matcher
  # number of arguments that are needed to fill our mandatory argument options
  mandatories_matched = @optionals_before.size

  # there are optionals at the end of the options, don't match the void
  if @optionals_before_has_remaining
    mandatories_matched -= 1
  end

  total = 0

  # loop through optional map
  _each_optional_before_sorted do |before|
    # are there enough arguments left to fill this optional group
    if (total + before.size + mandatories_matched) <= @argument_values.size
      total += before.size

      before.each do |val|
        val[:included] = true
      end
    end
  end

  self
end
_peek_next() click to toggle source

Peek at the next argument

Only useful inside {Consoler::Matcher#_loop_args}

@return [String, nil]

# File lib/consoler/matcher.rb, line 186
def _peek_next
  @arguments.args[@index + 1]
end
_skip_next() click to toggle source

Skip to the next argument

Useful if you use a peeked argument

@return [nil] @return [Consoler::Matcher]

# File lib/consoler/matcher.rb, line 196
def _skip_next
  @index += 1

  self
end
option_value!(option) click to toggle source

Set the value of an option

Long or short option needed

@param [Consoler::Option]

# File lib/consoler/matcher.rb, line 150
def option_value!(option)
  if option.is_short
    if @matched_options[option.name].nil?
      @matched_options[option.name] = 0
    end

    @matched_options[option.name] += 1
  else
    @matched_options[option.name] = true
  end
end