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/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 ==============================================================