consoler 1.0.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,6 @@
3
3
  require_relative 'matcher'
4
4
 
5
5
  module Consoler
6
-
7
6
  # Arguments
8
7
  #
9
8
  # @attr_reader [Array<String>] args Raw arguments
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Consoler
4
-
5
4
  # Consoler command
6
5
  #
7
6
  # Basically a named hash
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Consoler
4
-
5
4
  # Argument/Options matcher
6
5
  #
7
6
  # Given a list of arguments and a list option try to match them
8
7
  class Matcher
9
-
10
8
  # Create a matcher
11
9
  #
12
10
  # @param [Consoler::Arguments] arguments List of arguments
@@ -26,13 +24,15 @@ module Consoler
26
24
  def match
27
25
  parse_options = true
28
26
 
29
- loop_args do |arg|
30
- unless parse_options then
27
+ _loop_args do |arg|
28
+ unless parse_options
31
29
  @argument_values.push arg
32
30
  next
33
31
  end
34
32
 
35
- if arg == '--' then
33
+ # when "argument" is --, then stop parsing the rest of the arguments
34
+ # and treat the rest as regular arguments
35
+ if arg == '--'
36
36
  parse_options = false
37
37
  next
38
38
  end
@@ -47,126 +47,189 @@ module Consoler
47
47
  remaining = _match_arguments
48
48
  _fill_defaults
49
49
 
50
- if @matched_options.size == @options.size then
50
+ if @matched_options.size == @options.size
51
51
  @matched_options['remaining'] = remaining
52
+
53
+ # make sure all aliases are also filled
54
+ @options.each do |option|
55
+ option.aliases.each do |alias_|
56
+ @matched_options[alias_.name] = @matched_options[option.name]
57
+ end
58
+ end
59
+
52
60
  return @matched_options
53
61
  end
54
62
 
55
- return nil
63
+ nil
56
64
  end
57
65
 
58
66
  private
59
67
 
68
+ # Analyze a single argument
69
+ #
70
+ # @param [String] arg Single argument
71
+ # @return [true, nil] true on success, nil on failure
60
72
  def _analyze(arg)
61
73
  is_long = false
62
74
  is_short = false
63
75
  name = nil
64
76
 
65
- if arg[0..1] == '--' then
77
+ if arg[0..1] == '--'
66
78
  is_long = true
67
79
  name = arg[2..-1]
68
- elsif arg[0] == '-' then
80
+ elsif arg[0] == '-'
69
81
  is_short = true
70
82
  name = arg[1..-1]
71
83
  end
72
84
 
73
- if name.nil?
85
+ # arg is not a long/short option, add to arguments values
86
+ unless is_long || is_short
74
87
  @argument_values.push arg
75
88
  return true
76
89
  end
77
90
 
78
- unless name.nil? then
79
- option_name = if is_short then
91
+ unless name.nil?
92
+ # get the name of the option, short options use the first character
93
+ option_name = if is_short
80
94
  name[0]
81
95
  else
82
96
  name
83
97
  end
84
98
 
85
- option = @options.get option_name
99
+ option, matched = @options.get_with_alias option_name
86
100
 
101
+ # no option by this name in options
87
102
  return nil if option.nil?
88
103
 
89
- needs_short = option.is_short
90
- needs_long = option.is_long
91
-
92
- if needs_long and not is_long then
104
+ # see if the type if right, short or long
105
+ if matched.is_long && !is_long
93
106
  return nil
94
- elsif needs_short and not is_short then
107
+ elsif matched.is_short && !is_short
95
108
  return nil
96
109
  end
97
110
 
98
- if is_long then
99
- if option.is_value then
100
- return nil if peek_next.nil?
101
- @matched_options[name] = peek_next
102
- skip
111
+ if is_long
112
+ if option.is_value
113
+ # is_value needs a next argument for its value
114
+ return nil if _peek_next.nil?
115
+
116
+ @matched_options[option.name] = _peek_next
117
+ _skip_next
103
118
  else
104
- @matched_options[name] = true
119
+ option_value! option
105
120
  end
106
121
  end
107
122
 
108
- if is_short then
109
- if name.size == 1 and option.is_value then
110
- return nil if peek_next.nil?
111
- @matched_options[name] = peek_next
112
- skip
123
+ if is_short
124
+ if name.size == 1 && option.is_value
125
+ # is_value needs a next argument for its value
126
+ return nil if _peek_next.nil?
127
+
128
+ @matched_options[option.name] = _peek_next
129
+ _skip_next
113
130
  else
131
+ # for every character (short option) increment the option value
114
132
  name.split('').each do |n|
115
- if @matched_options[n].nil? then
116
- @matched_options[n] = 0
117
- end
133
+ short_option = @options.get n
134
+ return nil if short_option.nil?
118
135
 
119
- @matched_options[n] += 1
136
+ option_value! short_option
120
137
  end
121
138
  end
122
139
  end
123
140
  end
124
141
 
125
- return true
142
+ true
126
143
  end
127
144
 
128
- def current
129
- @arguments.args[@index]
130
- end
145
+ # Set the value of an option
146
+ #
147
+ # Long or short option needed
148
+ #
149
+ # @param [Consoler::Option]
150
+ def option_value!(option)
151
+ if option.is_short
152
+ if @matched_options[option.name].nil?
153
+ @matched_options[option.name] = 0
154
+ end
131
155
 
132
- def peek_next
133
- @arguments.args[@index + 1]
156
+ @matched_options[option.name] += 1
157
+ else
158
+ @matched_options[option.name] = true
159
+ end
134
160
  end
135
161
 
136
- def loop_args
162
+ # Loop through the arguments
163
+ #
164
+ # @yield [String] An argument
165
+ # @return [Consoler::Matcher]
166
+ def _loop_args
137
167
  @index = 0
138
168
  size = @arguments.args.size
139
169
 
140
- while @index < size do
141
- yield current
170
+ # use an incrementing index, to be able to peek to the next in the list
171
+ # and to skip an item
172
+ while @index < size
173
+ yield @arguments.args[@index]
142
174
 
143
- skip
175
+ _skip_next
144
176
  end
177
+
178
+ self
179
+ end
180
+
181
+ # Peek at the next argument
182
+ #
183
+ # Only useful inside {Consoler::Matcher#_loop_args}
184
+ #
185
+ # @return [String, nil]
186
+ def _peek_next
187
+ @arguments.args[@index + 1]
145
188
  end
146
189
 
147
- def skip
190
+ # Skip to the next argument
191
+ #
192
+ # Useful if you use a peeked argument
193
+ #
194
+ # @return [nil]
195
+ # @return [Consoler::Matcher]
196
+ def _skip_next
148
197
  @index += 1
198
+
199
+ self
149
200
  end
150
201
 
202
+ # Match arguments to defined option arguments
203
+ #
204
+ # @return [Array<String>, nil] The remaining args,
205
+ # or <tt>nil</tt> if there are not enough arguments
151
206
  def _match_arguments
152
207
  @optionals_before = {}
153
208
  @optionals_before_has_remaining = false
154
209
 
210
+ total_argument_values = @argument_values.size
155
211
  argument_values_index = 0
156
212
 
157
213
  _match_arguments_optionals_before
158
214
 
159
215
  @optionals_before.each do |mandatory_arg_name, optionals|
216
+ # fill the optional argument option with a value if there are enough
217
+ # arguments supplied (info available from optionals map)
160
218
  optionals.each do |_, optional|
161
219
  optional.each do |before|
162
- if before[:included] then
220
+ if before[:included]
221
+ return nil if argument_values_index >= total_argument_values
222
+
163
223
  @matched_options[before[:name]] = @argument_values[argument_values_index]
164
224
  argument_values_index += 1
165
225
  end
166
226
  end
167
227
  end
168
228
 
169
- if mandatory_arg_name != :REMAINING then
229
+ # only fill mandatory argument if its not the :REMAINING key
230
+ if mandatory_arg_name != :REMAINING
231
+ return nil if argument_values_index >= total_argument_values
232
+
170
233
  @matched_options[mandatory_arg_name] = @argument_values[argument_values_index]
171
234
  argument_values_index += 1
172
235
  end
@@ -174,7 +237,8 @@ module Consoler
174
237
 
175
238
  remaining = []
176
239
 
177
- while argument_values_index < @argument_values.size do
240
+ # left over arguments
241
+ while argument_values_index < @argument_values.size
178
242
  remaining.push @argument_values[argument_values_index]
179
243
  argument_values_index += 1
180
244
  end
@@ -182,78 +246,108 @@ module Consoler
182
246
  remaining
183
247
  end
184
248
 
249
+ # Create a map of all optionals and before which mandatory argument they appear
250
+ #
251
+ # @return [Consoler::Matcher]
185
252
  def _match_arguments_optionals_before
186
253
  @optionals_before = {}
187
254
  tracker = {}
188
255
 
189
- @options.each do |option, key|
256
+ @options.each do |option, _key|
190
257
  next unless option.is_argument
191
258
 
192
- if option.is_optional then
259
+ if option.is_optional
260
+ # setup tracker for optional group
193
261
  tracker[option.is_optional] = [] if tracker[option.is_optional].nil?
194
262
 
195
- tracker[option.is_optional].push({
263
+ # mark all optionals as not-included
264
+ tracker[option.is_optional].push(
196
265
  included: false,
197
266
  name: option.name,
198
- })
267
+ )
199
268
  else
200
269
  @optionals_before[option.name] = tracker
201
270
  tracker = {}
202
271
  end
203
272
  end
204
273
 
205
- if tracker != {} then
274
+ # make sure all optionals are accounted for in the map
275
+ if tracker != {}
276
+ # use a special key so we can handle it differently in the filling process
206
277
  @optionals_before[:REMAINING] = tracker
207
278
  @optionals_before_has_remaining = true
208
279
  end
209
280
 
210
- _match_arguments_optoins_before_matcher
281
+ _match_arguments_options_before_matcher
282
+
283
+ self
211
284
  end
212
285
 
213
- def _match_arguments_optoins_before_matcher
286
+ # Match remaining args against the optionals map
287
+ #
288
+ # @return [Consoler::Matcher]
289
+ def _match_arguments_options_before_matcher
290
+ # number of arguments that are needed to fill our mandatory argument options
214
291
  mandatories_matched = @optionals_before.size
215
292
 
216
- if @optionals_before_has_remaining then
293
+ # there are optionals at the end of the options, don't match the void
294
+ if @optionals_before_has_remaining
217
295
  mandatories_matched -= 1
218
296
  end
219
297
 
220
298
  total = 0
221
299
 
300
+ # loop through optional map
222
301
  _each_optional_before_sorted do |before|
223
- if (total + before.size + mandatories_matched) <= @argument_values.size then
302
+ # are there enough arguments left to fill this optional group
303
+ if (total + before.size + mandatories_matched) <= @argument_values.size
224
304
  total += before.size
225
305
 
226
306
  before.each do |val|
227
- val[:included] = true;
307
+ val[:included] = true
228
308
  end
229
309
  end
230
310
  end
311
+
312
+ self
231
313
  end
232
314
 
315
+ # Give all unmatched optional options there default value
316
+ #
317
+ # @return [Consoler::Matcher]
233
318
  def _fill_defaults
234
319
  @options.each do |option|
235
- if option.is_optional then
236
- unless @matched_options.has_key? option.name then
237
- @matched_options[option.name] = option.default_value
238
- end
320
+ next unless option.is_optional
321
+
322
+ unless @matched_options.key? option.name
323
+ @matched_options[option.name] = option.default_value
239
324
  end
240
325
  end
326
+
327
+ self
241
328
  end
242
329
 
330
+ # Loop through the optionals before map
331
+ #
332
+ # Sorted by number of optionals in a group
333
+ #
334
+ # @return [Consoler::Matcher]
243
335
  def _each_optional_before_sorted
244
336
  @optionals_before.each do |_, optionals|
245
337
  tmp = []
246
338
  optionals.each do |optional_index, before|
247
- tmp.push({
339
+ tmp.push(
248
340
  count: before.size,
249
341
  index: optional_index,
250
- })
342
+ )
251
343
  end
252
344
 
253
345
  tmp.sort! { |a, b| b[:count] - a[:count] }.each do |item|
254
346
  yield optionals[item[:index]]
255
347
  end
256
348
  end
349
+
350
+ self
257
351
  end
258
352
  end
259
353
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Consoler
4
-
5
4
  # Represents an option
6
5
  #
7
6
  # @attr_reader [String] name Name of the options
@@ -10,6 +9,7 @@ module Consoler
10
9
  # @attr_reader [Boolean] is_argument Is the option an argument
11
10
  # @attr_reader [Boolean] is_value Does the option need a value (<tt>--option=</tt>)
12
11
  # @attr_reader [Integer] is_optional Is the option optional (> 0) (<tt>[option]</tt>)
12
+ # @attr_reader [Array] aliases List of aliases of option (<tt>-v|--verbose</tt>)
13
13
  class Option
14
14
  attr_reader :name
15
15
  attr_reader :is_long
@@ -17,6 +17,7 @@ module Consoler
17
17
  attr_reader :is_argument
18
18
  attr_reader :is_value
19
19
  attr_reader :is_optional
20
+ attr_reader :aliases
20
21
 
21
22
  # Create a option
22
23
  #
@@ -27,11 +28,14 @@ module Consoler
27
28
  def self.create(option_def, tracker)
28
29
  option = Option.new option_def, tracker
29
30
 
30
- if option.is_short and option.name.size > 1 then
31
+ # split short options with more than 1 char in multiple options
32
+ if option.is_short && option.name.size > 1
33
+ # remember state
31
34
  old_tracking = tracker.is_tracking
32
35
  old_is_value = option.is_value
33
36
 
34
- if option.is_optional then
37
+ # if the complete option is optional, fake the tracker
38
+ if option.is_optional
35
39
  tracker.is_tracking = true
36
40
  end
37
41
 
@@ -40,13 +44,15 @@ module Consoler
40
44
  names.each_with_index do |name, i|
41
45
  new_name = "-#{name}"
42
46
 
43
- if old_is_value and i == names.count - 1 then
47
+ # if the short option should have a value, this only counts for the last option
48
+ if old_is_value && i == names.count - 1
44
49
  new_name = "#{new_name}="
45
50
  end
46
51
 
47
52
  yield Option.new new_name, tracker
48
53
  end
49
54
 
55
+ # reset to saved state
50
56
  tracker.is_tracking = old_tracking
51
57
  else
52
58
  yield option
@@ -62,14 +68,20 @@ module Consoler
62
68
  def to_definition
63
69
  definition = name
64
70
 
65
- if is_long then
71
+ if is_long
66
72
  definition = "--#{definition}"
67
- elsif is_short then
73
+ elsif is_short
68
74
  definition = "-#{definition}"
69
75
  end
70
76
 
71
- if is_value then
77
+ if is_value
72
78
  definition = "#{definition}="
79
+ elsif is_argument
80
+ definition = "<#{definition}>"
81
+ end
82
+
83
+ aliases.each do |alias_|
84
+ definition = "#{definition}|#{alias_.to_definition}"
73
85
  end
74
86
 
75
87
  definition
@@ -83,7 +95,7 @@ module Consoler
83
95
  return 0 if is_short
84
96
  return false if is_long
85
97
 
86
- return nil
98
+ nil
87
99
  end
88
100
 
89
101
  protected
@@ -92,29 +104,61 @@ module Consoler
92
104
  #
93
105
  # @param [String] option_def Definition of the option
94
106
  # @param [Consoler::Tracker] tracker tracker
107
+ # @raise [RuntimeError] if the option name is empty
108
+ # @raise [RuntimeError] if the option is long _and_ short
95
109
  def initialize(option_def, tracker)
96
- option, @is_optional = _is_optional option_def, tracker
110
+ # Check for multiple attributes in the option definition till we got the
111
+ # final name and all of its attributes
112
+
113
+ # make sure we don't wrongly process any alias
114
+ alias_defs = option_def.split '|'
115
+ option = alias_defs.shift || ''
116
+
117
+ option, @is_optional = _is_optional option, tracker
97
118
  option, @is_long = _is_long option
98
119
  option, @is_short = _is_short option
99
- @is_argument = (not @is_long and not @is_short)
120
+ @is_argument = (!@is_long && !@is_short)
100
121
  option, @is_value = _value option, @is_argument
122
+ option, @aliases = _aliases option, alias_defs, tracker
123
+
124
+ if option[0] == '<'
125
+ raise 'Invalid <, missing >' if option[-1] != '>'
126
+ raise 'Only arguments support <, > around name' unless @is_argument
127
+
128
+ option = option[1..-2]
129
+ end
130
+
131
+ raise 'Missing starting <' if option[-1] == '>'
101
132
 
102
133
  @name = option
103
134
 
104
- if @name.empty? then
135
+ if @name.empty?
105
136
  raise 'Option must have a name'
106
137
  end
107
138
 
108
- if @is_long and @is_short
139
+ if @is_long && @is_short
109
140
  raise 'Option can not be a long and a short option'
110
141
  end
111
142
  end
112
143
 
113
144
  private
114
145
 
146
+ # Check optional definition
147
+ #
148
+ # Does it open an optional group
149
+ # Does it close an optional group (can be both)
150
+ # Updates the tracker
151
+ # Removes leading [ and trailing ]
152
+ #
153
+ # @param [String] option Option definition
154
+ # @param [Consoler::Tracker] tracker Optional tracker
155
+ # @raise [RuntimeError] if you try to nest optional groups
156
+ # @raise [RuntimeError] if you try to close an unopened optional
157
+ # @return [(String, Integer|nil)] Remaining option definition, and, optional group if available
115
158
  def _is_optional(option, tracker)
116
- if option[0] == '[' then
117
- if !tracker.is_tracking then
159
+ if option[0] == '['
160
+ if !tracker.is_tracking
161
+ # mark tracker as tracking
118
162
  tracker.is_tracking = true
119
163
  tracker.index += 1
120
164
  option = option[1..-1]
@@ -123,14 +167,14 @@ module Consoler
123
167
  end
124
168
  end
125
169
 
126
- optional = if tracker.is_tracking then
170
+ # get optional group index from tracking, if tracking
171
+ optional = if tracker.is_tracking
127
172
  tracker.index
128
- else
129
- nil
130
173
  end
131
174
 
132
- if option[-1] == ']' then
133
- if tracker.is_tracking then
175
+ if option[-1] == ']'
176
+ if tracker.is_tracking
177
+ # mark tracker as non-tracking
134
178
  tracker.is_tracking = false
135
179
  option = option[0..-2]
136
180
  else
@@ -138,34 +182,47 @@ module Consoler
138
182
  end
139
183
  end
140
184
 
141
- return option, optional
185
+ [option, optional]
142
186
  end
143
187
 
188
+ # Check long definition
189
+ #
190
+ # @param [String] option Option definition
191
+ # @return [(String, Boolean)]
144
192
  def _is_long(option)
145
- if option[0..1] == '--' then
193
+ if option[0..1] == '--'
146
194
  long = true
147
195
  option = option[2..-1]
148
196
  else
149
197
  long = false
150
198
  end
151
199
 
152
- return option, long
200
+ [option, long]
153
201
  end
154
202
 
203
+ # Check short definition
204
+ #
205
+ # @param [String] option Option definition
206
+ # @return [(String, Boolean)]
155
207
  def _is_short(option)
156
- if option[0] == '-' then
208
+ if option[0] == '-'
157
209
  short = true
158
210
  option = option[1..-1]
159
211
  else
160
212
  short = false
161
213
  end
162
214
 
163
- return option, short
215
+ [option, short]
164
216
  end
165
217
 
218
+ # Check value definition
219
+ #
220
+ # @param [String] option Option definition
221
+ # @raise [RuntimeError] if you try to assign a value to an argument
222
+ # @return [(String, Boolean)]
166
223
  def _value(option, argument)
167
- if option[-1] == '=' then
168
- if argument then
224
+ if option[-1] == '='
225
+ if argument
169
226
  raise 'Arguments can\'t have a value'
170
227
  end
171
228
 
@@ -175,7 +232,36 @@ module Consoler
175
232
  value = false
176
233
  end
177
234
 
178
- return option, value
235
+ [option, value]
236
+ end
237
+
238
+ # Parse all possible aliases
239
+ #
240
+ # @param [String] option Option definition
241
+ # @param [Consoler::Tracker] tracker Optional tracker
242
+ # @raise [RuntimeError] On all kinds of occasions
243
+ # @return [(String, Array)] Remaining option definition, and, aliases if available
244
+ def _aliases(option, alias_defs, tracker)
245
+ return option, [] if alias_defs.empty?
246
+
247
+ raise 'Argument can\'t have aliases' if is_argument
248
+ raise 'Aliases are not allowed for multiple short options' if is_short && option.size > 1
249
+
250
+ aliases_ = []
251
+ alias_names = []
252
+
253
+ while (alias_def = alias_defs.shift)
254
+ Consoler::Option.create alias_def, tracker do |alias_|
255
+ raise "Duplicate alias name: #{alias_.name}" if alias_names.include? alias_.name
256
+ raise "Alias must have a value: #{alias_.name}" if is_value && !alias_.is_value
257
+ raise "Alias can't have a value: #{alias_.name}" if !is_value && alias_.is_value
258
+
259
+ aliases_.push alias_
260
+ alias_names.push alias_.name
261
+ end
262
+ end
263
+
264
+ [option, aliases_]
179
265
  end
180
266
  end
181
267
  end