# frozen-string-literal: true
# Copyright (C) 2020 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]
# @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..-1]]
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, 'flagged parameter unexpected' 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, "occur limit reached (#{param.id})" 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, 'unflagged parameter unexpected' if opts.fetch(:strict, false)
break
end
param = unflagged_params.first
if arguments.first == '--'
# End of parameter
raise TermUtils::AP::ParseError, "parameter not consumed (#{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, "parameter not consumed (#{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, "parameter not consumed (#{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, 'article not consumed' if occ < arts.first.min_occurs
raise TermUtils::AP::ParseError, 'article not consumed' if self.class.eval_article_min_occurs(arts[1..-1]) > 0
break
end
if arguments.first == '-'
# End of article
raise TermUtils::AP::ParseError, 'article not consumed' 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, 'article not consumed' if occ < arts.first.min_occurs
raise TermUtils::AP::ParseError, 'article not consumed' if self.class.eval_article_min_occurs(arts[1..-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