usage 0.0.3
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/CHANGES +75 -0
- data/LICENSE +1 -0
- data/README +219 -0
- data/TODO +2 -0
- data/lib/Usage.rb +1009 -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/samples/Sample8.rb +8 -0
- data/samples/sample10.rb +9 -0
- data/samples/sample11.rb +9 -0
- data/samples/sample9.rb +9 -0
- data/tests/TC_Usage.rb +521 -0
- metadata +65 -0
data/lib/Usage.rb
ADDED
@@ -0,0 +1,1009 @@
|
|
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
|
+
|
95
|
+
class InvalidInputFilenameError < Error
|
96
|
+
attr_reader :filename
|
97
|
+
def initialize(filename)
|
98
|
+
@filename = filename
|
99
|
+
super("invalid input file: '#{filename}'")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class FileOutputExistsError < Error
|
104
|
+
attr_reader :filename
|
105
|
+
def initialize(filename)
|
106
|
+
@filename = filename
|
107
|
+
super("output file exists: '#{filename}'")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class FileAppendDoesntExistError < Error
|
112
|
+
attr_reader :filename
|
113
|
+
def initialize(filename)
|
114
|
+
@filename = filename
|
115
|
+
super("trying to append to a file that doesn't exist: '#{filename}'")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# ------------------------------- Parsing Errors -----------------------------
|
120
|
+
|
121
|
+
#
|
122
|
+
# The infinite argument was not the last in the argument list in the usage string
|
123
|
+
#
|
124
|
+
class InfiniteArgNotLast < Error
|
125
|
+
def initialize
|
126
|
+
super("the infinite argument (the one ending in ...) must be the last argument")
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
#
|
131
|
+
# Too many closing parens
|
132
|
+
#
|
133
|
+
class NestingUnderflow < Error
|
134
|
+
def initialize
|
135
|
+
super("unbalanced parenthesis: too many ')' and not enough '('")
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
#
|
140
|
+
# Too many open parens
|
141
|
+
#
|
142
|
+
class NestingOverflow < Error
|
143
|
+
def initialize
|
144
|
+
super("unbalanced parenthesis: too many '(' and not enough ')'")
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
#
|
149
|
+
# Mismatched number of square braces
|
150
|
+
#
|
151
|
+
class MismatchedBracesError < Error
|
152
|
+
def initialize
|
153
|
+
super("mismatched braces")
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
#
|
158
|
+
# doing a choice with an option or typed parameter is an error
|
159
|
+
#
|
160
|
+
class ChoiceNoTypeError < Error
|
161
|
+
def initialize
|
162
|
+
super("doing a choice with an option or typed parameter is not allowed")
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
#
|
167
|
+
# choices are are only allowed as option arguments
|
168
|
+
#
|
169
|
+
class ChoicesNotOnOptionError < Error
|
170
|
+
def initialize
|
171
|
+
super("choices are only allowed as option arguments")
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# ---------------------- Usage Classes -------------------------------------
|
176
|
+
|
177
|
+
#
|
178
|
+
# This class represents an option (something that starts with a '-' or '--')
|
179
|
+
# and its characteristics, such as:
|
180
|
+
# * whether it is required or not
|
181
|
+
# * its short and long names
|
182
|
+
# * its type if it has one (default is string)
|
183
|
+
# * the choices for it if it is an option with fixed number of choices
|
184
|
+
# * its description
|
185
|
+
#
|
186
|
+
class Option
|
187
|
+
attr_reader :name
|
188
|
+
attr_accessor :long_name, :arg_name, :arg_type, :is_required
|
189
|
+
attr_accessor :has_argument, :choices, :description
|
190
|
+
def initialize(name)
|
191
|
+
@name = name
|
192
|
+
@long_name = nil
|
193
|
+
@arg_name = nil
|
194
|
+
@arg_type = nil
|
195
|
+
@has_argument = nil
|
196
|
+
@is_required = true
|
197
|
+
@choices = Choices.new([])
|
198
|
+
end
|
199
|
+
|
200
|
+
#
|
201
|
+
# allow it to be used in a hash
|
202
|
+
#
|
203
|
+
def hash
|
204
|
+
@name.hash
|
205
|
+
end
|
206
|
+
|
207
|
+
#
|
208
|
+
# allow it to be compared with other Option objects
|
209
|
+
#
|
210
|
+
def ==(other)
|
211
|
+
@name = other.name
|
212
|
+
end
|
213
|
+
|
214
|
+
#
|
215
|
+
# the option name as a symbol (ie. :dash_x if -x is the option)
|
216
|
+
#
|
217
|
+
def name_as_symbol
|
218
|
+
"dash_#{@name}"
|
219
|
+
end
|
220
|
+
|
221
|
+
#
|
222
|
+
# the long option name as a symbol (ie. --long_name is :long_name)
|
223
|
+
#
|
224
|
+
def long_name_as_symbol
|
225
|
+
@long_name.gsub(/-/, "_")
|
226
|
+
end
|
227
|
+
|
228
|
+
def to_s
|
229
|
+
"OPTION:[#{@name}][#{@long_name}][#{@arg_type}][required=#{@is_required}]"
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
#
|
234
|
+
# This class is an array to hold the choices for an option
|
235
|
+
#
|
236
|
+
class Choices < Array
|
237
|
+
def initialize(array)
|
238
|
+
array.each {|a| self.push(a)}
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
#
|
243
|
+
# This class describes arguments (not options) which can include:
|
244
|
+
# * the name of the argument
|
245
|
+
# * its type (default is String)
|
246
|
+
# * whether it is an infinite argument or not
|
247
|
+
# * whether it is optional or not
|
248
|
+
#
|
249
|
+
class Argument
|
250
|
+
SINGLE = 1
|
251
|
+
INFINITE = 2
|
252
|
+
|
253
|
+
attr_reader :name, :arg_type, :infinite, :optional
|
254
|
+
|
255
|
+
def initialize(name, arg_type, infinite, optional=false)
|
256
|
+
@name = name
|
257
|
+
@arg_type = arg_type
|
258
|
+
@infinite = infinite
|
259
|
+
@optional = optional
|
260
|
+
end
|
261
|
+
|
262
|
+
#
|
263
|
+
# allow it to be used in a hash
|
264
|
+
#
|
265
|
+
def hash
|
266
|
+
@name.hash
|
267
|
+
end
|
268
|
+
|
269
|
+
#
|
270
|
+
# allow it to be compared with other Argument objects
|
271
|
+
#
|
272
|
+
def ==(other)
|
273
|
+
@name = other.name
|
274
|
+
end
|
275
|
+
|
276
|
+
def to_s
|
277
|
+
"ARG:[#{@name}][#{@arg_type}][#{@infinite}]"
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
#
|
282
|
+
# This class holds the parsed representation of the usage string. It holds the
|
283
|
+
# options (both required and optional) and the arguments (both required and optional)
|
284
|
+
#
|
285
|
+
class ArgumentList
|
286
|
+
attr_accessor :infinite_argument, :required_arguments, :options, :optional_arguments
|
287
|
+
|
288
|
+
def initialize()
|
289
|
+
@required_arguments = []
|
290
|
+
@optional_arguments = []
|
291
|
+
@options_hash = {}
|
292
|
+
@infinite_argument = nil
|
293
|
+
end
|
294
|
+
|
295
|
+
#
|
296
|
+
# give the last argument in the list
|
297
|
+
#
|
298
|
+
def last_arg
|
299
|
+
@required_arguments[@required_arguments.size - 1]
|
300
|
+
end
|
301
|
+
|
302
|
+
#
|
303
|
+
# is there an infinite argument and is it last
|
304
|
+
#
|
305
|
+
def has_infinite_arg
|
306
|
+
@required_arguments.size > 0 && last_arg.infinite
|
307
|
+
end
|
308
|
+
|
309
|
+
#
|
310
|
+
# get the next argument
|
311
|
+
#
|
312
|
+
def get_next_arg( index )
|
313
|
+
if index < @required_arguments.size then
|
314
|
+
arg = @required_arguments[index]
|
315
|
+
elsif index < (@required_arguments.size + @optional_arguments.size) then
|
316
|
+
arg = @optional_arguments[index-@required_arguments.size]
|
317
|
+
else
|
318
|
+
arg = nil
|
319
|
+
end
|
320
|
+
|
321
|
+
arg
|
322
|
+
end
|
323
|
+
|
324
|
+
#
|
325
|
+
# add and argument to the list
|
326
|
+
#
|
327
|
+
def push_arg(aArgument)
|
328
|
+
if aArgument.optional then
|
329
|
+
$TRACE.debug 5, "push_arg: pushing optional argument #{aArgument.inspect}"
|
330
|
+
@optional_arguments.push(aArgument)
|
331
|
+
else
|
332
|
+
# FIXME: raise "Required arguments cannot follow optional arguments" if @optional_arguments.size > 0
|
333
|
+
$TRACE.debug 5, "push_arg: pushing required argument #{aArgument.inspect}"
|
334
|
+
@required_arguments.push(aArgument)
|
335
|
+
end
|
336
|
+
$TRACE.debug 5, "push_arg: @optional_arguments.size = #{@optional_arguments.size}"
|
337
|
+
$TRACE.debug 5, "push_arg: @optional_arguments = #{@optional_arguments.inspect}"
|
338
|
+
end
|
339
|
+
|
340
|
+
#
|
341
|
+
# add an option to the list
|
342
|
+
#
|
343
|
+
def push_option(aOption)
|
344
|
+
@options_hash[aOption.name] = aOption
|
345
|
+
end
|
346
|
+
|
347
|
+
#
|
348
|
+
# update the options_hash with their long name equivalents
|
349
|
+
#
|
350
|
+
def update_with_long_names
|
351
|
+
$TRACE.debug 9, "before long names: options_hash = #{@options_hash.inspect}"
|
352
|
+
@options_hash.values.each do |option|
|
353
|
+
@options_hash[option.long_name] = option if option.long_name
|
354
|
+
end
|
355
|
+
$TRACE.debug 9, "after long names: options_hash = #{@options_hash.inspect}"
|
356
|
+
end
|
357
|
+
|
358
|
+
#
|
359
|
+
# lookup an option and return Option object based on the name
|
360
|
+
#
|
361
|
+
def lookup_option(name)
|
362
|
+
@options_hash[name]
|
363
|
+
end
|
364
|
+
|
365
|
+
#
|
366
|
+
# returns the options that are required
|
367
|
+
#
|
368
|
+
def required_options
|
369
|
+
ar = @options_hash.values
|
370
|
+
$TRACE.debug 9, "options_hash = #{@options_hash.inspect}"
|
371
|
+
$TRACE.debug 9, "ar = #{ar.inspect}"
|
372
|
+
ar.select{|opt| opt.is_required}
|
373
|
+
end
|
374
|
+
|
375
|
+
#
|
376
|
+
# check to make sure the argument list is correct?
|
377
|
+
#
|
378
|
+
def process
|
379
|
+
@required_arguments.each_with_index do |arg, i|
|
380
|
+
if arg.infinite then
|
381
|
+
if i != required_arguments.size - 1 then
|
382
|
+
raise InfiniteArgNotLast.new("infinite argument #{arg.name} is not last")
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
# constants for user responses
|
390
|
+
YES_RESPONSE = :yes
|
391
|
+
NO_RESPONSE = :non
|
392
|
+
|
393
|
+
class ArgumentParserPlugin
|
394
|
+
attr_reader :value
|
395
|
+
|
396
|
+
def initialize(usage_ui, str)
|
397
|
+
end
|
398
|
+
|
399
|
+
def close
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
class IntegerArgumentPlugin < ArgumentParserPlugin
|
404
|
+
#
|
405
|
+
# parse and integer argument, validate it, and return a Integer object. If there
|
406
|
+
# is an error, throw a InvalidIntegerError exception.
|
407
|
+
#
|
408
|
+
def initialize(usage_ui, str)
|
409
|
+
if /^-?\d+/.match(str) then
|
410
|
+
@value = str.to_i
|
411
|
+
else
|
412
|
+
raise InvalidIntegerError.new(str)
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
class FloatArgumentPlugin < ArgumentParserPlugin
|
418
|
+
def initialize(usage_ui, str)
|
419
|
+
@value = str.to_f
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
class TimeArgumentPlugin < ArgumentParserPlugin
|
424
|
+
def initialize(usage_ui, str)
|
425
|
+
if /(\d+)\/(\d+)\/(\d+)-(\d+):(\d+)/.match(str) then
|
426
|
+
month = $1.to_i
|
427
|
+
day = $2.to_i
|
428
|
+
if $3.size < 3 then
|
429
|
+
year = $3.to_i + 2000
|
430
|
+
else
|
431
|
+
year = $3.to_i
|
432
|
+
end
|
433
|
+
hour = $4.to_i
|
434
|
+
minute = $5.to_i
|
435
|
+
@value = Time.local(year, month, day, hour, minute)
|
436
|
+
else
|
437
|
+
raise InvalidTimeError.new(str)
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
class FilePlugin < ArgumentParserPlugin
|
443
|
+
def close
|
444
|
+
@value.close
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
class FileInputPlugin < FilePlugin
|
449
|
+
def initialize(usage_ui, str)
|
450
|
+
$TRACE.debug 5, "trying to open file '#{str}' for input"
|
451
|
+
if FileTest.exist?(str) then
|
452
|
+
@value = File.open(str)
|
453
|
+
else
|
454
|
+
raise InvalidInputFilenameError.new(str)
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
class FileOutputPlugin < FilePlugin
|
460
|
+
def initialize(usage_ui, str)
|
461
|
+
$TRACE.debug 5, "trying to open file '#{str}' for output"
|
462
|
+
@value = File.open(str, "w")
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
OVERWRITE_QUERY = "%s exists, would you like to overwrite it"
|
467
|
+
APPEND_NO_EXIST_QUERY = "%s doesn't exist, would you still like to append"
|
468
|
+
|
469
|
+
class FileOutputQueryPlugin < FilePlugin
|
470
|
+
def initialize(usage_ui, str)
|
471
|
+
$TRACE.debug 5, "trying to open file '#{str}' for output"
|
472
|
+
if FileTest.exist?(str) then
|
473
|
+
raise FileOutputExistsError.new(str) if usage_ui.ask_yes_no(OVERWRITE_QUERY % str, NO_RESPONSE) == NO_RESPONSE
|
474
|
+
end
|
475
|
+
@value = File.open(str, "w")
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
class FileAppendPlugin < FilePlugin
|
480
|
+
def initialize(usage_ui, str)
|
481
|
+
$TRACE.debug 5, "trying to open file '#{str}' for append"
|
482
|
+
@value = File.open(str, "a")
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
class FileAppendQueryPlugin < FilePlugin
|
487
|
+
def initialize(usage_ui, str)
|
488
|
+
$TRACE.debug 5, "trying to open file '#{str}' for append(query)"
|
489
|
+
if !FileTest.exist?(str) then
|
490
|
+
$TRACE.debug 5, "file doesn't exist"
|
491
|
+
raise FileAppendDoesntExistError.new(str) if usage_ui.ask_yes_no(APPEND_NO_EXIST_QUERY % str, NO_RESPONSE) == NO_RESPONSE
|
492
|
+
end
|
493
|
+
@value = File.open(str, "a")
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
class FileReadLinesPlugin < ArgumentParserPlugin
|
498
|
+
def initialize(usage_ui, str)
|
499
|
+
$TRACE.debug 5, "trying to open file '#{str}' for read lines"
|
500
|
+
if FileTest.exist?(str) then
|
501
|
+
@value = File.readlines(str)
|
502
|
+
else
|
503
|
+
raise InvalidInputFilenameError.new(str)
|
504
|
+
end
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
#
|
509
|
+
# This is the class that does the heavy lifting for the Usage class. It parses the
|
510
|
+
# usage string, options string and makes the information available by using
|
511
|
+
# method_missing.
|
512
|
+
#
|
513
|
+
# see the README document for how to format the usage string and options string
|
514
|
+
#
|
515
|
+
class Base
|
516
|
+
class << self
|
517
|
+
def add_type_handler(type_char, klass)
|
518
|
+
@@type_chars[type_char] = klass
|
519
|
+
end
|
520
|
+
|
521
|
+
def reset_type_handlers
|
522
|
+
@@type_chars = {}
|
523
|
+
add_type_handler("@", TimeArgumentPlugin)
|
524
|
+
add_type_handler("%", IntegerArgumentPlugin)
|
525
|
+
add_type_handler("#", FloatArgumentPlugin)
|
526
|
+
add_type_handler(">>?", FileAppendQueryPlugin)
|
527
|
+
add_type_handler(">>", FileAppendPlugin)
|
528
|
+
add_type_handler("<<", FileReadLinesPlugin)
|
529
|
+
add_type_handler(">?", FileOutputQueryPlugin)
|
530
|
+
add_type_handler("<", FileInputPlugin)
|
531
|
+
add_type_handler(">", FileOutputPlugin)
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
# reset type handlers to just built-ins
|
536
|
+
reset_type_handlers
|
537
|
+
|
538
|
+
#
|
539
|
+
# create a UsageMod::Base object. usageString specifies the arguments expected. The
|
540
|
+
# optionsString specifies further information about the arguments. userArguments
|
541
|
+
# defaults to the program arguments but allows the user to get the arguments from
|
542
|
+
# somewhere else.
|
543
|
+
#
|
544
|
+
def initialize(usage_ui, usageString, optionsString="", userArguments=ARGV)
|
545
|
+
@usage_ui = usage_ui
|
546
|
+
@argHash = {}
|
547
|
+
@usageString = usageString
|
548
|
+
@optionsString = optionsString
|
549
|
+
@userArguments = userArguments
|
550
|
+
@custom_args = []
|
551
|
+
|
552
|
+
# this is what is used to support plugin parsing
|
553
|
+
@type_chars = @@type_chars.keys.sort_by{|x| -(x.size)}.map{|x| x.gsub(/./){|s| "\\" + s}}.join("|")
|
554
|
+
$TRACE.debug 5, "type_chars = '#{@type_chars}'"
|
555
|
+
|
556
|
+
# parse the usage string
|
557
|
+
@argList = parse_usage(usageString)
|
558
|
+
|
559
|
+
# parse the description
|
560
|
+
@descriptions = parse_option_descriptions
|
561
|
+
|
562
|
+
# update options hash now that some long names may have been parsed
|
563
|
+
@argList.update_with_long_names
|
564
|
+
end
|
565
|
+
|
566
|
+
|
567
|
+
DESC_PARSE_REGEX = /^-(\w),--(\S+)\s+(.*)$/
|
568
|
+
|
569
|
+
OPTION_NAME = 1
|
570
|
+
OPTION_LONG_NAME = 2
|
571
|
+
OPTION_DESCRIPTION = 3
|
572
|
+
|
573
|
+
#
|
574
|
+
# parse the option descriptions
|
575
|
+
#
|
576
|
+
def parse_option_descriptions
|
577
|
+
@maxOptionNameLen = 0
|
578
|
+
@optionsString.split("\n").each do |line|
|
579
|
+
if m = DESC_PARSE_REGEX.match(line) then
|
580
|
+
$TRACE.debug 5, "[#{m[1]}][#{m[2]}][#{m[3]}]"
|
581
|
+
len = m[OPTION_NAME].size + m[OPTION_LONG_NAME].size + 4
|
582
|
+
@maxOptionNameLen = len if len > @maxOptionNameLen
|
583
|
+
if option = @argList.lookup_option(m[OPTION_NAME]) then
|
584
|
+
option.long_name = m[OPTION_LONG_NAME]
|
585
|
+
option.description = m[OPTION_DESCRIPTION]
|
586
|
+
end
|
587
|
+
end
|
588
|
+
end
|
589
|
+
end
|
590
|
+
|
591
|
+
OPEN_PAREN_OR_BRACKET = 1
|
592
|
+
DASH_OR_TYPE = 2
|
593
|
+
ARG_OR_OPTION_NAME = 3
|
594
|
+
INFINITE = 4
|
595
|
+
CLOSE_PAREN_OR_BRACKET = 5
|
596
|
+
|
597
|
+
|
598
|
+
#
|
599
|
+
# this parses the usage string and returns an ArgumentList object that
|
600
|
+
# describes the arguments by their characteristics (type, optional/required, etc)
|
601
|
+
#
|
602
|
+
def parse_usage(usageString)
|
603
|
+
type_chars = @type_chars + "|\-"
|
604
|
+
# ([ @%#<><<>> name ... )]
|
605
|
+
parse_regex = /^([\(\[])?(#{type_chars})?([^.\)\]]*)(\.\.\.)?([\)\]])?$/
|
606
|
+
$TRACE.debug 5, "whole regex = #{parse_regex.source}"
|
607
|
+
|
608
|
+
arg_list = ArgumentList.new
|
609
|
+
option_stack = []
|
610
|
+
processing_options = true
|
611
|
+
option = nil
|
612
|
+
choices = nil
|
613
|
+
usageString.split(/\s/).each do |arg|
|
614
|
+
$TRACE.debug 5, "arg = '#{arg}'"
|
615
|
+
m = parse_regex.match(arg)
|
616
|
+
infinite = false
|
617
|
+
optional_arg = false
|
618
|
+
option = nil
|
619
|
+
if m then
|
620
|
+
$TRACE.debug 5, "got match, opening='#{m[OPEN_PAREN_OR_BRACKET]}' " +
|
621
|
+
"dash_or_type='#{m[DASH_OR_TYPE]}' " +
|
622
|
+
"main='#{m[ARG_OR_OPTION_NAME]}' " +
|
623
|
+
"infinite = '#{m[INFINITE]}' " +
|
624
|
+
"closing='#{m[CLOSE_PAREN_OR_BRACKET]}'"
|
625
|
+
$TRACE.debug 9, "option = #{option.inspect}, option_stack = #{option_stack.inspect}"
|
626
|
+
|
627
|
+
choices = m[ARG_OR_OPTION_NAME].split(/\|/)
|
628
|
+
raise ChoicesNotOnOptionError.new if choices.size > 1 && option_stack.size == 0
|
629
|
+
|
630
|
+
if m[DASH_OR_TYPE] then
|
631
|
+
raise ChoiceNoTypeError.new if choices.size > 1
|
632
|
+
|
633
|
+
case m[DASH_OR_TYPE]
|
634
|
+
when "-"
|
635
|
+
option = Option.new(m[ARG_OR_OPTION_NAME])
|
636
|
+
if m[OPEN_PAREN_OR_BRACKET] then
|
637
|
+
option.is_required = (m[OPEN_PAREN_OR_BRACKET] == "(")
|
638
|
+
option_stack.push(option)
|
639
|
+
else
|
640
|
+
option.arg_type = TrueClass
|
641
|
+
end
|
642
|
+
else
|
643
|
+
arg_type = "Arg:#{m[DASH_OR_TYPE]}"
|
644
|
+
$TRACE.debug 5, "is custom type '#{m[DASH_OR_TYPE]}'"
|
645
|
+
end
|
646
|
+
else
|
647
|
+
arg_type = String
|
648
|
+
end
|
649
|
+
|
650
|
+
if m[CLOSE_PAREN_OR_BRACKET] then
|
651
|
+
if option_stack.size == 0 then
|
652
|
+
if m[OPEN_PAREN_OR_BRACKET] && m[DASH_OR_TYPE] != '-' then
|
653
|
+
# Assume this is an optional parameter
|
654
|
+
optional_arg = true
|
655
|
+
$TRACE.debug 9, "parse_usage: Optional paramater found: #{m[ARG_OR_OPTION_NAME]}"
|
656
|
+
else
|
657
|
+
raise NestingUnderflow.new
|
658
|
+
end
|
659
|
+
else
|
660
|
+
option = option_stack.pop
|
661
|
+
if option.is_required != (m[CLOSE_PAREN_OR_BRACKET] == ")") then
|
662
|
+
raise MismatchedBracesError.new
|
663
|
+
end
|
664
|
+
# it has an argument if this token didn't also have a open bracket
|
665
|
+
option.has_argument = !m[OPEN_PAREN_OR_BRACKET]
|
666
|
+
if option.has_argument then
|
667
|
+
if choices.size > 1 then
|
668
|
+
option.choices = Choices.new(choices)
|
669
|
+
option.arg_type = Choices
|
670
|
+
else
|
671
|
+
option.arg_name = m[ARG_OR_OPTION_NAME]
|
672
|
+
option.arg_type = arg_type
|
673
|
+
end
|
674
|
+
end
|
675
|
+
end
|
676
|
+
end
|
677
|
+
|
678
|
+
if m[INFINITE] != nil then
|
679
|
+
$TRACE.debug 5, "is infinite"
|
680
|
+
infinite = true
|
681
|
+
end
|
682
|
+
|
683
|
+
$TRACE.debug 9, "option = #{option.inspect}, option_stack = #{option_stack.inspect}"
|
684
|
+
unless option || option_stack.size > 0
|
685
|
+
$TRACE.debug 9, "no longer processing options"
|
686
|
+
processing_options = false
|
687
|
+
end
|
688
|
+
else
|
689
|
+
arg_type = String
|
690
|
+
$TRACE.debug 5, "is string"
|
691
|
+
end
|
692
|
+
|
693
|
+
if processing_options then
|
694
|
+
if option_stack.size == 0 then
|
695
|
+
$TRACE.debug 5, "adding option #{option.name}"
|
696
|
+
arg_list.push_option(option)
|
697
|
+
end
|
698
|
+
else
|
699
|
+
arg = m[ARG_OR_OPTION_NAME]
|
700
|
+
if infinite then
|
701
|
+
@argHash[arg] = []
|
702
|
+
else
|
703
|
+
@argHash[arg] = nil
|
704
|
+
end
|
705
|
+
$TRACE.debug 5, "adding argument '#{arg}'"
|
706
|
+
arg_list.push_arg(Argument.new(arg, arg_type, infinite, optional_arg))
|
707
|
+
|
708
|
+
arg_list.process
|
709
|
+
end
|
710
|
+
end
|
711
|
+
|
712
|
+
raise NestingOverflow.new if option_stack.size > 0
|
713
|
+
|
714
|
+
$TRACE.debug 9, "arg_list = #{arg_list.inspect}\n"
|
715
|
+
arg_list
|
716
|
+
end
|
717
|
+
|
718
|
+
#
|
719
|
+
# set up @argHash for all the options based on two things:
|
720
|
+
#
|
721
|
+
# # the parse usage string description in @argList (which is an ArgumentList object)
|
722
|
+
# # the arguments that the user ran the program with which is in @userArguments (which is an array of strings)
|
723
|
+
#
|
724
|
+
# this allows method_missing to return the correct values when it is called
|
725
|
+
#
|
726
|
+
def parse_options()
|
727
|
+
last_index = @userArguments.size
|
728
|
+
found_options = []
|
729
|
+
option_waiting_for_argument = nil
|
730
|
+
$TRACE.debug 5, "parse_options: user arguments = #{@userArguments.inspect}"
|
731
|
+
@userArguments.each_with_index do |userarg, index|
|
732
|
+
$TRACE.debug 7, "arg = '#{userarg}'"
|
733
|
+
# if we found an option
|
734
|
+
if m = /^-{1,2}(.*)$/.match(userarg) then
|
735
|
+
# break out if we are waiting for an argument
|
736
|
+
break if option_waiting_for_argument
|
737
|
+
|
738
|
+
# handle requests for long usage string
|
739
|
+
if m[1] == "help" || m[1] == "?" then
|
740
|
+
raise LongUsageRequested
|
741
|
+
end
|
742
|
+
|
743
|
+
# look up the option
|
744
|
+
if option = @argList.lookup_option(m[1]) then
|
745
|
+
@argHash[option.name_as_symbol] = true
|
746
|
+
@argHash[option.long_name_as_symbol] = true if option.long_name
|
747
|
+
found_options.push(option)
|
748
|
+
if option.has_argument then
|
749
|
+
option_waiting_for_argument = option
|
750
|
+
end
|
751
|
+
else
|
752
|
+
raise UnknownOptionError.new("unknown option '#{userarg}'")
|
753
|
+
end
|
754
|
+
|
755
|
+
# else its either a option or regular argument
|
756
|
+
else
|
757
|
+
# if it is an option argument
|
758
|
+
if option = option_waiting_for_argument then
|
759
|
+
# process the argument
|
760
|
+
value = convert_value(option.arg_type, userarg)
|
761
|
+
if option.arg_type == Choices then
|
762
|
+
if !option.choices.include?(userarg) then
|
763
|
+
raise InvalidChoiceError.new(option, userarg)
|
764
|
+
end
|
765
|
+
@argHash[option.name_as_symbol] = value
|
766
|
+
@argHash[option.long_name_as_symbol] = value if option.long_name
|
767
|
+
else
|
768
|
+
@argHash[option.arg_name] = value
|
769
|
+
end
|
770
|
+
|
771
|
+
option_waiting_for_argument = nil
|
772
|
+
|
773
|
+
# otherwise its a regular argument
|
774
|
+
else
|
775
|
+
# we need to leave this loop
|
776
|
+
last_index = index
|
777
|
+
break
|
778
|
+
end
|
779
|
+
end
|
780
|
+
end
|
781
|
+
|
782
|
+
if option_waiting_for_argument then
|
783
|
+
raise MissingOptionArgumentError.new(option_waiting_for_argument)
|
784
|
+
end
|
785
|
+
|
786
|
+
missing_options = @argList.required_options - found_options
|
787
|
+
if missing_options.size > 0 then
|
788
|
+
raise MissingOptionsError.new(missing_options)
|
789
|
+
end
|
790
|
+
|
791
|
+
$TRACE.debug 5, "parse_options: last_index = #{last_index}"
|
792
|
+
@userArguments = @userArguments[last_index..-1]
|
793
|
+
end
|
794
|
+
|
795
|
+
#
|
796
|
+
# set up @argHash for the options and arguments based on two things (this calls
|
797
|
+
# UsageMod::Base#parse_options to parse the options:
|
798
|
+
#
|
799
|
+
# # the parse usage string description in @argList (which is an ArgumentList object)
|
800
|
+
# # the arguments that the user ran the program with which is in @userArguments (which is an array of strings)
|
801
|
+
#
|
802
|
+
# this allows method_missing to return the correct values when it is called
|
803
|
+
#
|
804
|
+
def parse_args()
|
805
|
+
parse_options
|
806
|
+
$TRACE.debug 5, "parse_args: user arguments = #{@userArguments.inspect}"
|
807
|
+
user_args_size = @userArguments.size
|
808
|
+
req_args_size = @argList.required_arguments.size
|
809
|
+
opt_args_size = @argList.optional_arguments.size
|
810
|
+
$TRACE.debug 5, "user_args_size = #{user_args_size}, req_args_size = #{req_args_size}\n"
|
811
|
+
if user_args_size < req_args_size then
|
812
|
+
$TRACE.debug 5, "parse_args: required_arguments = #{@argList.required_arguments.inspect}"
|
813
|
+
$TRACE.debug 5, "parse_args: optional_arguments = #{@argList.optional_arguments.inspect}"
|
814
|
+
raise TooFewArgError.new("too few arguments #{req_args_size} expected, #{user_args_size} given")
|
815
|
+
elsif user_args_size > (req_args_size + opt_args_size)
|
816
|
+
if !@argList.has_infinite_arg then
|
817
|
+
raise ExtraArgError.new("too many arguments #{req_args_size} expected, #{user_args_size} given")
|
818
|
+
end
|
819
|
+
end
|
820
|
+
|
821
|
+
@userArguments.each_with_index do |userarg, index|
|
822
|
+
arg = @argList.get_next_arg(index)
|
823
|
+
$TRACE.debug 5, "userarg = '#{userarg}', arg = '#{arg}'"
|
824
|
+
|
825
|
+
if @argList.has_infinite_arg && index + 1 >= req_args_size then
|
826
|
+
@argHash[@argList.last_arg.name].push(userarg)
|
827
|
+
else
|
828
|
+
@argHash[arg.name] = convert_value(arg.arg_type, userarg)
|
829
|
+
end
|
830
|
+
end
|
831
|
+
|
832
|
+
if block_given?
|
833
|
+
begin
|
834
|
+
yield self
|
835
|
+
ensure
|
836
|
+
@custom_args.each {|arg| arg.close}
|
837
|
+
end
|
838
|
+
end
|
839
|
+
|
840
|
+
self
|
841
|
+
end
|
842
|
+
|
843
|
+
#
|
844
|
+
# convert a value to its typed equivalent
|
845
|
+
#
|
846
|
+
def convert_value(arg_type, value)
|
847
|
+
$TRACE.debug 5, "convert_value: #{value} to #{arg_type}"
|
848
|
+
case arg_type.to_s
|
849
|
+
when /^Arg:(.+)$/
|
850
|
+
klass = @@type_chars[$1]
|
851
|
+
$TRACE.debug 5, "got custom arg type with char '#{$1}' about to call #{klass.inspect}"
|
852
|
+
custom_arg = klass.new(@usage_ui, value)
|
853
|
+
@custom_args.push(custom_arg)
|
854
|
+
return custom_arg.value
|
855
|
+
else
|
856
|
+
return value
|
857
|
+
end
|
858
|
+
end
|
859
|
+
|
860
|
+
#
|
861
|
+
# return the short usage string
|
862
|
+
#
|
863
|
+
def usage
|
864
|
+
@usageString.gsub(/#{@type_chars}/, "") + "\n"
|
865
|
+
end
|
866
|
+
|
867
|
+
#
|
868
|
+
# return the long usage string
|
869
|
+
#
|
870
|
+
def long_usage
|
871
|
+
@optionsString.split("\n").map do |line|
|
872
|
+
if m = DESC_PARSE_REGEX.match(line) then
|
873
|
+
opt_str = "-#{m[OPTION_NAME]},--#{m[OPTION_LONG_NAME]}"
|
874
|
+
opt_str + (" " * (@maxOptionNameLen + 2 - opt_str.size)) + m[OPTION_DESCRIPTION]
|
875
|
+
elsif /^\s+(\S.*)$/.match(line) then
|
876
|
+
(" " * (@maxOptionNameLen + 2)) + $1
|
877
|
+
else
|
878
|
+
line
|
879
|
+
end
|
880
|
+
end.join("\r\n")
|
881
|
+
end
|
882
|
+
|
883
|
+
#
|
884
|
+
# dump the argument hash
|
885
|
+
#
|
886
|
+
def dump
|
887
|
+
@argHash.keys.sort.each do |k|
|
888
|
+
puts "#{k} = #{@argHash[k].inspect}"
|
889
|
+
end
|
890
|
+
end
|
891
|
+
|
892
|
+
#
|
893
|
+
# uses the argHash to return data specified about the command line arguments
|
894
|
+
# by the symbol passed in.
|
895
|
+
#
|
896
|
+
def method_missing(symbol, *args)
|
897
|
+
$TRACE.debug 5, "UsageMod::Base method missing: #{symbol}"
|
898
|
+
if @argHash.has_key?(symbol.to_s) then
|
899
|
+
@argHash[symbol.to_s]
|
900
|
+
end
|
901
|
+
end
|
902
|
+
end
|
903
|
+
|
904
|
+
end # module UsageMod
|
905
|
+
|
906
|
+
#
|
907
|
+
# This is the main interface to this usage functionality. It operates by
|
908
|
+
# using method_missing to extract information about the command line arguments.
|
909
|
+
# Most all the functionality is implemented in the UsageMod::Base class with the Usage
|
910
|
+
# class being a wrapper around it to handle errors in a nice graceful fashion.
|
911
|
+
#
|
912
|
+
# The main way to use it is to give the usage string as the argument to build the
|
913
|
+
# usage object like below:
|
914
|
+
#
|
915
|
+
# usage = Usage.new "input_file output_file"
|
916
|
+
#
|
917
|
+
# see the README for a more complete description of what can appear in that string.
|
918
|
+
#
|
919
|
+
class Usage
|
920
|
+
#
|
921
|
+
# create a new usage object. It is forwarded on to UsageMod::Base#initialize. If an
|
922
|
+
# error occurs in UsageMod::Base, this displays the error to the user and displays the
|
923
|
+
# usage string as well.
|
924
|
+
#
|
925
|
+
def initialize(*args, &block)
|
926
|
+
@usageBase = UsageMod::Base.new(self, *args)
|
927
|
+
begin
|
928
|
+
@usageBase.parse_args(&block)
|
929
|
+
rescue UsageMod::LongUsageRequested
|
930
|
+
die_long
|
931
|
+
rescue UsageMod::Error => usageError
|
932
|
+
$TRACE.debug 1, usageError.backtrace.join("\n")
|
933
|
+
die(usageError.message)
|
934
|
+
rescue
|
935
|
+
raise
|
936
|
+
end
|
937
|
+
end
|
938
|
+
|
939
|
+
#
|
940
|
+
# Allows the program to die in a graceful manner. It displays
|
941
|
+
#
|
942
|
+
# ERROR: the error message
|
943
|
+
#
|
944
|
+
# It then calls exit (so it doesn't return)
|
945
|
+
#
|
946
|
+
def die(message)
|
947
|
+
print_program_name
|
948
|
+
print "ERROR: #{message}\n"
|
949
|
+
print "\n"
|
950
|
+
print_short_usage
|
951
|
+
exit 1
|
952
|
+
end
|
953
|
+
|
954
|
+
#
|
955
|
+
# Allows the program to die in a graceful manner. It displays
|
956
|
+
#
|
957
|
+
# ERROR: the error message
|
958
|
+
#
|
959
|
+
# followed by the usage string. It then calls exit (so it doesn't return)
|
960
|
+
#
|
961
|
+
def die_long
|
962
|
+
print_program_name
|
963
|
+
print_short_usage
|
964
|
+
print @usageBase.long_usage + "\n"
|
965
|
+
exit 1
|
966
|
+
end
|
967
|
+
|
968
|
+
#
|
969
|
+
# Prints out the program's name in a graceful manner.
|
970
|
+
#
|
971
|
+
# PROGRAM: program name
|
972
|
+
#
|
973
|
+
def print_program_name
|
974
|
+
print "PROGRAM: #{$0}\n"
|
975
|
+
end
|
976
|
+
|
977
|
+
#
|
978
|
+
# Print out the short usage string (without the options)
|
979
|
+
#
|
980
|
+
def print_short_usage
|
981
|
+
print "USAGE: #{File.basename($0)} #{@usageBase.usage}\n"
|
982
|
+
end
|
983
|
+
|
984
|
+
#
|
985
|
+
# forward all other calls to UsageMod::Base class
|
986
|
+
#
|
987
|
+
def method_missing(symbol, *args, &block)
|
988
|
+
$TRACE.debug 5, "Usage method missing: #{symbol}"
|
989
|
+
@usageBase.send(symbol, *args, &block)
|
990
|
+
end
|
991
|
+
|
992
|
+
def ask_yes_no(str, default)
|
993
|
+
while true
|
994
|
+
print str + " [" + (default == UsageMod::YES_RESPONSE ? "Yn" : "yN") + "]? "
|
995
|
+
$stdout.flush
|
996
|
+
s = $stdin.gets
|
997
|
+
case s
|
998
|
+
when /^$/
|
999
|
+
return default
|
1000
|
+
when /^y/i
|
1001
|
+
return UsageMod::YES_RESPONSE
|
1002
|
+
when /^n/i
|
1003
|
+
return UsageMod::NO_RESPONSE
|
1004
|
+
end
|
1005
|
+
end
|
1006
|
+
end
|
1007
|
+
end
|
1008
|
+
|
1009
|
+
# ================= if running this file ==============================================================
|