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.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/ruby-getoptions.rb +422 -0
  3. 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: []