Usage 0.0.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.
- data/LICENSE +1 -0
- data/README +137 -0
- data/TODO +6 -0
- data/lib/Usage.rb +858 -0
- data/samples/Sample1.rb +9 -0
- data/samples/Sample2.rb +9 -0
- data/samples/Sample3.rb +14 -0
- data/samples/Sample4.rb +21 -0
- data/samples/Sample5.rb +13 -0
- data/samples/Sample6.rb +11 -0
- data/samples/Sample7.rb +8 -0
- data/tests/TC_Usage.rb +303 -0
- metadata +60 -0
data/lib/Usage.rb
ADDED
@@ -0,0 +1,858 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require_gem "SimpleTrace", "<= 0.1.0"
|
3
|
+
|
4
|
+
#
|
5
|
+
# Use module to hide usage classes
|
6
|
+
#
|
7
|
+
module UsageMod
|
8
|
+
|
9
|
+
# ---------------------- Usage Exception Classes ---------------------------
|
10
|
+
#
|
11
|
+
# Base exception class for all usage exceptions
|
12
|
+
#
|
13
|
+
class Error < StandardError
|
14
|
+
end
|
15
|
+
|
16
|
+
# ----------------------- Runtime Errors -----------------------------------
|
17
|
+
|
18
|
+
#
|
19
|
+
# Too few arguments were given to the program when it was run
|
20
|
+
#
|
21
|
+
class TooFewArgError < Error
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# Extra arguments were given to the program when it was run
|
26
|
+
#
|
27
|
+
class ExtraArgError < Error
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# The option given by the user when the program was run was not allowed
|
32
|
+
#
|
33
|
+
class UnknownOptionError < Error
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# The user has requested the long version of the usage (by --help)
|
38
|
+
#
|
39
|
+
class LongUsageRequested < Error
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Certain required options were not specified when the program was run
|
44
|
+
#
|
45
|
+
class MissingOptionsError < Error
|
46
|
+
def initialize(missing_options)
|
47
|
+
super("missing option(s) '" + missing_options.uniq.map{|opt| opt.name}.join(",") + "'")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# A required argument for an option was omitted when the program was run
|
53
|
+
#
|
54
|
+
class MissingOptionArgumentError < Error
|
55
|
+
def initialize(option)
|
56
|
+
super("option '#{option.name}' is missing its argument")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# The user specified an invalid choice when the program was run
|
62
|
+
#
|
63
|
+
class InvalidChoiceError < Error
|
64
|
+
attr_reader :option, :choice
|
65
|
+
def initialize(option, choice)
|
66
|
+
@option, @choice = option, choice
|
67
|
+
super("invalid choice:'#{choice}', it should be one of these:" + option.choices.join(","))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# The user specified a non integer when an integer argument was expected when the
|
73
|
+
# program was run
|
74
|
+
#
|
75
|
+
class InvalidIntegerError < Error
|
76
|
+
attr_reader :integer_str
|
77
|
+
def initialize(integer_str)
|
78
|
+
@integer_str = integer_str
|
79
|
+
super("invalid integer value: '#{integer_str}'")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# The user specified a invalid date/time format when the program was run
|
85
|
+
#
|
86
|
+
class InvalidTimeError < Error
|
87
|
+
attr_reader :time_str
|
88
|
+
def initialize(time_str)
|
89
|
+
@time_str = time_str
|
90
|
+
super("invalid date/time: '#{time_str}'")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# ------------------------------- Parsing Errors -----------------------------
|
95
|
+
|
96
|
+
#
|
97
|
+
# The infinite argument was not the last in the argument list in the usage string
|
98
|
+
#
|
99
|
+
class InfiniteArgNotLast < Error
|
100
|
+
def initialize
|
101
|
+
super("the infinite argument (the one ending in ...) must be the last argument")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
#
|
106
|
+
# Too many closing parens
|
107
|
+
#
|
108
|
+
class NestingUnderflow < Error
|
109
|
+
def initialize
|
110
|
+
super("unbalanced parenthesis: too many ')' and not enough '('")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
#
|
115
|
+
# Too many open parens
|
116
|
+
#
|
117
|
+
class NestingOverflow < Error
|
118
|
+
def initialize
|
119
|
+
super("unbalanced parenthesis: too many '(' and not enough ')'")
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# Mismatched number of square braces
|
125
|
+
#
|
126
|
+
class MismatchedBracesError < Error
|
127
|
+
def initialize
|
128
|
+
super("mismatched braces")
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
#
|
133
|
+
# doing a choice with an option or typed parameter is an error
|
134
|
+
#
|
135
|
+
class ChoiceNoTypeError < Error
|
136
|
+
def initialize
|
137
|
+
super("doing a choice with an option or typed parameter is not allowed")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
#
|
142
|
+
# choices are are only allowed as option arguments
|
143
|
+
#
|
144
|
+
class ChoicesNotOnOptionError < Error
|
145
|
+
def initialize
|
146
|
+
super("choices are only allowed as option arguments")
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# ---------------------- Usage Classes -------------------------------------
|
151
|
+
|
152
|
+
#
|
153
|
+
# This class represents an option (something that starts with a '-' or '--')
|
154
|
+
# and its characteristics, such as:
|
155
|
+
# * whether it is required or not
|
156
|
+
# * its short and long names
|
157
|
+
# * its type if it has one (default is string)
|
158
|
+
# * the choices for it if it is an option with fixed number of choices
|
159
|
+
# * its description
|
160
|
+
#
|
161
|
+
class Option
|
162
|
+
attr_reader :name
|
163
|
+
attr_accessor :long_name, :arg_name, :arg_type, :is_required
|
164
|
+
attr_accessor :has_argument, :choices, :description
|
165
|
+
def initialize(name)
|
166
|
+
@name = name
|
167
|
+
@long_name = nil
|
168
|
+
@arg_name = nil
|
169
|
+
@arg_type = nil
|
170
|
+
@has_argument = nil
|
171
|
+
@is_required = true
|
172
|
+
@choices = Choices.new([])
|
173
|
+
end
|
174
|
+
|
175
|
+
#
|
176
|
+
# allow it to be used in a hash
|
177
|
+
#
|
178
|
+
def hash
|
179
|
+
@name.hash
|
180
|
+
end
|
181
|
+
|
182
|
+
#
|
183
|
+
# allow it to be compared with other Option objects
|
184
|
+
#
|
185
|
+
def ==(other)
|
186
|
+
@name = other.name
|
187
|
+
end
|
188
|
+
|
189
|
+
#
|
190
|
+
# the option name as a symbol (ie. :dash_x if -x is the option)
|
191
|
+
#
|
192
|
+
def name_as_symbol
|
193
|
+
"dash_#{@name}"
|
194
|
+
end
|
195
|
+
|
196
|
+
#
|
197
|
+
# the long option name as a symbol (ie. --long_name is :long_name)
|
198
|
+
#
|
199
|
+
def long_name_as_symbol
|
200
|
+
@long_name.gsub(/-/, "_")
|
201
|
+
end
|
202
|
+
|
203
|
+
def to_s
|
204
|
+
"OPTION:[#{@name}][#{@long_name}][#{@arg_type}][required=#{@is_required}]"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
#
|
209
|
+
# This class is an array to hold the choices for an option
|
210
|
+
#
|
211
|
+
class Choices < Array
|
212
|
+
def initialize(array)
|
213
|
+
array.each {|a| self.push(a)}
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
#
|
218
|
+
# This class describes arguments (not options) which can include:
|
219
|
+
# * the name of the argument
|
220
|
+
# * its type (default is String)
|
221
|
+
# * whether it is an infinite argument or not
|
222
|
+
# * whether it is optional or not
|
223
|
+
#
|
224
|
+
class Argument
|
225
|
+
SINGLE = 1
|
226
|
+
INFINITE = 2
|
227
|
+
|
228
|
+
attr_reader :name, :arg_type, :infinite, :optional
|
229
|
+
|
230
|
+
def initialize(name, arg_type, infinite, optional=false)
|
231
|
+
@name = name
|
232
|
+
@arg_type = arg_type
|
233
|
+
@infinite = infinite
|
234
|
+
@optional = optional
|
235
|
+
end
|
236
|
+
|
237
|
+
#
|
238
|
+
# allow it to be used in a hash
|
239
|
+
#
|
240
|
+
def hash
|
241
|
+
@name.hash
|
242
|
+
end
|
243
|
+
|
244
|
+
#
|
245
|
+
# allow it to be compared with other Argument objects
|
246
|
+
#
|
247
|
+
def ==(other)
|
248
|
+
@name = other.name
|
249
|
+
end
|
250
|
+
|
251
|
+
def to_s
|
252
|
+
"ARG:[#{@name}][#{@arg_type}][#{@infinite}]"
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
#
|
257
|
+
# This class holds the parsed representation of the usage string. It holds the
|
258
|
+
# options (both required and optional) and the arguments (both required and optional)
|
259
|
+
#
|
260
|
+
class ArgumentList
|
261
|
+
attr_accessor :infinite_argument, :required_arguments, :options, :optional_arguments
|
262
|
+
|
263
|
+
def initialize()
|
264
|
+
@required_arguments = []
|
265
|
+
@optional_arguments = []
|
266
|
+
@options_hash = {}
|
267
|
+
@infinite_argument = nil
|
268
|
+
end
|
269
|
+
|
270
|
+
#
|
271
|
+
# give the last argument in the list
|
272
|
+
#
|
273
|
+
def last_arg
|
274
|
+
@required_arguments[@required_arguments.size - 1]
|
275
|
+
end
|
276
|
+
|
277
|
+
#
|
278
|
+
# is there an infinite argument and is it last
|
279
|
+
#
|
280
|
+
def has_infinite_arg
|
281
|
+
@required_arguments.size > 0 && last_arg.infinite
|
282
|
+
end
|
283
|
+
|
284
|
+
#
|
285
|
+
# get the next argument
|
286
|
+
#
|
287
|
+
def get_next_arg( index )
|
288
|
+
if index < @required_arguments.size then
|
289
|
+
arg = @required_arguments[index]
|
290
|
+
elsif index < (@required_arguments.size + @optional_arguments.size) then
|
291
|
+
arg = @optional_arguments[index-@required_arguments.size]
|
292
|
+
else
|
293
|
+
arg = nil
|
294
|
+
end
|
295
|
+
|
296
|
+
arg
|
297
|
+
end
|
298
|
+
|
299
|
+
#
|
300
|
+
# add and argument to the list
|
301
|
+
#
|
302
|
+
def push_arg(aArgument)
|
303
|
+
if aArgument.optional then
|
304
|
+
$TRACE.debug 5, "push_arg: pushing optional argument #{aArgument.inspect}"
|
305
|
+
@optional_arguments.push(aArgument)
|
306
|
+
else
|
307
|
+
# FIXME: raise "Required arguments cannot follow optional arguments" if @optional_arguments.size > 0
|
308
|
+
$TRACE.debug 5, "push_arg: pushing required argument #{aArgument.inspect}"
|
309
|
+
@required_arguments.push(aArgument)
|
310
|
+
end
|
311
|
+
$TRACE.debug 5, "push_arg: @optional_arguments.size = #{@optional_arguments.size}"
|
312
|
+
$TRACE.debug 5, "push_arg: @optional_arguments = #{@optional_arguments.inspect}"
|
313
|
+
end
|
314
|
+
|
315
|
+
#
|
316
|
+
# add an option to the list
|
317
|
+
#
|
318
|
+
def push_option(aOption)
|
319
|
+
@options_hash[aOption.name] = aOption
|
320
|
+
end
|
321
|
+
|
322
|
+
#
|
323
|
+
# update the options_hash with their long name equivalents
|
324
|
+
#
|
325
|
+
def update_with_long_names
|
326
|
+
$TRACE.debug 9, "before long names: options_hash = #{@options_hash.inspect}"
|
327
|
+
@options_hash.values.each do |option|
|
328
|
+
@options_hash[option.long_name] = option if option.long_name
|
329
|
+
end
|
330
|
+
$TRACE.debug 9, "after long names: options_hash = #{@options_hash.inspect}"
|
331
|
+
end
|
332
|
+
|
333
|
+
#
|
334
|
+
# lookup an option and return Option object based on the name
|
335
|
+
#
|
336
|
+
def lookup_option(name)
|
337
|
+
@options_hash[name]
|
338
|
+
end
|
339
|
+
|
340
|
+
#
|
341
|
+
# returns the options that are required
|
342
|
+
#
|
343
|
+
def required_options
|
344
|
+
ar = @options_hash.values
|
345
|
+
$TRACE.debug 9, "options_hash = #{@options_hash.inspect}"
|
346
|
+
$TRACE.debug 9, "ar = #{ar.inspect}"
|
347
|
+
ar.select{|opt| opt.is_required}
|
348
|
+
end
|
349
|
+
|
350
|
+
#
|
351
|
+
# check to make sure the argument list is correct?
|
352
|
+
#
|
353
|
+
def process
|
354
|
+
@required_arguments.each_with_index do |arg, i|
|
355
|
+
if arg.infinite then
|
356
|
+
if i != required_arguments.size - 1 then
|
357
|
+
raise InfiniteArgNotLast.new("infinite argument #{arg.name} is not last")
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
#
|
365
|
+
# This is the class that does the heavy lifting for the Usage class. It parses the
|
366
|
+
# usage string, options string and makes the information available by using
|
367
|
+
# method_missing.
|
368
|
+
#
|
369
|
+
# see the README document for how to format the usage string and options string
|
370
|
+
#
|
371
|
+
class Base
|
372
|
+
#
|
373
|
+
# create a UsageMod::Base object. usageString specifies the arguments expected. The
|
374
|
+
# optionsString specifies further information about the arguments. userArguments
|
375
|
+
# defaults to the program arguments but allows the user to get the arguments from
|
376
|
+
# somewhere else.
|
377
|
+
#
|
378
|
+
def initialize(usageString, optionsString="", userArguments=ARGV)
|
379
|
+
@argHash = {}
|
380
|
+
@usageString = usageString
|
381
|
+
@optionsString = optionsString
|
382
|
+
@userArguments = userArguments
|
383
|
+
|
384
|
+
# parse the usage string
|
385
|
+
@argList = parse_usage(usageString)
|
386
|
+
|
387
|
+
# parse the description
|
388
|
+
@descriptions = parse_option_descriptions
|
389
|
+
|
390
|
+
# update options hash now that some long names may have been parsed
|
391
|
+
@argList.update_with_long_names
|
392
|
+
end
|
393
|
+
|
394
|
+
|
395
|
+
DESC_PARSE_REGEX = /^-(\w),--(\S+)\s+(.*)$/
|
396
|
+
|
397
|
+
OPTION_NAME = 1
|
398
|
+
OPTION_LONG_NAME = 2
|
399
|
+
OPTION_DESCRIPTION = 3
|
400
|
+
|
401
|
+
#
|
402
|
+
# parse the option descriptions
|
403
|
+
#
|
404
|
+
def parse_option_descriptions
|
405
|
+
@maxOptionNameLen = 0
|
406
|
+
@optionsString.split("\n").each do |line|
|
407
|
+
if m = DESC_PARSE_REGEX.match(line) then
|
408
|
+
$TRACE.debug 5, "[#{m[1]}][#{m[2]}][#{m[3]}]"
|
409
|
+
len = m[OPTION_NAME].size + m[OPTION_LONG_NAME].size + 4
|
410
|
+
@maxOptionNameLen = len if len > @maxOptionNameLen
|
411
|
+
if option = @argList.lookup_option(m[OPTION_NAME]) then
|
412
|
+
option.long_name = m[OPTION_LONG_NAME]
|
413
|
+
option.description = m[OPTION_DESCRIPTION]
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
# @ : date
|
420
|
+
# % : integer
|
421
|
+
# # : float
|
422
|
+
# - : option
|
423
|
+
# ([ @%#- name ... )]
|
424
|
+
PARSE_REGEX = /^([\(\[])?([\@%#-])?([^.\)\]]*)(\.\.\.)?([\)\]])?$/
|
425
|
+
|
426
|
+
OPEN_PAREN_OR_BRACKET = 1
|
427
|
+
DASH_OR_TYPE = 2
|
428
|
+
ARG_OR_OPTION_NAME = 3
|
429
|
+
INFINITE = 4
|
430
|
+
CLOSE_PAREN_OR_BRACKET = 5
|
431
|
+
|
432
|
+
|
433
|
+
#
|
434
|
+
# this parses the usage string and returns an ArgumentList object that
|
435
|
+
# describes the arguments by their characteristics (type, optional/required, etc)
|
436
|
+
#
|
437
|
+
def parse_usage(usageString)
|
438
|
+
arg_list = ArgumentList.new
|
439
|
+
option_stack = []
|
440
|
+
processing_options = true
|
441
|
+
option = nil
|
442
|
+
choices = nil
|
443
|
+
usageString.split(/\s/).each do |arg|
|
444
|
+
$TRACE.debug 5, "arg = '#{arg}'"
|
445
|
+
m = PARSE_REGEX.match(arg)
|
446
|
+
infinite = false
|
447
|
+
optional_arg = false
|
448
|
+
option = nil
|
449
|
+
if m then
|
450
|
+
$TRACE.debug 5, "got match, opening='#{m[OPEN_PAREN_OR_BRACKET]}' " +
|
451
|
+
"dash_or_type='#{m[DASH_OR_TYPE]}' " +
|
452
|
+
"main='#{m[ARG_OR_OPTION_NAME]}' " +
|
453
|
+
"infinite = '#{m[INFINITE]}' " +
|
454
|
+
"closing='#{m[CLOSE_PAREN_OR_BRACKET]}'"
|
455
|
+
$TRACE.debug 9, "option = #{option.inspect}, option_stack = #{option_stack.inspect}"
|
456
|
+
|
457
|
+
choices = m[ARG_OR_OPTION_NAME].split(/\|/)
|
458
|
+
raise ChoicesNotOnOptionError.new if choices.size > 1 && option_stack.size == 0
|
459
|
+
|
460
|
+
if m[DASH_OR_TYPE] then
|
461
|
+
raise ChoiceNoTypeError.new if choices.size > 1
|
462
|
+
|
463
|
+
case m[DASH_OR_TYPE]
|
464
|
+
when "%"
|
465
|
+
arg_type = Integer
|
466
|
+
$TRACE.debug 5, "is integer"
|
467
|
+
when "#"
|
468
|
+
arg_type = Float
|
469
|
+
$TRACE.debug 5, "is float"
|
470
|
+
# when "<"
|
471
|
+
# when ">"
|
472
|
+
# arg_type = Time
|
473
|
+
|
474
|
+
when "@"
|
475
|
+
arg_type = Time
|
476
|
+
$TRACE.debug 5, "is time"
|
477
|
+
when "-"
|
478
|
+
option = Option.new(m[ARG_OR_OPTION_NAME])
|
479
|
+
if m[OPEN_PAREN_OR_BRACKET] then
|
480
|
+
option.is_required = (m[OPEN_PAREN_OR_BRACKET] == "(")
|
481
|
+
option_stack.push(option)
|
482
|
+
else
|
483
|
+
option.arg_type = TrueClass
|
484
|
+
end
|
485
|
+
end
|
486
|
+
else
|
487
|
+
arg_type = String
|
488
|
+
end
|
489
|
+
|
490
|
+
if m[CLOSE_PAREN_OR_BRACKET] then
|
491
|
+
if option_stack.size == 0 then
|
492
|
+
if m[OPEN_PAREN_OR_BRACKET] && m[DASH_OR_TYPE] != '-' then
|
493
|
+
# Assume this is an optional parameter
|
494
|
+
optional_arg = true
|
495
|
+
$TRACE.debug 9, "parse_usage: Optional paramater found: #{m[ARG_OR_OPTION_NAME]}"
|
496
|
+
else
|
497
|
+
raise NestingUnderflow.new
|
498
|
+
end
|
499
|
+
else
|
500
|
+
option = option_stack.pop
|
501
|
+
if option.is_required != (m[CLOSE_PAREN_OR_BRACKET] == ")") then
|
502
|
+
raise MismatchedBracesError.new
|
503
|
+
end
|
504
|
+
# it has an argument if this token didn't also have a open bracket
|
505
|
+
option.has_argument = !m[OPEN_PAREN_OR_BRACKET]
|
506
|
+
if option.has_argument then
|
507
|
+
if choices.size > 1 then
|
508
|
+
option.choices = Choices.new(choices)
|
509
|
+
option.arg_type = Choices
|
510
|
+
else
|
511
|
+
option.arg_name = m[ARG_OR_OPTION_NAME]
|
512
|
+
option.arg_type = arg_type
|
513
|
+
end
|
514
|
+
end
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
if m[INFINITE] != nil then
|
519
|
+
$TRACE.debug 5, "is infinite"
|
520
|
+
infinite = true
|
521
|
+
end
|
522
|
+
|
523
|
+
$TRACE.debug 9, "option = #{option.inspect}, option_stack = #{option_stack.inspect}"
|
524
|
+
unless option || option_stack.size > 0
|
525
|
+
$TRACE.debug 9, "no longer processing options"
|
526
|
+
processing_options = false
|
527
|
+
end
|
528
|
+
else
|
529
|
+
arg_type = String
|
530
|
+
$TRACE.debug 5, "is string"
|
531
|
+
end
|
532
|
+
|
533
|
+
if processing_options then
|
534
|
+
if option_stack.size == 0 then
|
535
|
+
$TRACE.debug 5, "adding option #{option.name}"
|
536
|
+
arg_list.push_option(option)
|
537
|
+
end
|
538
|
+
else
|
539
|
+
arg = m[ARG_OR_OPTION_NAME]
|
540
|
+
if infinite then
|
541
|
+
@argHash[arg] = []
|
542
|
+
else
|
543
|
+
@argHash[arg] = nil
|
544
|
+
end
|
545
|
+
$TRACE.debug 5, "adding argument '#{arg}'"
|
546
|
+
arg_list.push_arg(Argument.new(arg, arg_type, infinite, optional_arg))
|
547
|
+
|
548
|
+
arg_list.process
|
549
|
+
end
|
550
|
+
end
|
551
|
+
|
552
|
+
raise NestingOverflow.new if option_stack.size > 0
|
553
|
+
|
554
|
+
$TRACE.debug 9, "arg_list = #{arg_list.inspect}\n"
|
555
|
+
arg_list
|
556
|
+
end
|
557
|
+
|
558
|
+
#
|
559
|
+
# set up @argHash for all the options based on two things:
|
560
|
+
#
|
561
|
+
# # the parse usage string description in @argList (which is an ArgumentList object)
|
562
|
+
# # the arguments that the user ran the program with which is in @userArguments (which is an array of strings)
|
563
|
+
#
|
564
|
+
# this allows method_missing to return the correct values when it is called
|
565
|
+
#
|
566
|
+
def parse_options()
|
567
|
+
last_index = @userArguments.size
|
568
|
+
found_options = []
|
569
|
+
option_waiting_for_argument = nil
|
570
|
+
$TRACE.debug 5, "parse_options: user arguments = #{@userArguments.inspect}"
|
571
|
+
@userArguments.each_with_index do |userarg, index|
|
572
|
+
$TRACE.debug 7, "arg = '#{userarg}'"
|
573
|
+
# if we found an option
|
574
|
+
if m = /^-{1,2}(.*)$/.match(userarg) then
|
575
|
+
# break out if we are waiting for an argument
|
576
|
+
break if option_waiting_for_argument
|
577
|
+
|
578
|
+
# handle requests for long usage string
|
579
|
+
if m[1] == "help" || m[1] == "?" then
|
580
|
+
raise LongUsageRequested
|
581
|
+
end
|
582
|
+
|
583
|
+
# look up the option
|
584
|
+
if option = @argList.lookup_option(m[1]) then
|
585
|
+
@argHash[option.name_as_symbol] = true
|
586
|
+
@argHash[option.long_name_as_symbol] = true if option.long_name
|
587
|
+
found_options.push(option)
|
588
|
+
if option.has_argument then
|
589
|
+
option_waiting_for_argument = option
|
590
|
+
end
|
591
|
+
else
|
592
|
+
raise UnknownOptionError.new("unknown option '#{userarg}'")
|
593
|
+
end
|
594
|
+
|
595
|
+
# else its either a option or regular argument
|
596
|
+
else
|
597
|
+
# if it is an option argument
|
598
|
+
if option = option_waiting_for_argument then
|
599
|
+
# process the argument
|
600
|
+
value = convert_value(option.arg_type, userarg)
|
601
|
+
if option.arg_type == Choices then
|
602
|
+
if !option.choices.include?(userarg) then
|
603
|
+
raise InvalidChoiceError.new(option, userarg)
|
604
|
+
end
|
605
|
+
@argHash[option.name_as_symbol] = value
|
606
|
+
@argHash[option.long_name_as_symbol] = value if option.long_name
|
607
|
+
else
|
608
|
+
@argHash[option.arg_name] = value
|
609
|
+
end
|
610
|
+
|
611
|
+
option_waiting_for_argument = nil
|
612
|
+
|
613
|
+
# otherwise its a regular argument
|
614
|
+
else
|
615
|
+
# we need to leave this loop
|
616
|
+
last_index = index
|
617
|
+
break
|
618
|
+
end
|
619
|
+
end
|
620
|
+
end
|
621
|
+
|
622
|
+
if option_waiting_for_argument then
|
623
|
+
raise MissingOptionArgumentError.new(option_waiting_for_argument)
|
624
|
+
end
|
625
|
+
|
626
|
+
missing_options = @argList.required_options - found_options
|
627
|
+
if missing_options.size > 0 then
|
628
|
+
raise MissingOptionsError.new(missing_options)
|
629
|
+
end
|
630
|
+
|
631
|
+
$TRACE.debug 5, "parse_options: last_index = #{last_index}"
|
632
|
+
@userArguments = @userArguments[last_index..-1]
|
633
|
+
end
|
634
|
+
|
635
|
+
#
|
636
|
+
# set up @argHash for the options and arguments based on two things (this calls
|
637
|
+
# UsageMod::Base#parse_options to parse the options:
|
638
|
+
#
|
639
|
+
# # the parse usage string description in @argList (which is an ArgumentList object)
|
640
|
+
# # the arguments that the user ran the program with which is in @userArguments (which is an array of strings)
|
641
|
+
#
|
642
|
+
# this allows method_missing to return the correct values when it is called
|
643
|
+
#
|
644
|
+
def parse_args()
|
645
|
+
parse_options
|
646
|
+
$TRACE.debug 5, "parse_args: user arguments = #{@userArguments.inspect}"
|
647
|
+
user_args_size = @userArguments.size
|
648
|
+
req_args_size = @argList.required_arguments.size
|
649
|
+
opt_args_size = @argList.optional_arguments.size
|
650
|
+
$TRACE.debug 5, "user_args_size = #{user_args_size}, req_args_size = #{req_args_size}\n"
|
651
|
+
if user_args_size < req_args_size then
|
652
|
+
$TRACE.debug 5, "parse_args: required_arguments = #{@argList.required_arguments.inspect}"
|
653
|
+
$TRACE.debug 5, "parse_args: optional_arguments = #{@argList.optional_arguments.inspect}"
|
654
|
+
raise TooFewArgError.new("too few arguments #{req_args_size} expected, #{user_args_size} given")
|
655
|
+
elsif user_args_size > (req_args_size + opt_args_size)
|
656
|
+
if !@argList.has_infinite_arg then
|
657
|
+
raise ExtraArgError.new("too many arguments #{req_args_size} expected, #{user_args_size} given")
|
658
|
+
end
|
659
|
+
end
|
660
|
+
|
661
|
+
@userArguments.each_with_index do |userarg, index|
|
662
|
+
arg = @argList.get_next_arg(index)
|
663
|
+
$TRACE.debug 5, "userarg = '#{userarg}', arg = '#{arg}'"
|
664
|
+
|
665
|
+
if @argList.has_infinite_arg && index + 1 >= req_args_size then
|
666
|
+
@argHash[@argList.last_arg.name].push(userarg)
|
667
|
+
else
|
668
|
+
@argHash[arg.name] = convert_value(arg.arg_type, userarg)
|
669
|
+
end
|
670
|
+
end
|
671
|
+
|
672
|
+
self
|
673
|
+
end
|
674
|
+
|
675
|
+
#
|
676
|
+
# parse a date/time string, validate it and return a Time object. If there is
|
677
|
+
# an error, then throw a InvalidTimeError exception.
|
678
|
+
#
|
679
|
+
def parse_date_time(str)
|
680
|
+
if /(\d+)\/(\d+)\/(\d+)-(\d+):(\d+)/.match(str) then
|
681
|
+
month = $1.to_i
|
682
|
+
day = $2.to_i
|
683
|
+
if $3.size < 3 then
|
684
|
+
year = $3.to_i + 2000
|
685
|
+
else
|
686
|
+
year = $3.to_i
|
687
|
+
end
|
688
|
+
hour = $4.to_i
|
689
|
+
minute = $5.to_i
|
690
|
+
return Time.local(year, month, day, hour, minute)
|
691
|
+
else
|
692
|
+
raise InvalidTimeError.new(str)
|
693
|
+
end
|
694
|
+
end
|
695
|
+
|
696
|
+
#
|
697
|
+
# parse and integer argument, validate it, and return a Integer object. If there
|
698
|
+
# is an error, throw a InvalidIntegerError exception.
|
699
|
+
#
|
700
|
+
def parse_integer(str)
|
701
|
+
if /^-?\d+/.match(str) then
|
702
|
+
return str.to_i
|
703
|
+
else
|
704
|
+
raise InvalidIntegerError.new(str)
|
705
|
+
end
|
706
|
+
end
|
707
|
+
|
708
|
+
#
|
709
|
+
# convert a value to its typed equivalent
|
710
|
+
#
|
711
|
+
def convert_value(arg_type, value)
|
712
|
+
$TRACE.debug 5, "convert_value: #{value} to #{arg_type}"
|
713
|
+
case arg_type.to_s
|
714
|
+
when "Integer"
|
715
|
+
return parse_integer(value)
|
716
|
+
when "Float"
|
717
|
+
return value.to_f
|
718
|
+
when "Time"
|
719
|
+
return parse_date_time(value)
|
720
|
+
else
|
721
|
+
return value
|
722
|
+
end
|
723
|
+
end
|
724
|
+
|
725
|
+
#
|
726
|
+
# return the short usage string
|
727
|
+
#
|
728
|
+
def usage
|
729
|
+
@usageString.gsub(/[%#()]/, "") + "\n"
|
730
|
+
end
|
731
|
+
|
732
|
+
#
|
733
|
+
# return the long usage string
|
734
|
+
#
|
735
|
+
def long_usage
|
736
|
+
@optionsString.split("\n").map do |line|
|
737
|
+
if m = DESC_PARSE_REGEX.match(line) then
|
738
|
+
opt_str = "-#{m[OPTION_NAME]},--#{m[OPTION_LONG_NAME]}"
|
739
|
+
opt_str + (" " * (@maxOptionNameLen + 2 - opt_str.size)) + m[OPTION_DESCRIPTION]
|
740
|
+
elsif /^\s+(\S.*)$/.match(line) then
|
741
|
+
(" " * (@maxOptionNameLen + 2)) + $1
|
742
|
+
else
|
743
|
+
line
|
744
|
+
end
|
745
|
+
end.join("\r\n")
|
746
|
+
end
|
747
|
+
|
748
|
+
#
|
749
|
+
# dump the argument hash
|
750
|
+
#
|
751
|
+
def dump
|
752
|
+
@argHash.keys.sort.each do |k|
|
753
|
+
puts "#{k} = #{@argHash[k].inspect}"
|
754
|
+
end
|
755
|
+
end
|
756
|
+
|
757
|
+
#
|
758
|
+
# uses the argHash to return data specified about the command line arguments
|
759
|
+
# by the symbol passed in.
|
760
|
+
#
|
761
|
+
def method_missing(symbol, *args)
|
762
|
+
$TRACE.debug 5, "UsageMod::Base method missing: #{symbol}"
|
763
|
+
if @argHash.has_key?(symbol.to_s) then
|
764
|
+
@argHash[symbol.to_s]
|
765
|
+
end
|
766
|
+
end
|
767
|
+
end
|
768
|
+
|
769
|
+
end # module UsageMod
|
770
|
+
|
771
|
+
#
|
772
|
+
# This is the main interface to this usage functionality. It operates by
|
773
|
+
# using method_missing to extract information about the command line arguments.
|
774
|
+
# Most all the functionality is implemented in the UsageMod::Base class with the Usage
|
775
|
+
# class being a wrapper around it to handle errors in a nice graceful fashion.
|
776
|
+
#
|
777
|
+
# The main way to use it is to give the usage string as the argument to build the
|
778
|
+
# usage object like below:
|
779
|
+
#
|
780
|
+
# usage = Usage.new "input_file output_file"
|
781
|
+
#
|
782
|
+
# see the README for a more complete description of what can appear in that string.
|
783
|
+
#
|
784
|
+
class Usage
|
785
|
+
#
|
786
|
+
# create a new usage object. It is forwarded on to UsageMod::Base#initialize. If an
|
787
|
+
# error occurs in UsageMod::Base, this displays the error to the user and displays the
|
788
|
+
# usage string as well.
|
789
|
+
#
|
790
|
+
def initialize(*args)
|
791
|
+
@usageBase = UsageMod::Base.new(*args)
|
792
|
+
begin
|
793
|
+
@usageBase.parse_args()
|
794
|
+
rescue LongUsageRequested
|
795
|
+
die_long
|
796
|
+
rescue UsageMod::Error => usageError
|
797
|
+
$TRACE.debug 1, usageError.backtrace.join("\n")
|
798
|
+
die(usageError.message)
|
799
|
+
rescue
|
800
|
+
raise
|
801
|
+
end
|
802
|
+
end
|
803
|
+
|
804
|
+
#
|
805
|
+
# Allows the program to die in a graceful manner. It displays
|
806
|
+
#
|
807
|
+
# ERROR: the error message
|
808
|
+
#
|
809
|
+
# It then calls exit (so it doesn't return)
|
810
|
+
#
|
811
|
+
def die(message)
|
812
|
+
print_program_name
|
813
|
+
print "ERROR: #{message}\n"
|
814
|
+
print "\n"
|
815
|
+
print_short_usage
|
816
|
+
exit 1
|
817
|
+
end
|
818
|
+
|
819
|
+
#
|
820
|
+
# Allows the program to die in a graceful manner. It displays
|
821
|
+
#
|
822
|
+
# ERROR: the error message
|
823
|
+
#
|
824
|
+
# followed by the usage string. It then calls exit (so it doesn't return)
|
825
|
+
#
|
826
|
+
def die_long
|
827
|
+
print_program_name
|
828
|
+
print_short_usage
|
829
|
+
print @usageBase.long_usage + "\n"
|
830
|
+
exit 1
|
831
|
+
end
|
832
|
+
|
833
|
+
#
|
834
|
+
# Prints out the program's name in a graceful manner.
|
835
|
+
#
|
836
|
+
# PROGRAM: program name
|
837
|
+
#
|
838
|
+
def print_program_name
|
839
|
+
print "PROGRAM: #{$0}\n"
|
840
|
+
end
|
841
|
+
|
842
|
+
#
|
843
|
+
# Print out the short usage string (without the options)
|
844
|
+
#
|
845
|
+
def print_short_usage
|
846
|
+
print "USAGE: #{File.basename($0)} #{@usageBase.usage}\n"
|
847
|
+
end
|
848
|
+
|
849
|
+
#
|
850
|
+
# forward all other calls to UsageMod::Base class
|
851
|
+
#
|
852
|
+
def method_missing(symbol, *args)
|
853
|
+
$TRACE.debug 5, "Usage method missing: #{symbol}"
|
854
|
+
@usageBase.send(symbol, *args)
|
855
|
+
end
|
856
|
+
end
|
857
|
+
|
858
|
+
# ================= if running this file ==============================================================
|