ruby-getoptions 0.1.0

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