trollop

Ruby code posted by murphy < William Morgan
created at 02 Jun 04:11

Edit | Back
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
## lib/trollop.rb -- trollop command-line processing library
## Author::    William Morgan (mailto: wmorgan-trollop@masanjin.net)
## Copyright:: Copyright 2007 William Morgan
## License::   the same terms as ruby itself

require 'date'

module Trollop

VERSION = "1.16.2"

## Thrown by Parser in the event of a commandline error. Not needed if
## you're using the Trollop::options entry.
class CommandlineError < StandardError; end
  
## Thrown by Parser if the user passes in '-h' or '--help'. Handled
## automatically by Trollop#options.
class HelpNeeded < StandardError; end

## Thrown by Parser if the user passes in '-h' or '--version'. Handled
## automatically by Trollop#options.
class VersionNeeded < StandardError; end

## Regex for floating point numbers
FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))([eE][-+]?[\d]+)?$/

## Regex for parameters
PARAM_RE = /^-(-|\.$|[^\d\.])/

class Parser

  ## The set of values that indicate a flag option when passed as the
  ## +:type+ parameter of #opt.
  FLAG_TYPES = [:flag, :bool, :boolean]

  ## The set of values that indicate a single-parameter (normal) option when
  ## passed as the +:type+ parameter of #opt.
  ##
  ## A value of +io+ corresponds to a readable IO resource, including
  ## a filename, URI, or the strings 'stdin' or '-'.
  SINGLE_ARG_TYPES = [:int, :integer, :string, :double, :float, :io, :date]

  ## The set of values that indicate a multiple-parameter option (i.e., that
  ## takes multiple space-separated values on the commandline) when passed as
  ## the +:type+ parameter of #opt.
  MULTI_ARG_TYPES = [:ints, :integers, :strings, :doubles, :floats, :ios, :dates]

  ## The complete set of legal values for the +:type+ parameter of #opt.
  TYPES = FLAG_TYPES + SINGLE_ARG_TYPES + MULTI_ARG_TYPES

  INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc:

  ## The values from the commandline that were not interpreted by #parse.
  attr_reader :leftovers

  ## The complete configuration hashes for each option. (Mainly useful
  ## for testing.)
  attr_reader :specs

  ## Initializes the parser, and instance-evaluates any block given.
  def initialize *a, &b
    @version = nil
    @leftovers = []
    @specs = {}
    @long = {}
    @short = {}
    @order = []
    @constraints = []
    @stop_words = []
    @stop_on_unknown = false

    #instance_eval(&b) if b # can't take arguments
    cloaker(&b).bind(self).call(*a) if b
  end

  def opt name, desc="", opts={}
    raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name

    ## fill in :type
    opts[:type] = # normalize
      case opts[:type]
      when :boolean, :bool; :flag
      when :integer; :int
      when :integers; :ints
      when :double; :float
      when :doubles; :floats
      when Class
        case opts[:type].name
        when 'TrueClass', 'FalseClass'; :flag
        when 'String'; :string
        when 'Integer'; :int
        when 'Float'; :float
        when 'IO'; :io
        when 'Date'; :date
        else
          raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'"
        end
      when nil; nil
      else
        raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type])
        opts[:type]
      end

    ## for options with :multi => true, an array default doesn't imply
    ## a multi-valued argument. for that you have to specify a :type
    ## as well. (this is how we disambiguate an ambiguous situation;
    ## see the docs for Parser#opt for details.)
    disambiguated_default =
      if opts[:multi] && opts[:default].is_a?(Array) && !opts[:type]
        opts[:default].first
      else
        opts[:default]
      end

    type_from_default =
      case disambiguated_default
      when Integer; :int
      when Numeric; :float
      when TrueClass, FalseClass; :flag
      when String; :string
      when IO; :io
      when Date; :date
      when Array
        if opts[:default].empty?
          raise ArgumentError, "multiple argument type cannot be deduced from an empty array for '#{opts[:default][0].class.name}'"
        end
        case opts[:default][0]    # the first element determines the types
        when Integer; :ints
        when Numeric; :floats
        when String; :strings
        when IO; :ios
        when Date; :dates
        else
          raise ArgumentError, "unsupported multiple argument type '#{opts[:default][0].class.name}'"
        end
      when nil; nil
      else
        raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'"
      end

    raise ArgumentError, ":type specification and default type don't match (default type is #{type_from_default})" if opts[:type] && type_from_default && opts[:type] != type_from_default

    opts[:type] = opts[:type] || type_from_default || :flag

    ## fill in :long
    opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-")
    opts[:long] =
      case opts[:long]
      when /^--([^-].*)$/
        $1
      when /^[^-]/
        opts[:long]
      else
        raise ArgumentError, "invalid long option name #{opts[:long].inspect}"
      end
    raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]]

    ## fill in :short
    opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none
    opts[:short] = case opts[:short]
      when /^-(.)$/; $1
      when nil, :none, /^.$/; opts[:short]
      else raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'"
    end

    if opts[:short]
      raise ArgumentError, "short option name #{opts[:short].inspect} is already taken; please specify a (different) :short" if @short[opts[:short]]
      raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ INVALID_SHORT_ARG_REGEX
    end

    ## fill in :default for flags
    opts[:default] = false if opts[:type] == :flag && opts[:default].nil?

    ## autobox :default for :multi (multi-occurrence) arguments
    opts[:default] = [opts[:default]] if opts[:default] && opts[:multi] && !opts[:default].is_a?(Array)

    ## fill in :multi
    opts[:multi] ||= false

    opts[:desc] ||= desc
    @long[opts[:long]] = name
    @short[opts[:short]] = name if opts[:short] && opts[:short] != :none
    @specs[name] = opts
    @order << [:opt, name]
  end

  ## Sets the version string. If set, the user can request the version
  ## on the commandline. Should probably be of the form "<program name>
  ## <version number>".
  def version s=nil; @version = s if s; @version end

  ## Adds text to the help display. Can be interspersed with calls to
  ## #opt to build a multi-section help page.
  def banner s; @order << [:text, s] end
  alias :text :banner

  ## Marks two (or more!) options as requiring each other. Only handles
  ## undirected (i.e., mutual) dependencies. Directed dependencies are
  ## better modeled with Trollop::die.
  def depends *syms
    syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
    @constraints << [:depends, syms]
  end
  
  ## Marks two (or more!) options as conflicting.
  def conflicts *syms
    syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
    @constraints << [:conflicts, syms]
  end

  ## Defines a set of words which cause parsing to terminate when
  ## encountered, such that any options to the left of the word are
  ## parsed as usual, and options to the right of the word are left
  ## intact.
  ##
  ## A typical use case would be for subcommand support, where these
  ## would be set to the list of subcommands. A subsequent Trollop
  ## invocation would then be used to parse subcommand options, after
  ## shifting the subcommand off of ARGV.
  def stop_on *words
    @stop_words = [*words].flatten
  end

  ## Similar to #stop_on, but stops on any unknown word when encountered
  ## (unless it is a parameter for an argument). This is useful for
  ## cases where you don't know the set of subcommands ahead of time,
  ## i.e., without first parsing the global options.
  def stop_on_unknown
    @stop_on_unknown = true
  end

  ## Parses the commandline. Typically called by Trollop::options,
  ## but you can call it directly if you need more control.
  ##
  ## throws CommandlineError, HelpNeeded, and VersionNeeded exceptions.
  def parse cmdline=ARGV
    vals = {}
    required = {}

    opt :version, "Print version and exit" if @version unless @specs[:version] || @long["version"]
    opt :help, "Show this message" unless @specs[:help] || @long["help"]

    @specs.each do |sym, opts|
      required[sym] = true if opts[:required]
      vals[sym] = opts[:default]
      vals[sym] = [] if opts[:multi] && !opts[:default] # multi arguments default to [], not nil
    end

    resolve_default_short_options

    ## resolve symbols
    given_args = {}
    @leftovers = each_arg cmdline do |arg, params|
      sym = case arg
      when /^-([^-])$/
        @short[$1]
      when /^--([^-]\S*)$/
        @long[$1]
      else
        raise CommandlineError, "invalid argument syntax: '#{arg}'"
      end
      raise CommandlineError, "unknown argument '#{arg}'" unless sym

      if given_args.include?(sym) && !@specs[sym][:multi]
        raise CommandlineError, "option '#{arg}' specified multiple times"
      end

      given_args[sym] ||= {}

      given_args[sym][:arg] = arg
      given_args[sym][:params] ||= []

      # The block returns the number of parameters taken.
      num_params_taken = 0

      unless params.nil?
        if SINGLE_ARG_TYPES.include?(@specs[sym][:type])
          given_args[sym][:params] << params[0, 1]  # take the first parameter
          num_params_taken = 1
        elsif MULTI_ARG_TYPES.include?(@specs[sym][:type])
          given_args[sym][:params] << params        # take all the parameters
          num_params_taken = params.size
        end
      end

      num_params_taken
    end

    ## check for version and help args
    raise VersionNeeded if given_args.include? :version
    raise HelpNeeded if given_args.include? :help

    ## check constraint satisfaction
    @constraints.each do |type, syms|
      constraint_sym = syms.find { |sym| given_args[sym] }
      next unless constraint_sym

      case type
      when :depends
        syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} requires --#{@specs[sym][:long]}" unless given_args.include? sym }
      when :conflicts
        syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} conflicts with --#{@specs[sym][:long]}" if given_args.include?(sym) && (sym != constraint_sym) }
      end
    end

    required.each do |sym, val|
      raise CommandlineError, "option --#{@specs[sym][:long]} must be specified" unless given_args.include? sym
    end

    ## parse parameters
    given_args.each do |sym, given_data|
      arg = given_data[:arg]
      params = given_data[:params]

      opts = @specs[sym]
      raise CommandlineError, "option '#{arg}' needs a parameter" if params.empty? && opts[:type] != :flag

      vals["#{sym}_given".intern] = true # mark argument as specified on the commandline

      case opts[:type]
      when :flag
        vals[sym] = !opts[:default]
      when :int, :ints
        vals[sym] = params.map { |pg| pg.map { |p| parse_integer_parameter p, arg } }
      when :float, :floats
        vals[sym] = params.map { |pg| pg.map { |p| parse_float_parameter p, arg } }
      when :string, :strings
        vals[sym] = params.map { |pg| pg.map { |p| p.to_s } }
      when :io, :ios
        vals[sym] = params.map { |pg| pg.map { |p| parse_io_parameter p, arg } }
      when :date, :dates
        vals[sym] = params.map { |pg| pg.map { |p| parse_date_parameter p, arg } }
      end

      if SINGLE_ARG_TYPES.include?(opts[:type])
        unless opts[:multi]       # single parameter
          vals[sym] = vals[sym][0][0]
        else                      # multiple options, each with a single parameter
          vals[sym] = vals[sym].map { |p| p[0] }
        end
      elsif MULTI_ARG_TYPES.include?(opts[:type]) && !opts[:multi]
        vals[sym] = vals[sym][0]  # single option, with multiple parameters
      end
      # else: multiple options, with multiple parameters
    end

    ## modify input in place with only those
    ## arguments we didn't process
    cmdline.clear
    @leftovers.each { |l| cmdline << l }

    ## allow openstruct-style accessors
    class << vals
      def method_missing(m, *args)
        self[m] || self[m.to_s]
      end
    end
    vals
  end

  def parse_date_parameter param, arg #:nodoc:
    begin
      begin
        time = Chronic.parse(param)
      rescue NameError
        # chronic is not available
      end
      time ? Date.new(time.year, time.month, time.day) : Date.parse(param)
    rescue ArgumentError => e
      raise CommandlineError, "option '#{arg}' needs a date"
    end
  end

  ## Print the help message to +stream+.
  def educate stream=$stdout
    width # just calculate it now; otherwise we have to be careful not to
          # call this unless the cursor's at the beginning of a line.

    left = {}
    @specs.each do |name, spec| 
      left[name] = "--#{spec[:long]}" +
        (spec[:short] && spec[:short] != :none ? ", -#{spec[:short]}" : "") +
        case spec[:type]
        when :flag; ""
        when :int; " <i>"
        when :ints; " <i+>"
        when :string; " <s>"
        when :strings; " <s+>"
        when :float; " <f>"
        when :floats; " <f+>"
        when :io; " <filename/uri>"
        when :ios; " <filename/uri+>"
        when :date; " <date>"
        when :dates; " <date+>"
        end
    end

    leftcol_width = left.values.map { |s| s.length }.max || 0
    rightcol_start = leftcol_width + 6 # spaces

    unless @order.size > 0 && @order.first.first == :text
      stream.puts "#@version\n" if @version
      stream.puts "Options:"
    end

    @order.each do |what, opt|
      if what == :text
        stream.puts wrap(opt)
        next
      end

      spec = @specs[opt]
      stream.printf "  %#{leftcol_width}s:   ", left[opt]
      desc = spec[:desc] + begin
        default_s = case spec[:default]
        when $stdout; "<stdout>"
        when $stdin; "<stdin>"
        when $stderr; "<stderr>"
        when Array
          spec[:default].join(", ")
        else
          spec[:default].to_s
        end

        if spec[:default]
          if spec[:desc] =~ /\.$/
            " (Default: #{default_s})"
          else
            " (default: #{default_s})"
          end
        else
          ""
        end
      end
      stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
    end
  end

  def width #:nodoc:
    @width ||= if $stdout.tty?
      begin
        require 'curses'
        Curses::init_screen
        x = Curses::cols
        Curses::close_screen
        x
      rescue Exception
        80
      end
    else
      80
    end
  end

  def wrap str, opts={} # :nodoc:
    if str == ""
      [""]
    else
      str.split("\n").map { |s| wrap_line s, opts }.flatten
    end
  end

  ## The per-parser version of Trollop::die (see that for documentation).
  def die arg, msg
    if msg
      $stderr.puts "Error: argument --#{@specs[arg][:long]} #{msg}."
    else
      $stderr.puts "Error: #{arg}."
    end
    $stderr.puts "Try --help for help."
    exit(-1)
  end

private

  ## yield successive arg, parameter pairs
  def each_arg args
    remains = []
    i = 0

    until i >= args.length
      if @stop_words.member? args[i]
        remains += args[i .. -1]
        return remains
      end
      case args[i]
      when /^--$/ # arg terminator
        remains += args[(i + 1) .. -1]
        return remains
      when /^--(\S+?)=(.*)$/ # long argument with equals
        yield "--#{$1}", [$2]
        i += 1
      when /^--(\S+)$/ # long argument
        params = collect_argument_parameters(args, i + 1)
        unless params.empty?
          num_params_taken = yield args[i], params
          unless num_params_taken
            if @stop_on_unknown
              remains += args[i + 1 .. -1]
              return remains
            else
              remains += params
            end
          end
          i += 1 + num_params_taken
        else # long argument no parameter
          yield args[i], nil
          i += 1
        end
      when /^-(\S+)$/ # one or more short arguments
        shortargs = $1.split(//)
        shortargs.each_with_index do |a, j|
          if j == (shortargs.length - 1)
            params = collect_argument_parameters(args, i + 1)
            unless params.empty?
              num_params_taken = yield "-#{a}", params
              unless num_params_taken
                if @stop_on_unknown
                  remains += args[i + 1 .. -1]
                  return remains
                else
                  remains += params
                end
              end
              i += 1 + num_params_taken
            else # argument no parameter
              yield "-#{a}", nil
              i += 1
            end
          else
            yield "-#{a}", nil
          end
        end
      else
        if @stop_on_unknown
          remains += args[i .. -1]
          return remains
        else
          remains << args[i]
          i += 1
        end
      end
    end

    remains
  end

  def parse_integer_parameter param, arg
    raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^\d+$/
    param.to_i
  end

  def parse_float_parameter param, arg
    raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE
    param.to_f
  end

  def parse_io_parameter param, arg
    case param
    when /^(stdin|-)$/i; $stdin
    else
      require 'open-uri'
      begin
        open param
      rescue SystemCallError => e
        raise CommandlineError, "file or url for option '#{arg}' cannot be opened: #{e.message}"
      end
    end
  end

  def collect_argument_parameters args, start_at
    params = []
    pos = start_at
    while args[pos] && args[pos] !~ PARAM_RE && !@stop_words.member?(args[pos]) do
      params << args[pos]
      pos += 1
    end
    params
  end

  def resolve_default_short_options
    @order.each do |type, name|
      next unless type == :opt
      opts = @specs[name]
      next if opts[:short]

      c = opts[:long].split(//).find { |d| d !~ INVALID_SHORT_ARG_REGEX && !@short.member?(d) }
      if c # found a character to use
        opts[:short] = c
        @short[c] = name
      end
    end
  end

  def wrap_line str, opts={}
    prefix = opts[:prefix] || 0
    width = opts[:width] || (self.width - 1)
    start = 0
    ret = []
    until start > str.length
      nextt = 
        if start + width >= str.length
          str.length
        else
          x = str.rindex(/\s/, start + width)
          x = str.index(/\s/, start) if x && x < start
          x || str.length
        end
      ret << (ret.empty? ? "" : " " * prefix) + str[start ... nextt]
      start = nextt + 1
    end
    ret
  end

  ## instance_eval but with ability to handle block arguments
  ## thanks to why: http://redhanded.hobix.com/inspect/aBlockCostume.html
  def cloaker &b
    (class << self; self; end).class_eval do
      define_method :cloaker_, &b
      meth = instance_method :cloaker_
      remove_method :cloaker_
      meth
    end
  end
end
19.7 KB in 61 ms with coderay