CommandLine 0.6.0

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.
@@ -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