class Toys::ArgParser

An internal class that parses command line arguments for a tool.

Generally, you should not need to use this class directly. It is called from {Toys::CLI}.

Constants

ARG_HANDLER
REMAINING_HANDLER

Attributes

active_flag_def[R]

The current flag definition whose value is still pending

@return [Toys::Flag] The pending flag definition @return [nil] if there is no pending flag

data[R]

The collected tool data from parsed arguments. @return [Hash]

errors[R]

An array of parse error messages. @return [Array<Toys::ArgParser::UsageError>]

parsed_args[R]

All command line arguments that have been parsed. @return [Array<String>]

tool[R]

The tool definition governing this parser. @return [Toys::ToolDefinition]

unmatched_args[R]

All args that were not matched. @return [Array<String>]

unmatched_flags[R]

Flags that were not matched. @return [Array<String>]

unmatched_positional[R]

Extra positional args that were not matched. @return [Array<String>]

Public Class Methods

new(cli, tool, default_data: {}, require_exact_flag_match: false) click to toggle source

Create an argument parser for a particular tool.

@param cli [Toys::CLI] The CLI in effect. @param tool [Toys::ToolDefinition] The tool defining the argument format. @param default_data [Hash] Additional initial data (such as verbosity). @param require_exact_flag_match [Boolean] Whether to require flag matches

be exact (not partial). Default is false.
# File lib/toys/arg_parser.rb, line 277
def initialize(cli, tool, default_data: {}, require_exact_flag_match: false)
  @require_exact_flag_match = require_exact_flag_match
  @loader = cli.loader
  @data = initial_data(cli, tool, default_data)
  @tool = tool
  @seen_flag_keys = []
  @errors = []
  @unmatched_args = []
  @unmatched_positional = []
  @unmatched_flags = []
  @parsed_args = []
  @active_flag_def = nil
  @active_flag_arg = nil
  @arg_defs = tool.positional_args
  @arg_def_index = 0
  @flags_allowed = true
  @finished = false
end

Public Instance Methods

finish() click to toggle source

Complete parsing. This should be called after all arguments have been processed. It does a final check for any errors, including:

*  The arguments ended with a flag that was expecting a value but wasn't
   provided.
*  One or more required arguments were never given a value.
*  One or more extra arguments were provided.
*  Restrictions defined in one or more flag groups were not fulfilled.

Any errors are added to the errors array. It also fills in final values for `Context::Key::USAGE_ERRORS` and `Context::Key::ARGS`.

After this method is called, this object is locked down, and no additional arguments may be parsed.

@return [self]

# File lib/toys/arg_parser.rb, line 407
def finish
  finish_active_flag
  finish_arg_defs
  finish_flag_groups
  finish_special_data
  @finished = true
  self
end
finished?() click to toggle source

Determine if this parser is finished @return [Boolean]

# File lib/toys/arg_parser.rb, line 358
def finished?
  @finished
end
flags_allowed?() click to toggle source

Whether flags are currently allowed. Returns false after `–` is received. @return [Boolean]

# File lib/toys/arg_parser.rb, line 350
def flags_allowed?
  @flags_allowed
end
next_arg_def() click to toggle source

The argument definition that will be applied to the next argument.

@return [Toys::PositionalArg] The next argument definition. @return [nil] if all arguments have been filled.

# File lib/toys/arg_parser.rb, line 368
def next_arg_def
  @arg_defs[@arg_def_index]
end
parse(args) click to toggle source

Incrementally parse a single string or an array of strings

@param args [String,Array<String>] @return [self]

# File lib/toys/arg_parser.rb, line 378
def parse(args)
  raise "Parser has finished" if @finished
  Array(args).each do |arg|
    @parsed_args << arg
    unless @tool.argument_parsing_disabled?
      check_flag_value(arg) || check_flag(arg) || handle_positional(arg)
    end
  end
  self
end

Private Instance Methods

add_data(key, handler, accept, value, type_name, display_name) click to toggle source
# File lib/toys/arg_parser.rb, line 541
def add_data(key, handler, accept, value, type_name, display_name)
  if accept
    match = accept.match(value)
    unless match
      error_class = type_name == :flag ? FlagValueUnacceptableError : ArgValueUnacceptableError
      suggestions = accept.respond_to?(:suggestions) ? accept.suggestions(value) : nil
      @errors << error_class.new(value: value, name: display_name, suggestions: suggestions)
      return
    end
    value = accept.convert(*Array(match))
  end
  if handler
    value = handler.call(value, @data[key])
  end
  @data[key] = value
end
check_flag(arg) click to toggle source
# File lib/toys/arg_parser.rb, line 448
def check_flag(arg)
  return false unless @flags_allowed
  case arg
  when "--"
    @flags_allowed = false
  when /\A(--\w[?\w-]*)=(.*)\z/
    handle_valued_flag(::Regexp.last_match(1), ::Regexp.last_match(2))
  when /\A--.+\z/
    handle_plain_flag(arg)
  when /\A-(.+)\z/
    handle_single_flags(::Regexp.last_match(1))
  else
    return false
  end
  true
end
check_flag_value(arg) click to toggle source
# File lib/toys/arg_parser.rb, line 437
def check_flag_value(arg)
  return false unless @active_flag_def
  result = @active_flag_def.value_type == :required || !arg.start_with?("-")
  add_data(@active_flag_def.key, @active_flag_def.handler, @active_flag_def.acceptor,
           result ? arg : nil, :flag, @active_flag_arg)
  @seen_flag_keys << @active_flag_def.key
  @active_flag_def = nil
  @active_flag_arg = nil
  result
end
find_flag(name) click to toggle source
# File lib/toys/arg_parser.rb, line 521
def find_flag(name)
  flag_result = @tool.resolve_flag(name)
  if flag_result.not_found? || @require_exact_flag_match && !flag_result.found_exact?
    @errors << FlagUnrecognizedError.new(
      value: name, suggestions: Compat.suggestions(name, @tool.used_flags)
    )
    @unmatched_flags << name
    @unmatched_args << name
    flag_result = nil
  elsif flag_result.found_multiple?
    @errors << FlagAmbiguousError.new(
      value: name, suggestions: flag_result.matching_flag_strings
    )
    @unmatched_flags << name
    @unmatched_args << name
    flag_result = nil
  end
  flag_result
end
finish_active_flag() click to toggle source
# File lib/toys/arg_parser.rb, line 558
def finish_active_flag
  if @active_flag_def
    if @active_flag_def.value_type == :required
      @errors << FlagValueMissingError.new(name: @active_flag_arg)
    else
      add_data(@active_flag_def.key, @active_flag_def.handler, @active_flag_def.acceptor,
               nil, :flag, @active_flag_arg)
    end
  end
end
finish_arg_defs() click to toggle source
# File lib/toys/arg_parser.rb, line 569
def finish_arg_defs
  arg_def = @arg_defs[@arg_def_index]
  if arg_def && arg_def.type == :required
    @errors << ArgMissingError.new(name: arg_def.display_name)
  end
  unless @unmatched_positional.empty?
    first_arg = @unmatched_positional.first
    @errors <<
      if @tool.runnable? || !@seen_flag_keys.empty?
        ExtraArgumentsError.new(values: @unmatched_positional, value: first_arg)
      else
        dictionary = @loader.list_subtools(@tool.full_name).map(&:simple_name)
        ToolUnrecognizedError.new(values: @tool.full_name + [first_arg],
                                  value: first_arg,
                                  suggestions: Compat.suggestions(first_arg, dictionary))
      end
  end
end
finish_flag_groups() click to toggle source
# File lib/toys/arg_parser.rb, line 588
def finish_flag_groups
  @tool.flag_groups.each do |group|
    @errors += Array(group.validation_errors(@seen_flag_keys))
  end
end
finish_special_data() click to toggle source
# File lib/toys/arg_parser.rb, line 594
def finish_special_data
  @data[Context::Key::USAGE_ERRORS] = @errors
  @data[Context::Key::ARGS] = @parsed_args
  @data[Context::Key::UNMATCHED_ARGS] = @unmatched_args
  @data[Context::Key::UNMATCHED_POSITIONAL] = @unmatched_positional
  @data[Context::Key::UNMATCHED_FLAGS] = @unmatched_flags
end
handle_plain_flag(name, following = "") click to toggle source
# File lib/toys/arg_parser.rb, line 471
def handle_plain_flag(name, following = "")
  flag_result = find_flag(name)
  flag_def = flag_result&.unique_flag
  return "" unless flag_def
  @seen_flag_keys << flag_def.key
  if flag_def.flag_type == :boolean
    add_data(flag_def.key, flag_def.handler, nil, !flag_result.unique_flag_negative?, :flag, name)
  elsif following.empty?
    if flag_def.value_type == :required || flag_result.unique_flag_syntax.value_delim == " "
      @active_flag_def = flag_def
      @active_flag_arg = name
    else
      add_data(flag_def.key, flag_def.handler, flag_def.acceptor, nil, :flag, name)
    end
  else
    add_data(flag_def.key, flag_def.handler, flag_def.acceptor, following, :flag, name)
    following = ""
  end
  following
end
handle_positional(arg) click to toggle source
# File lib/toys/arg_parser.rb, line 506
def handle_positional(arg)
  if @tool.flags_before_args_enforced?
    @flags_allowed = false
  end
  arg_def = next_arg_def
  unless arg_def
    @unmatched_positional << arg
    @unmatched_args << arg
    return
  end
  @arg_def_index += 1 unless arg_def.type == :remaining
  handler = arg_def.type == :remaining ? REMAINING_HANDLER : ARG_HANDLER
  add_data(arg_def.key, handler, arg_def.acceptor, arg, :arg, arg_def.display_name)
end
handle_single_flags(str) click to toggle source
# File lib/toys/arg_parser.rb, line 465
def handle_single_flags(str)
  until str.empty?
    str = handle_plain_flag("-#{str[0]}", str[1..-1])
  end
end
handle_valued_flag(name, value) click to toggle source
# File lib/toys/arg_parser.rb, line 492
def handle_valued_flag(name, value)
  flag_result = find_flag(name)
  flag_def = flag_result&.unique_flag
  return unless flag_def
  @seen_flag_keys << flag_def.key
  if flag_def.flag_type == :value
    add_data(flag_def.key, flag_def.handler, flag_def.acceptor, value, :flag, name)
  else
    add_data(flag_def.key, flag_def.handler, nil, !flag_result.unique_flag_negative?,
             :flag, name)
    @errors << FlagValueNotAllowedError.new(name: name)
  end
end
initial_data(cli, tool, default_data) click to toggle source
# File lib/toys/arg_parser.rb, line 421
def initial_data(cli, tool, default_data)
  data = {
    Context::Key::ARGS => nil,
    Context::Key::CLI => cli,
    Context::Key::CONTEXT_DIRECTORY => tool.context_directory,
    Context::Key::LOGGER => cli.logger_factory.call(tool),
    Context::Key::TOOL => tool,
    Context::Key::TOOL_SOURCE => tool.source_info,
    Context::Key::TOOL_NAME => tool.full_name,
    Context::Key::USAGE_ERRORS => [],
  }
  tool.default_data.each { |k, v| data[k] = v.clone }
  default_data.each { |k, v| data[k] ||= v }
  data
end