usage 0.0.3

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