# frozen_string_literal: true # Copyright (C) 2023 Thomas Baron # # This file is part of term_utils. # # term_utils is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, version 3 of the License. # # term_utils is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with term_utils. If not, see . module TermUtils module AP # Represents the argument list parser. class Parser # Constructs a new Parser. def initialize end # Parses a given list of arguments. # @param syntax [Syntax] # @param arguments [Array] # @param opts [Hash] # @option opts [Boolean] :strict Whether the Syntax must be considered as strict. # @return [Result] # @raise [ParseError] # @raise [SyntaxError] def parse_arguments(syntax, arguments, opts = {}, &block) syntax = syntax.dup syntax.finalize! arguments = arguments.dup res = TermUtils::AP::Result.new(syntax) catch :done do parse0(res, syntax, arguments, opts) end res.remaining_arguments = arguments res.walk(&block) if block res end # Tests whether a given sample matches a shortcut flag. # @param shortcut_flags [Hash] # @param arg [String] # @return [Array, nil] Replacements on success, nil otherwise. def self.match_shortcut_flag(shortcut_flags, arg) shortcut_flags.each do |label, flag| next unless arg.start_with? label return [flag.label, arg[label.length..]] end nil end # Evaluates the added number of min occurs of a given array of articles. # @param articles [Array] # @return [Integer] def self.eval_article_min_occurs(articles) articles.inject(0) { |acc, a| acc + a.min_occurs } end private # Parses a given argument list. # @param result [Result] # @param syntax [Syntax] # @param arguments [Array] # @raise [ParseError] def parse0(result, syntax, arguments, opts = {}) unflagged_params, flagged_params, shortcut_flags = syntax.fetch_parameters fp_occ = {} syntax.parameters.each { |p| fp_occ[p.id] = 0 if p.flagged? } up_occ = 0 loop do break if arguments.empty? if arguments.first.start_with?('-') && !%w[- --].include?(arguments.first) # Flagged unless flagged_params.key? arguments.first # Unknown flag # Try shortcut flag flag, arg = self.class.match_shortcut_flag(shortcut_flags, arguments.first) if flag && arg # Shortcut match arguments.shift arguments.unshift arg arguments.unshift flag end end unless flagged_params.key? arguments.first # Unknown flag # End of parsing raise TermUtils::AP::ParseError.new(message: 'flagged parameter unexpected', fault: arguments.first) if opts.fetch(:strict, false) break end param = flagged_params[arguments.first] if param.occur_bounded? && (fp_occ[param.id] >= param.max_occurs) # Max occurs reached raise TermUtils::AP::ParseError.new(message: 'occur limit reached', parameter: param.id, fault: arguments.first) if opts.fetch(:strict, false) break end fp_occ[param.id] += 1 arguments.shift param_res = TermUtils::AP::ParameterResult.new(result, param) parse0_param(param_res, param, arguments) else # Unflagged if unflagged_params.empty? # End of parsing raise TermUtils::AP::ParseError.new(message: 'unflagged parameter unexpected', fault: arguments.first) if opts.fetch(:strict, false) break end param = unflagged_params.first if arguments.first == '--' # End of parameter raise TermUtils::AP::ParseError.new(message: 'parameter not consumed', parameter: param.id) if up_occ < param.min_occurs arguments.shift unflagged_params.shift up_occ = 0 next end up_occ += 1 param_res = TermUtils::AP::ParameterResult.new(result, param) case parse0_param(param_res, param, arguments) when :esc_param raise TermUtils::AP::ParseError.new(message: 'parameter not consumed', parameter: param.id) if up_occ < param.min_occurs unflagged_params.shift up_occ = 0 else if !param.multiple_occurs? || (param.occur_bounded? && (up_occ >= param.max_occurs)) unflagged_params.shift up_occ = 0 end end end end # loop # Check min occurs syntax.parameters.each do |p| next if result.find_parameters(p.id).length >= p.min_occurs raise TermUtils::AP::ParseError.new(message: 'parameter not consumed', parameter: p.id) end end # Parses with a given Parameter. # @param result [ParameterResult] # @param param [Parameter] # @param arguments [Array] # @return [Symbol, nil] `:esc_param`, or nil. def parse0_param(result, param, arguments) arts = param.fetch_articles occ = 0 loop do break if arts.empty? if arguments.empty? # End of arguments raise TermUtils::AP::ParseError.new(message: 'article not consumed', parameter: param.id) if occ < arts.first.min_occurs raise TermUtils::AP::ParseError.new(message: 'article not consumed', parameter: param.id) if self.class.eval_article_min_occurs(arts[1..]) > 0 break end if arguments.first == '-' # End of article raise TermUtils::AP::ParseError.new(message: 'article not consumed', parameter: param.id) if occ < arts.first.min_occurs arguments.shift arts.shift occ = 0 next elsif arguments.first.start_with? '-' # End of parameter raise TermUtils::AP::ParseError.new(message: 'article not consumed', parameter: param.id) if occ < arts.first.min_occurs raise TermUtils::AP::ParseError.new(message: 'article not consumed', parameter: param.id) if self.class.eval_article_min_occurs(arts[1..]) > 0 if arguments.first == '--' arguments.shift return :esc_param end break end art = arts.first TermUtils::AP::ArticleResult.new(result, art, arguments.shift) occ += 1 if !art.multiple_occurs? || (art.occur_bounded? && (occ >= art.max_occurs)) arts.shift occ = 0 end end # loop nil end end end end