# typed: true require 'cli/kit' module CLI module Kit module Args class Parser extend T::Sig autoload :Node, 'cli/kit/args/parser/node' Error = Class.new(Args::Error) class InvalidOptionError < Error extend T::Sig sig { params(option: String).void } def initialize(option) super("invalid option -- '#{option}'") end end class OptionRequiresAnArgumentError < Error extend T::Sig sig { params(option: String).void } def initialize(option) super("option requires an argument -- '#{option}'") end end sig { params(tokens: T::Array[Tokenizer::Token]).returns(T::Array[Node]) } def parse(tokens) nodes = T.let([], T::Array[Node]) args = T.let(tokens, T::Array[T.nilable(Tokenizer::Token)]) args << nil # to make each_cons pass (args.last, nil) on the final round. state = :init # TODO: test that "--height -- 3" is parsed correctly. args.each_cons(2) do |(arg, next_arg)| case state when :skip state = :init when :init state, val = parse_token(T.must(arg), next_arg) nodes << val when :unparsed unless arg.is_a?(Tokenizer::Token::UnparsedArgument) raise(Error, 'bug: non-unparsed argument after unparsed argument') end unparsed = nodes.last unless unparsed.is_a?(Node::Unparsed) # :nocov: not actually possible, in theory raise(Error, 'bug: parser failed to recognize first unparsed argument') # :nocov: end unparsed.value << arg.value end end nodes end sig { params(definition: Definition).void } def initialize(definition) @defn = definition end private sig do params(token: Tokenizer::Token, next_token: T.nilable(Tokenizer::Token)) .returns([Symbol, Parser::Node]) end def parse_token(token, next_token) case token when Tokenizer::Token::LongOptionName case @defn.lookup_long(token.value) when Definition::Option [:skip, parse_option(token, next_token)] when Definition::Flag [:init, Node::LongFlag.new(token.value)] else raise(InvalidOptionError, token.value) end when Tokenizer::Token::ShortOptionName case @defn.lookup_short(token.value) when Definition::Option [:skip, parse_option(token, next_token)] when Definition::Flag [:init, Node::ShortFlag.new(token.value)] else raise(InvalidOptionError, token.value) end when Tokenizer::Token::OptionValue raise(Error, "bug: unexpected option value in argument parse sequence: #{token.value}") when Tokenizer::Token::PositionalArgument [:init, Node::Argument.new(token.value)] when Tokenizer::Token::OptionValueOrPositionalArgument [:init, Node::Argument.new(token.value)] when Tokenizer::Token::UnparsedArgument [:unparsed, Node::Unparsed.new([token.value])] else raise(Error, "bug: unexpected token type: #{token.class}") end end sig { params(arg: Tokenizer::Token::OptionName, next_arg: T.nilable(Tokenizer::Token)).returns(Node) } def parse_option(arg, next_arg) case next_arg when nil, Tokenizer::Token::LongOptionName, Tokenizer::Token::ShortOptionName, Tokenizer::Token::PositionalArgument raise(OptionRequiresAnArgumentError, arg.value) when Tokenizer::Token::OptionValue, Tokenizer::Token::OptionValueOrPositionalArgument case arg when Tokenizer::Token::LongOptionName Node::LongOption.new(arg.value, next_arg.value) when Tokenizer::Token::ShortOptionName Node::ShortOption.new(arg.value, next_arg.value) else raise(Error, "bug: unexpected token type: #{arg.class}") end else raise(Error, "bug: unexpected argument type: #{next_arg.class}") end end end end end end