File: parser.rb

package info (click to toggle)
ruby-cri 2.15.12-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 384 kB
  • sloc: ruby: 2,776; makefile: 11
file content (244 lines) | stat: -rw-r--r-- 7,113 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# frozen_string_literal: true

module Cri
  # Cri::Parser is used for parsing command-line options and arguments.
  class Parser
    # Error that will be raised when an unknown option is encountered.
    class IllegalOptionError < Cri::Error
    end

    # Error that will be raised when an option with an invalid or
    # non-transformable value is encountered.
    class IllegalOptionValueError < Cri::Error
      attr_reader :definition
      attr_reader :value

      def initialize(definition, value)
        super("invalid value #{value.inspect} for #{definition.formatted_name} option")

        @value = value
        @definition = definition
      end
    end

    # Error that will be raised when an option without argument is
    # encountered.
    class OptionRequiresAnArgumentError < Cri::Error
    end

    # The delegate to which events will be sent. The following methods will
    # be send to the delegate:
    #
    # * `option_added(key, value, cmd)`
    # * `argument_added(argument, cmd)`
    #
    # @return [#option_added, #argument_added] The delegate
    attr_accessor :delegate

    # The options that have already been parsed.
    #
    # If the parser was stopped before it finished, this will not contain all
    # options and `unprocessed_arguments_and_options` will contain what is
    # left to be processed.
    #
    # @return [Hash] The already parsed options.
    attr_reader :options

    # The options and arguments that have not yet been processed. If the
    # parser wasn’t stopped (using {#stop}), this list will be empty.
    #
    # @return [Array] The not yet parsed options and arguments.
    attr_reader :unprocessed_arguments_and_options

    # Creates a new parser with the given options/arguments and definitions.
    #
    # @param [Array<String>] arguments_and_options An array containing the
    #   command-line arguments (will probably be `ARGS` for a root command)
    #
    # @param [Array<Cri::OptionDefinition>] option_defns An array of option
    #   definitions
    #
    # @param [Array<Cri::ParamDefinition>] param_defns An array of parameter
    #   definitions
    def initialize(arguments_and_options, option_defns, param_defns, explicitly_no_params)
      @unprocessed_arguments_and_options = arguments_and_options.dup
      @option_defns = option_defns
      @param_defns = param_defns
      @explicitly_no_params = explicitly_no_params

      @options       = {}
      @raw_arguments = []

      @running = false
      @no_more_options = false
    end

    # @return [Boolean] true if the parser is running, false otherwise.
    def running?
      @running
    end

    # Stops the parser. The parser will finish its current parse cycle but
    # will not start parsing new options and/or arguments.
    #
    # @return [void]
    def stop
      @running = false
    end

    # Parses the command-line arguments into options and arguments.
    #
    # During parsing, two errors can be raised:
    #
    # @raise IllegalOptionError if an unrecognised option was encountered,
    #   i.e. an option that is not present in the list of option definitions
    #
    # @raise OptionRequiresAnArgumentError if an option was found that did not
    #   have a value, even though this value was required.
    #
    # @return [Cri::Parser] The option parser self
    def run
      @running = true

      while running?
        # Get next item
        e = @unprocessed_arguments_and_options.shift
        break if e.nil?

        if e == '--'
          handle_dashdash(e)
        elsif e =~ /^--./ && !@no_more_options
          handle_dashdash_option(e)
        elsif e =~ /^-./ && !@no_more_options
          handle_dash_option(e)
        else
          add_argument(e)
        end
      end

      self
    ensure
      @running = false
    end

    # @return [Cri::ArgumentList] The list of arguments that have already been
    #   parsed, excluding the -- separator.
    def gen_argument_list
      ArgumentList.new(@raw_arguments, @explicitly_no_params, @param_defns)
    end

    private

    def handle_dashdash(elem)
      add_argument(elem)
      @no_more_options = true
    end

    def handle_dashdash_option(elem)
      # Get option key, and option value if included
      if elem =~ /^--([^=]+)=(.+)$/
        option_key   = Regexp.last_match[1]
        option_value = Regexp.last_match[2]
      else
        option_key    = elem[2..-1]
        option_value  = nil
      end

      # Find definition
      option_defn = @option_defns.find { |d| d.long == option_key }
      raise IllegalOptionError.new(option_key) if option_defn.nil?

      if %i[required optional].include?(option_defn.argument)
        # Get option value if necessary
        if option_value.nil?
          option_value = find_option_value(option_defn, option_key)
        end

        # Store option
        add_option(option_defn, option_value)
      else
        # Store option
        add_option(option_defn, true)
      end
    end

    def handle_dash_option(elem)
      # Get option keys
      option_keys = elem[1..-1].scan(/./)

      # For each key
      option_keys.each do |option_key|
        # Find definition
        option_defn = @option_defns.find { |d| d.short == option_key }
        raise IllegalOptionError.new(option_key) if option_defn.nil?

        if %i[required optional].include?(option_defn.argument)
          # Get option value
          option_value = find_option_value(option_defn, option_key)

          # Store option
          add_option(option_defn, option_value)
        else
          # Store option
          add_option(option_defn, true)
        end
      end
    end

    def find_option_value(option_defn, option_key)
      option_value = @unprocessed_arguments_and_options.shift
      if option_value.nil? || option_value =~ /^-/
        if option_defn.argument == :optional && option_defn.default
          option_value = option_defn.default
        elsif option_defn.argument == :required
          raise OptionRequiresAnArgumentError.new(option_key)
        else
          @unprocessed_arguments_and_options.unshift(option_value)
          option_value = true
        end
      end
      option_value
    end

    def add_option(option_defn, value, transform: true)
      key = key_for(option_defn)

      value = transform ? transform_value(option_defn, value) : value

      if option_defn.multiple
        options[key] ||= []
        options[key] << value
      else
        options[key] = value
      end

      delegate&.option_added(key, value, self)
    end

    def transform_value(option_defn, value)
      transformer = option_defn.transform

      if transformer
        begin
          transformer.call(value)
        rescue StandardError
          raise IllegalOptionValueError.new(option_defn, value)
        end
      else
        value
      end
    end

    def key_for(option_defn)
      (option_defn.long || option_defn.short).to_sym
    end

    def add_argument(value)
      @raw_arguments << value

      unless value == '--'
        delegate&.argument_added(value, self)
      end
    end
  end
end