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
The current flag definition whose value is still pending
@return [Toys::Flag] The pending flag definition @return [nil] if there is no pending flag
The collected tool data from parsed arguments. @return [Hash]
An array of parse error messages. @return [Array<Toys::ArgParser::UsageError>]
All command line arguments that have been parsed. @return [Array<String>]
The tool definition governing this parser. @return [Toys::ToolDefinition]
All args that were not matched. @return [Array<String>]
Flags that were not matched. @return [Array<String>]
Extra positional args that were not matched. @return [Array<String>]
Public Class Methods
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
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
Determine if this parser is finished @return [Boolean]
# File lib/toys/arg_parser.rb, line 358 def finished? @finished end
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
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
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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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