ruby-getoptions 0.1.0
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.
- checksums.yaml +7 -0
- data/lib/ruby-getoptions.rb +422 -0
- metadata +44 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9df3d1191065b31d1c57c195ccb229713aac8504
|
4
|
+
data.tar.gz: e8ab09210178a8e7ac149bdf2ab295365fc6afbe
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c28f7037039976785bcc6f02cb0e14eb655b0698d5aa7c17b754bc09d36248191ce97b496291ee9eb1affd80e9e3541ee455c4e57ca502880ebe6a1d6d4c0023
|
7
|
+
data.tar.gz: a61c1bb65ffcbc6632fcf2081e220e771b2ea65b17a193ed3d5a212bd219286fa2118bc800eced29558619e1d767eedf34a3fe60ab6a260cca65b11cdf086f35
|
@@ -0,0 +1,422 @@
|
|
1
|
+
# The MIT License (MIT)
|
2
|
+
#
|
3
|
+
# Copyright (c) 2014 David Gamba
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
13
|
+
# copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
# SOFTWARE.
|
22
|
+
|
23
|
+
class GetOptions
|
24
|
+
# argument_specification:
|
25
|
+
# [ '',
|
26
|
+
# '!',
|
27
|
+
# '+',
|
28
|
+
# '= type [destype] [repeat]',
|
29
|
+
# ': number [destype]',
|
30
|
+
# ': + [destype]'
|
31
|
+
# ': type [destype]',
|
32
|
+
# ]
|
33
|
+
# type: [ 's', 'i', 'o', 'f']
|
34
|
+
# destype: ['@', '%']
|
35
|
+
# repeat: { [ min ] [ , [ max ] ] }
|
36
|
+
|
37
|
+
# External method, this is the main interface
|
38
|
+
def self.parse(args, option_map = {}, options = {})
|
39
|
+
@options = options
|
40
|
+
set_initial_values()
|
41
|
+
set_logging()
|
42
|
+
info "input args: '#{args}'"
|
43
|
+
info "input option_map: '#{option_map}'"
|
44
|
+
info "input options: '#{options}'"
|
45
|
+
@option_map = generate_extended_option_map(option_map)
|
46
|
+
option_result, remaining_args = process_arguments(args, {}, [])
|
47
|
+
debug "option_result: '#{option_result}', remaining_args: '#{remaining_args}'"
|
48
|
+
@log = nil
|
49
|
+
[option_result, remaining_args]
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def self.set_initial_values()
|
54
|
+
# Regex definitions
|
55
|
+
@end_processing_regex = /^--$/
|
56
|
+
@type_regex = /[siof]/
|
57
|
+
@desttype_regex = /[@%]/
|
58
|
+
@repeat_regex = /\{\d+(?:,\s?\d+)?\}/
|
59
|
+
@valid_simbols = '=:+!'
|
60
|
+
@is_option_regex = /^--?[^\d]/
|
61
|
+
|
62
|
+
# Instance variables
|
63
|
+
@option_map = {}
|
64
|
+
@level = 2
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.info(msg)
|
68
|
+
STDERR.puts "INFO |" + msg if @level <= 1
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.debug(msg)
|
72
|
+
STDERR.puts "DEBUG |" + msg if @level <= 0
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.set_logging()
|
76
|
+
case @options[:debug]
|
77
|
+
when true
|
78
|
+
@level = 0
|
79
|
+
when 'debug'
|
80
|
+
@level = 0
|
81
|
+
when 'info'
|
82
|
+
@level = 1
|
83
|
+
else
|
84
|
+
@level = 2
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.generate_extended_option_map(option_map)
|
89
|
+
opt_map = {}
|
90
|
+
unique_options = []
|
91
|
+
option_map.each_pair do |k, v|
|
92
|
+
if k.match(/^[=:+!]/)
|
93
|
+
fail ArgumentError,
|
94
|
+
"GetOptions option_map missing name in definition: '#{k}'"
|
95
|
+
end
|
96
|
+
definitions = k.match(/^([^#{@valid_simbols}]+)[#{@valid_simbols}]?(.*?)$/)[1].split('|')
|
97
|
+
unique_options.push(*definitions)
|
98
|
+
arg_spec, *arg_opts = process_type(k.match(/^[^#{@valid_simbols}]+([#{@valid_simbols}]?(.*?))$/)[1])
|
99
|
+
opt_map[definitions] = { arg_spec: arg_spec, arg_opts: arg_opts, opt_dest: v }
|
100
|
+
end
|
101
|
+
unless unique_options.uniq.length == unique_options.length
|
102
|
+
duplicate_elements = unique_options.find { |e| unique_options.count(e) > 1 }
|
103
|
+
fail ArgumentError,
|
104
|
+
"GetOptions option_map needs to have unique options: '#{duplicate_elements}'"
|
105
|
+
end
|
106
|
+
debug "opt_map: #{opt_map}"
|
107
|
+
opt_map
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.process_type(type_str)
|
111
|
+
# argument_specification:
|
112
|
+
# [ '',
|
113
|
+
# '!',
|
114
|
+
# '+',
|
115
|
+
# '= type [destype] [repeat]',
|
116
|
+
# ': number [destype]',
|
117
|
+
# ': + [destype]'
|
118
|
+
# ': type [destype]',
|
119
|
+
# ]
|
120
|
+
# type: [ 's', 'i', 'o', 'f']
|
121
|
+
# destype: ['@', '%']
|
122
|
+
# repeat: { [ min ] [ , [ max ] ] }
|
123
|
+
|
124
|
+
# flag: ''
|
125
|
+
if type_str.match(/^$/)
|
126
|
+
['flag']
|
127
|
+
# negatable flag: '!'
|
128
|
+
elsif type_str.match(/^!$/)
|
129
|
+
['nflag']
|
130
|
+
# incremental int: '+'
|
131
|
+
elsif type_str.match(/^\+$/)
|
132
|
+
['increment']
|
133
|
+
# required: '= type [destype] [repeat]'
|
134
|
+
elsif (matches = type_str.match(/^=(#{@type_regex})(#{@desttype_regex}?)(#{@repeat_regex}?)$/))
|
135
|
+
['required', matches[1], matches[2], matches[3]]
|
136
|
+
# optional with default: ': number [destype]'
|
137
|
+
elsif (matches = type_str.match(/^:(\d+)(#{@desttype_regex}?)$/))
|
138
|
+
['optional_with_default', matches[1], matches[2]]
|
139
|
+
# optional with increment: ': + [destype]'
|
140
|
+
elsif (matches = type_str.match(/^:(\+)(#{@desttype_regex}?)$/))
|
141
|
+
['optional_with_increment', matches[1], matches[2]]
|
142
|
+
# optional: ': type [destype]'
|
143
|
+
elsif (matches = type_str.match(/^:(#{@type_regex})(#{@desttype_regex}?)$/))
|
144
|
+
['optional', matches[1], matches[2]]
|
145
|
+
else
|
146
|
+
fail ArgumentError, "Unknown option type: '#{type_str}'!"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def self.process_arguments(args, option_result, remaining_args)
|
151
|
+
if args.size > 0
|
152
|
+
arg = args.shift
|
153
|
+
if arg.match(@end_processing_regex)
|
154
|
+
remaining_args.push(*args)
|
155
|
+
return option_result, remaining_args
|
156
|
+
elsif option? arg
|
157
|
+
option_result, args, remaining_args = process_option(arg, option_result, args, remaining_args)
|
158
|
+
option_result, remaining_args = process_arguments(args, option_result, remaining_args)
|
159
|
+
else
|
160
|
+
remaining_args.push arg
|
161
|
+
option_result, remaining_args = process_arguments(args, option_result, remaining_args)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
return option_result, remaining_args
|
165
|
+
end
|
166
|
+
|
167
|
+
def self.process_option(orig_opt, option_result, args, remaining_args)
|
168
|
+
opt = orig_opt.gsub(/^-+/, '')
|
169
|
+
# Check if option has a value defined with an equal sign
|
170
|
+
if (matches = opt.match(/^([^=]+)=(.*)$/))
|
171
|
+
opt = matches[1]
|
172
|
+
arg = matches[2]
|
173
|
+
end
|
174
|
+
# Make it obvious that find_option_matches is updating the instance variable
|
175
|
+
opt_match, @option_map = find_option_matches(opt)
|
176
|
+
if opt_match.nil?
|
177
|
+
remaining_args.push orig_opt
|
178
|
+
return option_result, args, remaining_args
|
179
|
+
end
|
180
|
+
args.unshift arg unless arg.nil?
|
181
|
+
debug "new args: #{args}"
|
182
|
+
option_result, args = execute_option(opt_match, option_result, args)
|
183
|
+
debug "option_result: #{option_result}"
|
184
|
+
return option_result, args, remaining_args
|
185
|
+
end
|
186
|
+
|
187
|
+
def self.find_option_matches(opt)
|
188
|
+
matches = []
|
189
|
+
@option_map.each_pair do |k, v|
|
190
|
+
local_matches = []
|
191
|
+
k.map { |name| local_matches.push name if name.match(/^#{opt}$/) }
|
192
|
+
if v[:arg_spec] == 'nflag'
|
193
|
+
k.map do |name|
|
194
|
+
if opt.match(/^no-?/) && name.match(/^#{opt.gsub(/no-?/, '')}$/)
|
195
|
+
# Update the instance variable
|
196
|
+
@option_map[k][:negated] = true
|
197
|
+
local_matches.push name
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
matches.push(k) if local_matches.size > 0
|
202
|
+
end
|
203
|
+
# FIXME: Too much repetition.
|
204
|
+
# If the strict match returns no results, lets be more permisive.
|
205
|
+
if matches.size == 0
|
206
|
+
@option_map.each_pair do |k, v|
|
207
|
+
local_matches = []
|
208
|
+
k.map { |name| local_matches.push name if name.match(/^#{opt}/) }
|
209
|
+
if v[:arg_spec] == 'nflag'
|
210
|
+
k.map do |name|
|
211
|
+
if opt.match(/^no-?/) && name.match(/^#{opt.gsub(/^no-?/, '')}/)
|
212
|
+
# Update the instance variable
|
213
|
+
@option_map[k][:negated] = true
|
214
|
+
local_matches.push name
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
matches.push(k) if local_matches.size > 0
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
if matches.size == 0
|
223
|
+
if @options[:fail_on_unknown]
|
224
|
+
abort "[ERROR] Option '#{opt}' not found!"
|
225
|
+
else
|
226
|
+
debug "Option '#{opt}' not found!"
|
227
|
+
$stderr.puts "[WARNING] Option '#{opt}' not found!" unless @options[:pass_through]
|
228
|
+
return nil
|
229
|
+
end
|
230
|
+
elsif matches.size > 1
|
231
|
+
abort "[ERROR] option '#{opt}' matches multiple names '#{matches}'!"
|
232
|
+
end
|
233
|
+
debug "matches: #{matches}"
|
234
|
+
[matches[0], @option_map]
|
235
|
+
end
|
236
|
+
|
237
|
+
def self.execute_option(opt_match, option_result, args)
|
238
|
+
opt_def = @option_map[opt_match]
|
239
|
+
debug "#{opt_def[:arg_spec]}"
|
240
|
+
case opt_def[:arg_spec]
|
241
|
+
when 'flag'
|
242
|
+
if opt_def[:opt_dest].kind_of? Symbol
|
243
|
+
option_result[opt_def[:opt_dest]] = true
|
244
|
+
else
|
245
|
+
debug "Flag definition is a function"
|
246
|
+
opt_def[:opt_dest].call
|
247
|
+
end
|
248
|
+
when 'nflag'
|
249
|
+
if opt_def[:negated]
|
250
|
+
option_result[opt_def[:opt_dest]] = false
|
251
|
+
else
|
252
|
+
option_result[opt_def[:opt_dest]] = true
|
253
|
+
end
|
254
|
+
when 'increment'
|
255
|
+
# TODO
|
256
|
+
abort "[ERROR] Unimplemented option definition 'increment'"
|
257
|
+
when 'required'
|
258
|
+
option_result, args = process_desttype(option_result, args, opt_match, false)
|
259
|
+
when 'optional_with_default'
|
260
|
+
# TODO
|
261
|
+
abort "[ERROR] Unimplemented option definition 'optional_with_default'"
|
262
|
+
when 'optional_with_increment'
|
263
|
+
# TODO
|
264
|
+
abort "[ERROR] Unimplemented option definition 'optional_with_increment'"
|
265
|
+
when 'optional'
|
266
|
+
option_result, args = process_desttype(option_result, args, opt_match, true)
|
267
|
+
end
|
268
|
+
[option_result, args]
|
269
|
+
end
|
270
|
+
|
271
|
+
def self.process_option_type(arg, opt_match, optional = false)
|
272
|
+
case @option_map[opt_match][:arg_opts][0]
|
273
|
+
when 's'
|
274
|
+
arg = '' if optional && arg.nil?
|
275
|
+
when 'i'
|
276
|
+
arg = 0 if optional && arg.nil?
|
277
|
+
unless integer?(arg)
|
278
|
+
abort "[ERROR] argument for option '#{opt_match}' is not of type 'Integer'!"
|
279
|
+
end
|
280
|
+
arg = arg.to_i
|
281
|
+
when 'f'
|
282
|
+
arg = 0 if optional && arg.nil?
|
283
|
+
unless numeric?(arg)
|
284
|
+
abort "[ERROR] argument for option '#{opt_match}' is not of type 'Float'!"
|
285
|
+
end
|
286
|
+
arg = arg.to_f
|
287
|
+
when 'o'
|
288
|
+
# FIXME
|
289
|
+
abort "[ERROR] Unimplemented type 'o'!"
|
290
|
+
end
|
291
|
+
return arg
|
292
|
+
end
|
293
|
+
|
294
|
+
def self.process_desttype(option_result, args, opt_match, optional = false)
|
295
|
+
opt_def = @option_map[opt_match]
|
296
|
+
case opt_def[:arg_opts][1]
|
297
|
+
when '@'
|
298
|
+
unless option_result[opt_def[:opt_dest]].kind_of? Array
|
299
|
+
option_result[opt_def[:opt_dest]] = []
|
300
|
+
end
|
301
|
+
# check for repeat specifier {min, max}
|
302
|
+
if (matches = opt_def[:arg_opts][2].match(/\{(\d+)(?:,\s?(\d+))?\}/))
|
303
|
+
min = matches[1].to_i
|
304
|
+
max = matches[2]
|
305
|
+
max = min if max.nil?
|
306
|
+
max = max.to_i
|
307
|
+
if min > max
|
308
|
+
fail ArgumentError, "GetOptions repeat, max '#{max}' <= min '#{min}'"
|
309
|
+
end
|
310
|
+
while min > 0
|
311
|
+
debug "min: #{min}, max: #{max}"
|
312
|
+
min -= 1
|
313
|
+
max -= 1
|
314
|
+
abort "[ERROR] missing argument for option '#{opt_match}'!" if args.size <= 0
|
315
|
+
args, arg = process_desttype_arg(args, opt_match, optional)
|
316
|
+
option_result[opt_def[:opt_dest]].push arg
|
317
|
+
end
|
318
|
+
while max > 0
|
319
|
+
debug "min: #{min}, max: #{max}"
|
320
|
+
max -= 1
|
321
|
+
break if args.size <= 0
|
322
|
+
args, arg = process_desttype_arg(args, opt_match, optional, true)
|
323
|
+
break if arg.nil?
|
324
|
+
option_result[opt_def[:opt_dest]].push arg
|
325
|
+
end
|
326
|
+
else
|
327
|
+
args, arg = process_desttype_arg(args, opt_match, optional)
|
328
|
+
option_result[opt_def[:opt_dest]].push arg
|
329
|
+
end
|
330
|
+
when '%'
|
331
|
+
unless option_result[opt_def[:opt_dest]].kind_of? Hash
|
332
|
+
option_result[opt_def[:opt_dest]] = {}
|
333
|
+
end
|
334
|
+
# check for repeat specifier {min, max}
|
335
|
+
if (matches = opt_def[:arg_opts][2].match(/\{(\d+)(?:,\s?(\d+))?\}/))
|
336
|
+
min = matches[1].to_i
|
337
|
+
max = matches[2]
|
338
|
+
max = min if max.nil?
|
339
|
+
max = max.to_i
|
340
|
+
if min > max
|
341
|
+
fail ArgumentError, "GetOptions repeat, max '#{max}' <= min '#{min}'"
|
342
|
+
end
|
343
|
+
while min > 0
|
344
|
+
debug "min: #{min}, max: #{max}"
|
345
|
+
min -= 1
|
346
|
+
max -= 1
|
347
|
+
abort "[ERROR] missing argument for option '#{opt_match}'!" if args.size <= 0
|
348
|
+
args, arg, key = process_desttype_hash_arg(args, opt_match, optional)
|
349
|
+
option_result[opt_def[:opt_dest]][key] = arg
|
350
|
+
end
|
351
|
+
while max > 0
|
352
|
+
debug "min: #{min}, max: #{max}"
|
353
|
+
max -= 1
|
354
|
+
break if args.size <= 0
|
355
|
+
break if option?(args[0])
|
356
|
+
args, arg, key = process_desttype_hash_arg(args, opt_match, optional)
|
357
|
+
option_result[opt_def[:opt_dest]][key] = arg
|
358
|
+
end
|
359
|
+
else
|
360
|
+
args, arg, key = process_desttype_hash_arg(args, opt_match, optional)
|
361
|
+
option_result[opt_def[:opt_dest]][key] = arg
|
362
|
+
end
|
363
|
+
else
|
364
|
+
args, arg = process_desttype_arg(args, opt_match, optional)
|
365
|
+
option_result[opt_def[:opt_dest]] = arg
|
366
|
+
end
|
367
|
+
[option_result, args]
|
368
|
+
end
|
369
|
+
|
370
|
+
def self.process_desttype_arg(args, opt_match, optional, required = false)
|
371
|
+
if !args[0].nil? && option?(args[0])
|
372
|
+
debug "args[0] option"
|
373
|
+
if required
|
374
|
+
return args, nil
|
375
|
+
end
|
376
|
+
arg = process_option_type(nil, opt_match, optional)
|
377
|
+
else
|
378
|
+
arg = process_option_type(args.shift, opt_match, optional)
|
379
|
+
end
|
380
|
+
debug "arg: '#{arg}'"
|
381
|
+
if arg.nil?
|
382
|
+
debug "arg is nil"
|
383
|
+
abort "[ERROR] missing argument for option '#{opt_match}'!"
|
384
|
+
end
|
385
|
+
[args, arg]
|
386
|
+
end
|
387
|
+
|
388
|
+
def self.process_desttype_hash_arg(args, opt_match, optional)
|
389
|
+
if args[0].nil? || (!args[0].nil? && option?(args[0]))
|
390
|
+
abort "[ERROR] missing argument for option '#{opt_match}'!"
|
391
|
+
end
|
392
|
+
input = args.shift
|
393
|
+
if (matches = input.match(/^([^=]+)=(.*)$/))
|
394
|
+
key = matches[1]
|
395
|
+
arg = matches[2]
|
396
|
+
else
|
397
|
+
abort "[ERROR] argument for option '#{opt_match}' must be of type key=value!"
|
398
|
+
end
|
399
|
+
debug "key: '#{key}', arg: '#{arg}'"
|
400
|
+
arg = process_option_type(arg, opt_match, optional)
|
401
|
+
debug "arg: '#{arg}'"
|
402
|
+
if arg.nil?
|
403
|
+
debug "arg is nil"
|
404
|
+
abort "[ERROR] missing argument for option '#{opt_match}'!"
|
405
|
+
end
|
406
|
+
[args, arg, key]
|
407
|
+
end
|
408
|
+
|
409
|
+
def self.integer?(obj)
|
410
|
+
obj.to_s.match(/\A[+-]?\d+?\Z/) == nil ? false : true
|
411
|
+
end
|
412
|
+
|
413
|
+
def self.numeric?(obj)
|
414
|
+
obj.to_s.match(/\A[+-]?\d+?(\.\d+)?\Z/) == nil ? false : true
|
415
|
+
end
|
416
|
+
|
417
|
+
def self.option?(arg)
|
418
|
+
result = !!(arg.match(@is_option_regex))
|
419
|
+
debug "Is option? '#{arg}' #{result}"
|
420
|
+
result
|
421
|
+
end
|
422
|
+
end
|
metadata
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruby-getoptions
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Gamba
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-09-01 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: The best looking option parser out there
|
14
|
+
email: davidgamba@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/ruby-getoptions.rb
|
20
|
+
homepage: https://github.com/DavidGamba/ruby-getoptions
|
21
|
+
licenses:
|
22
|
+
- MIT
|
23
|
+
metadata: {}
|
24
|
+
post_install_message:
|
25
|
+
rdoc_options: []
|
26
|
+
require_paths:
|
27
|
+
- lib
|
28
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
requirements: []
|
39
|
+
rubyforge_project:
|
40
|
+
rubygems_version: 2.2.2
|
41
|
+
signing_key:
|
42
|
+
specification_version: 4
|
43
|
+
summary: Ruby option parser based on Perl’s Getopt::Long
|
44
|
+
test_files: []
|