CommandLine 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,54 @@
1
+ # $Id$
2
+ # $Source$
3
+ #
4
+ # Author: Jim Freeze
5
+ # Copyright (c) 2005 Jim Freeze
6
+ #
7
+ # =DESCRIPTION
8
+ # A very flexible commandline parser
9
+ #
10
+ # =Revision History
11
+ # Jim.Freeze 04/01/2005 Birthday
12
+ #
13
+ #
14
+
15
+ module CommandLine
16
+
17
+ #
18
+ # Data resulting from parsing a command line (Array)
19
+ # using a particular OptionParser object
20
+ #
21
+ class OptionData
22
+ attr_reader :argv, :unknown_options, :args, :not_parsed, :cmd
23
+
24
+ class OptionDataError < StandardError; end
25
+ class UnknownOptionError < OptionDataError; end
26
+
27
+ def initialize(argv, opts, unknown_options, args, not_parsed, cmd)
28
+ @opts = {}
29
+ opts.each { |k,v|
30
+ @opts[k] =
31
+ begin
32
+ Marshal.load(Marshal.dump(v))
33
+ rescue
34
+ v
35
+ end
36
+ }
37
+ @unknown_options = Marshal.load(Marshal.dump(unknown_options))
38
+ @not_parsed = Marshal.load(Marshal.dump(not_parsed))
39
+ @argv = Marshal.load(Marshal.dump(argv))
40
+ @args = Marshal.load(Marshal.dump(args))
41
+ @cmd = Marshal.load(Marshal.dump(cmd))
42
+ end
43
+
44
+ def [](key)
45
+ if @opts.has_key?(key)
46
+ @opts[key]
47
+ else
48
+ raise(UnknownOptionError, "Unknown option '#{key}'.")
49
+ end
50
+ end
51
+
52
+ end#class OptionData
53
+
54
+ end#module CommandLine
@@ -0,0 +1,521 @@
1
+ # $Id$
2
+ # $Source$
3
+ #
4
+ # Author: Jim Freeze
5
+ # Copyright (c) 2005 Jim Freeze
6
+ #
7
+ # =DESCRIPTION
8
+ # A very flexible commandline parser
9
+ #
10
+ # =Revision History
11
+ # Jim.Freeze 04/01/2005 Birthday
12
+ #
13
+ # :include: README
14
+ #
15
+
16
+ module CommandLine
17
+
18
+ class OptionParser
19
+ attr_reader :posix, :unknown_options_action,
20
+ :options
21
+
22
+ attr_accessor :columns, :body_indent, :tag_paragraph
23
+
24
+ DEFAULT_CONSOLE_WIDTH = 70
25
+ MIN_CONSOLE_WIDTH = 10
26
+ DEFAULT_BODY_INDENT = 4
27
+
28
+ #
29
+ # These helper lambdas are here because OptionParser is the object
30
+ # that calls them and hence knows the parameter order.
31
+ #
32
+
33
+ OPT_NOT_FOUND_BUT_REQUIRED = lambda { |opt|
34
+ raise(MissingRequiredOptionError,
35
+ "Missing required parameter '#{opt.names[0]}'.")
36
+ }
37
+
38
+ GET_ARG_ARRAY = lambda { |opt, user_opt, args| args }
39
+
40
+ GET_ARGS = lambda { |opt, user_opt, args|
41
+ return true if args.empty?
42
+ return args[0] if 1 == args.size
43
+ args
44
+ }
45
+
46
+ #
47
+ # Option Errors. Not the oxymoron below for MissingRequiredOptionError.
48
+ # The user can make an option required by adding the OPT_NOT_FOUND_BUT_REQUIRED
49
+ # lambda for the #opt_not_found action.
50
+ #
51
+ class OptionParserError < StandardError; end
52
+ class DuplicateOptionNameError < OptionParserError; end
53
+ class MissingRequiredOptionError < OptionParserError; end
54
+ class MissingRequiredOptionArgumentError < OptionParserError; end
55
+ class UnknownOptionError < OptionParserError; end
56
+ class UnknownPropertyError < OptionParserError; end
57
+ class PosixMismatchError < OptionParserError; end
58
+
59
+ def initialize(*opts_and_props)
60
+ @posix = false
61
+ @unknown_options_action = :raise
62
+ @unknown_options = []
63
+ @opt_lookup_by_any_name = {}
64
+ @command_options = nil
65
+
66
+ #
67
+ # Formatting defaults
68
+ #
69
+ console_width = ENV["COLUMNS"]
70
+ @columns =
71
+ if console_width.nil?
72
+ DEFAULT_CONSOLE_WIDTH
73
+ elsif console_width < MIN_CONSOLE_WIDTH
74
+ console_width
75
+ else
76
+ console_width - DEFAULT_BODY_INDENT
77
+ end
78
+ @body_indent = DEFAULT_BODY_INDENT
79
+ @tag_paragraph = false
80
+ @order = :index # | :alpha
81
+
82
+ props = []
83
+ keys = {}
84
+ opts_and_props.flatten!
85
+ opts_and_props.delete_if { |op|
86
+ if Symbol === op
87
+ props << op; true
88
+ elsif Hash === op
89
+ keys.update(op); true
90
+ else
91
+ false
92
+ end
93
+ }
94
+
95
+ props.each { |p|
96
+ case p
97
+ when :posix then @posix = true
98
+ else
99
+ raise(UnknownPropertyError, "Unknown property '#{p.inspect}'.")
100
+ end
101
+ }
102
+
103
+ keys.each { |k,v|
104
+ case k
105
+ when :unknown_options_action
106
+ if [:collect, :ignore, :raise].include?(v)
107
+ @unknown_options_action = v
108
+ else
109
+ raise(UnknownPropertyError, "Unknown value '#{v}' for "+
110
+ ":unknown_options property.")
111
+ end
112
+ when :command_options
113
+ @command_options = v
114
+ @commands = v.keys
115
+ else
116
+ raise(UnknownPropertyError, "Unknown property '#{k.inspect}'.")
117
+ end
118
+ }
119
+ # :unknown_options => :collect
120
+ # :unknown_options => :ignore
121
+ # :unknown_options => :raise
122
+
123
+ opts = opts_and_props
124
+
125
+ @options = []
126
+ opts.each { |opt|
127
+ # If user wants to parse posix, then ensure all options are posix
128
+ raise(PosixMismatchError,
129
+ "Posix types do not match. #{opt.inspect}") if @posix && !opt.posix
130
+ @options << opt
131
+ }
132
+
133
+ #p "options-"*5
134
+ #p @options
135
+ add_names(@options)
136
+
137
+ yield self if block_given?
138
+ end
139
+
140
+ #
141
+ # add_option :names => %w{--file --use-this-file -f},
142
+ # :
143
+ #
144
+ # add_option :names => %w(--version -v),
145
+ # :arg_arity => [0,0], # default
146
+ # :option_description => "Returns Version"
147
+ # add_option :names => %w(--file -f),
148
+ # :arg_arity => [1,:unlimited],
149
+ # :opt_description => "Define the output filename.",
150
+ # :arg_description => "Output file"
151
+ # :opt_exists => lambda {}
152
+ # :opt_not_exists => lambda {}
153
+ # :option_found
154
+ # :no_option_found
155
+ # :opt_found => lambda {}
156
+ # :no_opt_found => lambda {}
157
+ #
158
+
159
+ #
160
+ # Add an option
161
+ #
162
+ def <<(option)
163
+ @options << option
164
+ add_names(option)
165
+ self
166
+ end
167
+
168
+ def add_option(*h)
169
+ opt = Option.new(*h)
170
+ @options << opt
171
+ add_names(opt)
172
+ end
173
+
174
+ def add_names(*options)
175
+ options.flatten.each { |option|
176
+ raise "Wrong data type '#{option.name}." unless Option === option
177
+ option.names.each { |name|
178
+ raise(DuplicateOptionNameError,
179
+ "Duplicate option name '#{name}'.") if
180
+ @opt_lookup_by_any_name.has_key?(name)
181
+ @opt_lookup_by_any_name[name] = option
182
+ }
183
+ }
184
+ end
185
+
186
+ def validate_parse_options(h)
187
+ h[:names].each { |name| check_option_name(name) }
188
+
189
+ #if @posix
190
+ # all are single-dash:single-char OR double-dash:multi-char
191
+ #else if unix compliant
192
+ # single-dash only
193
+ #else any - does not support combination - try to on single/single
194
+ #end
195
+ end
196
+
197
+ # def [](opt)
198
+ # @options[@opt_lookup_by_any_name[opt][0]]
199
+ # end
200
+
201
+ #
202
+ # Parse the command line
203
+ #
204
+ def parse(argv=ARGV)
205
+ argv = [argv] unless Array === argv
206
+
207
+ #
208
+ # Holds the results of each option. The key used is
209
+ # the first in the :names Array.
210
+ #
211
+ opts = Hash.new( :not_found )
212
+
213
+ #
214
+ # A command is the first non-option free argument on the command line.
215
+ # This is a user selection and is the first argument in args.
216
+ # cmd = args.shift
217
+ # Example:
218
+ # cvs -v cmd --cmd-option arg
219
+ #
220
+ cmd = nil
221
+ cmd_options = {}
222
+
223
+ #
224
+ # #parse_argv yields an array containing the option and its arguments.
225
+ # [opts, array_args]
226
+ # How do we collect all the arguments when OptionParser deal with an
227
+ # empty option list
228
+ #
229
+ parse_argv(argv) { |optarg|
230
+ user_option = optarg[0]
231
+ args = optarg[1]
232
+
233
+ m = nil
234
+ if @opt_lookup_by_any_name.has_key?(user_option) ||
235
+ 1 == (m = @opt_lookup_by_any_name.keys.grep(/^#{user_option}/)).size
236
+ user_option = m[0] if m
237
+ opt = @opt_lookup_by_any_name[user_option]
238
+ opt_key = opt.names[0]
239
+
240
+ opts[opt_key] =
241
+ if Proc === opt.opt_found
242
+ # Take the arguments depending upon arity
243
+ opt_args = get_opt_args(opt, user_option, args)
244
+ opt.opt_found.call(opt, user_option, opt_args)
245
+ else
246
+ opt.opt_found
247
+ end
248
+ # Collect any remaining args
249
+ @args += args
250
+
251
+ elsif :collect == @unknown_options_action
252
+ @unknown_options << user_option
253
+ elsif :ignore == @unknown_options_action
254
+ else
255
+ raise(UnknownOptionError, "Unknown option '#{user_option}' in "+
256
+ "#{@opt_lookup_by_any_name.inspect}.")
257
+ end
258
+ }
259
+
260
+ #
261
+ # Call :not_found for all the options not on the command line.
262
+ #
263
+ @options.each { |opt|
264
+ name = opt.names[0]
265
+ if :not_found == opts[name]
266
+ opts[name] =
267
+ if Proc === opt.opt_not_found
268
+ opt.opt_not_found.call(opt)
269
+ else
270
+ opt.opt_not_found
271
+ end
272
+ end
273
+ }
274
+
275
+ OptionData.new(argv, opts, @unknown_options, @args, @not_parsed, cmd)
276
+ end
277
+
278
+ def get_opt_args(opt, user_option, args)
279
+ min, max = *opt.arg_arity
280
+ size = args.size
281
+
282
+ if (min == max && max > 0 && size < max) || (size < min)
283
+ raise(MissingRequiredOptionArgumentError,
284
+ "Insufficient arguments #{args.inspect}for option '#{user_option}' "+
285
+ "with :arg_arity #{opt.arg_arity.inspect}")
286
+ end
287
+
288
+ if 0 == min && 0 == max
289
+ []
290
+ else
291
+ max = size if -1 == max
292
+ args.slice!(0..[min, [max, size].min].max - 1)
293
+ end
294
+ end
295
+
296
+ def get_posix_re
297
+ flags = []
298
+ nflags = []
299
+ @options.each { |o|
300
+ if [0,0] == o.arg_arity
301
+ flags << o.names[0][1..1]
302
+ else
303
+ nflags << o.names[0][1..1]
304
+ end
305
+ }
306
+ flags = flags.join
307
+ flags = flags.empty? ? "" : "[#{flags}\]+"
308
+ nflags = nflags.join
309
+ nflags = nflags.empty? ? "" : "[#{nflags}\]"
310
+ Regexp.new("^-(#{flags})(#{nflags})(.*)\$")
311
+ end
312
+
313
+ #######################################################################
314
+ def parse_posix_argv(argv)
315
+ re = @posix ? get_posix_re : Option::GENERAL_OPT_EQ_ARG_RE
316
+ p re if $DEBUG
317
+ tagged = []
318
+
319
+ #
320
+ # A Posix command line must have all the options precede
321
+ # non option arguments. For example
322
+ # :names => -h -e -l -p -s
323
+ # where -p can take an argument
324
+ # Command line can read:
325
+ # -helps => -h -e -l -p s
326
+ # -p fred non-opt-arg
327
+ # -p fred non-opt-arg -h # not ok
328
+ # -he -popt-arg1 -popt-arg2 non-opt-arg
329
+ # -p=fred # this is not legal?
330
+ # -pfred === -p fred
331
+ #
332
+
333
+ #"-helps" "-pfred" "-p" "fred"
334
+ #-h -e -l -p [s] -p [fred] -p [fred]
335
+ #[-h, []], [-e []], [-l, []], [-p, [s]], -p
336
+
337
+ argv.each { |e|
338
+ m = re.match(e)
339
+ if m.nil?
340
+ tagged << [:arg, e]
341
+ else
342
+ raise "houston, we have a problem" if m.nil?
343
+ unless m[1].empty?
344
+ m[1].split(//).each { |e| tagged << [:opt, "-#{e}"] }
345
+ end
346
+
347
+ unless m[2].empty?
348
+ tagged << [:opt, "-#{m[2]}"]
349
+ tagged << [:arg, m[3]] unless m[3].empty?
350
+ end
351
+ end
352
+ }
353
+
354
+ if $DEBUG
355
+ print "Tagged:"
356
+ p tagged
357
+ end
358
+ #
359
+ # Now, combine any adjacent args such that
360
+ # [[:arg, "arg1"], [:arg, "arg2"]]
361
+ # becomes
362
+ # [[:args, ["arg1", "arg2"]]]
363
+ # and the final result should be
364
+ # [ "--file", ["arg1", "arg2"]]
365
+ #
366
+
367
+ parsed = []
368
+ @args = []
369
+ tagged.each { |e|
370
+ if :opt == e[0]
371
+ parsed << [e[1], []]
372
+ else
373
+ if Array === parsed[-1]
374
+ parsed[-1][-1] += [e[1]]
375
+ else
376
+ @args << e[1]
377
+ end
378
+ end
379
+ }
380
+ parsed.each { |e| yield e }
381
+ end
382
+
383
+ #
384
+ # Seperates options from arguments
385
+ # Does not look for valid options ( or should it? )
386
+ #
387
+ # %w(-fred file1 file2) => ["-fred", ["file1", "file2"]]
388
+ # %w(--fred -t -h xyz) => ["--fred", []] ["-t", []] ["-h", ["xyz"]]
389
+ # %w(-f=file) => ["-f", ["file"]]
390
+ # %w(--file=fred) => ["--file", ["fred"]]
391
+ # %w(-file=fred) => ["-file", ["fred"]]
392
+ # ['-file="fred1 fred2"'] => ["-file", ["fred1", "fred2"]]
393
+ #
394
+ def parse_argv(argv, &block)
395
+ return parse_posix_argv(argv, &block) if @posix
396
+
397
+ @not_parsed = []
398
+ tagged = []
399
+ argv.each_with_index { |e,i|
400
+ if "--" == e
401
+ @not_parsed = argv[(i+1)..(argv.size+1)]
402
+ break
403
+ elsif "-" == e
404
+ tagged << [:arg, e]
405
+ elsif ?- == e[0]
406
+ m = Option::GENERAL_OPT_EQ_ARG_RE.match(e)
407
+ if m.nil?
408
+ tagged << [:opt, e]
409
+ else
410
+ tagged << [:opt, m[1]]
411
+ tagged << [:arg, m[2]]
412
+ end
413
+ else
414
+ tagged << [:arg, e]
415
+ end
416
+ }
417
+
418
+ #
419
+ # The tagged array has the form:
420
+ # [
421
+ # [:opt, "-a"], [:arg, "filea"],
422
+ # [:opt, "-b"], [:arg, "fileb"],
423
+ # #[:not_parsed, ["-z", "-y", "file", "file2", "-a", "-b"]]
424
+ # ]
425
+
426
+ #
427
+ # Now, combine any adjacent args such that
428
+ # [[:arg, "arg1"], [:arg, "arg2"]]
429
+ # becomes
430
+ # [[:args, ["arg1", "arg2"]]]
431
+ # and the final result should be
432
+ # [ "--file", ["arg1", "arg2"]]
433
+ #
434
+
435
+ parsed = []
436
+ @args = []
437
+ tagged.each { |e|
438
+ if :opt == e[0]
439
+ parsed << [e[1], []]
440
+ elsif :arg == e[0]
441
+ if Array === parsed[-1]
442
+ parsed[-1][-1] += [e[1]]
443
+ else
444
+ @args << e[1]
445
+ end
446
+ else
447
+ raise "How did we get here?"
448
+ end
449
+ }
450
+ parsed.each { |e| block.call(e) }
451
+ end
452
+
453
+ def to_str
454
+ to_s
455
+ end
456
+
457
+ def to_s(sep="\n")
458
+ return "" if @options.empty?
459
+
460
+ require 'commandline/text/format'
461
+ @f = Text::Format.new
462
+ @f.columns = @columns
463
+ @f.first_indent = 4
464
+ @f.body_indent = 8
465
+ @f.tag_paragraph = false
466
+
467
+ header = ["OPTIONS\n"]
468
+ s = []
469
+ @options.each { |opt|
470
+ opt_str = []
471
+ if block_given?
472
+ result = yield(opt.names, opt.opt_description, opt.arg_description)
473
+ if result.kind_of?(String)
474
+ opt_str << result unless result.empty?
475
+ elsif result.nil?
476
+ opt_str << format_option(opt.names, opt.opt_description, opt.arg_description)
477
+ elsif result.kind_of?(Array) && 3 == result.size
478
+ opt_str << format_option(*result)
479
+ else
480
+ raise "Invalid return value #{result.inspect} from yield block "+
481
+ "attached to #to_s."
482
+ end
483
+ else
484
+ opt_str << format_option(opt.names, opt.opt_description, opt.arg_description)
485
+ end
486
+ s << opt_str.join unless opt_str.empty?
487
+ }
488
+ #s.collect! { |i| i.kind_of?(Array) && /\n+/ =~ i[0] ? i.join : f.paragraphs(i) }
489
+ [header, s].flatten.join(sep)
490
+ end
491
+
492
+ def format_option(names, opt_desc, arg_desc)
493
+ # TODO: Clean up the magic numbers
494
+
495
+ f = Text::Format.new
496
+ f.columns = @columns
497
+ f.first_indent = 4
498
+ f.body_indent = 8
499
+ f.tabstop = 4
500
+ s = ""
501
+ s << f.format("#{names.join(",")} #{arg_desc}")
502
+ #if 7 == s.last.size
503
+ if 7 == s.size
504
+ f.first_indent = f.first_indent - 2
505
+ s.rstrip!
506
+ s << f.format(opt_desc)
507
+ #elsif 8 == s.last.size
508
+ elsif 8 == s.size
509
+ f.first_indent = f.first_indent - 3
510
+ s.rstrip!
511
+ s << f.format(opt_desc)
512
+ else
513
+ f.first_indent = 2 * f.first_indent
514
+ s << f.format(opt_desc)
515
+ end
516
+ end
517
+ private :format_option
518
+
519
+ end#class OptionParser
520
+
521
+ end#module CommandLine