consoler 1.0.1 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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