1
## lib/trollop.rb -- trollop command-line processing library
2
## Author:: William Morgan (mailto: wmorgan-trollop@masanjin.net)
3
## Copyright:: Copyright 2007 William Morgan
4
## License:: GNU GPL version 2
10
## Thrown by Parser in the event of a commandline error. Not needed if
11
## you're using the Trollop::options entry.
12
class CommandlineError < StandardError; end
14
## Thrown by Parser if the user passes in '-h' or '--help'. Handled
15
## automatically by Trollop#options.
16
class HelpNeeded < StandardError; end
18
## Thrown by Parser if the user passes in '-h' or '--version'. Handled
19
## automatically by Trollop#options.
20
class VersionNeeded < StandardError; end
22
## Regex for floating point numbers
23
FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))$/
25
## Regex for parameters
26
PARAM_RE = /^-(-|\.$|[^\d\.])/
28
## The commandline parser. In typical usage, the methods in this class
29
## will be handled internally by Trollop#options, in which case only the
30
## methods #opt, #banner and #version, #depends, and #conflicts will
31
## typically be called.
34
## The set of values that indicate a flag type of option when one of
35
## the values is given to the :type parameter to #opt.
36
FLAG_TYPES = [:flag, :bool, :boolean]
38
## The set of values that indicate an option that takes a single
39
## parameter when one of the values is given to the :type parameter to
41
SINGLE_ARG_TYPES = [:int, :integer, :string, :double, :float]
43
## The set of values that indicate an option that takes multiple
44
## parameters when one of the values is given to the :type parameter to
46
MULTI_ARG_TYPES = [:ints, :integers, :strings, :doubles, :floats]
48
## The set of values specifiable as the :type parameter to #opt.
49
TYPES = FLAG_TYPES + SINGLE_ARG_TYPES + MULTI_ARG_TYPES
51
INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc:
53
## The values from the commandline that were not interpreted by #parse.
54
attr_reader :leftovers
56
## The complete configuration hashes for each option. (Mainly useful
60
## Initializes the parser, and instance-evaluates any block given.
70
@stop_on_unknown = false
72
#instance_eval(&b) if b # can't take arguments
73
cloaker(&b).bind(self).call(*a) if b
76
## Add an option. 'name' is the argument name, a unique identifier
77
## for the option that you will use internally. 'desc' a string
78
## description which will be displayed in help messages. Takes the
79
## following optional arguments:
81
## * :long: Specify the long form of the argument, i.e. the form
82
## with two dashes. If unspecified, will be automatically derived
83
## based on the argument name.
84
## * :short: Specify the short form of the argument, i.e. the form
85
## with one dash. If unspecified, will be automatically derived
86
## based on the argument name.
87
## * :type: Require that the argument take a parameter or parameters
88
## of type 'type'. For a single parameter, the value can be a
89
## member of the SINGLE_ARG_TYPES constant or a corresponding class
90
## (e.g. Integer for :int). For multiple parameters, the value can
91
## be a member of the MULTI_ARG_TYPES constant. If unset, the
92
## default argument type is :flag, meaning that the argument does
93
## not take a parameter. The specification of :type is not
94
## necessary if :default is given.
95
## * :default: Set the default value for an argument. Without a
96
## default value, the hash returned by #parse (and thus
97
## Trollop#options) will not contain the argument unless it is
98
## given on the commandline. The argument type is derived
99
## automatically from the class of the default value given, if
100
## any. Specifying a :flag argument on the commandline whose
101
## default value is true will change its value to false.
102
## * :required: If set to true, the argument must be provided on the
104
## * :multi: If set to true, allows multiple instances of the
105
## option. Otherwise, only a single instance of the option is
107
def opt name, desc="", opts={}
108
raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name
113
when :flag, :boolean, :bool; :flag
114
when :int, :integer; :int
115
when :ints, :integers; :ints
116
when :string; :string
117
when :strings; :strings
118
when :double, :float; :float
119
when :doubles, :floats; :floats
121
case opts[:type].to_s # sigh... there must be a better way to do this
122
when 'TrueClass', 'FalseClass'; :flag
123
when 'String'; :string
127
raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'"
131
raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type])
138
when TrueClass, FalseClass; :flag
141
if opts[:default].empty?
142
raise ArgumentError, "multiple argument type cannot be deduced from an empty array for '#{opts[:default][0].class.name}'"
144
case opts[:default][0] # the first element determines the types
146
when Numeric; :floats
147
when String; :strings
149
raise ArgumentError, "unsupported multiple argument type '#{opts[:default][0].class.name}'"
153
raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'"
156
raise ArgumentError, ":type specification and default type don't match" if opts[:type] && type_from_default && opts[:type] != type_from_default
158
opts[:type] = (opts[:type] || type_from_default || :flag)
161
opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-")
169
raise ArgumentError, "invalid long option name #{opts[:long].inspect}"
171
raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]]
174
opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none
178
c = opts[:long].split(//).find { |c| c !~ INVALID_SHORT_ARG_REGEX && !@short.member?(c) }
179
raise ArgumentError, "can't generate a short option name for #{opts[:long].inspect}: out of unique characters" unless c
188
raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'"
191
raise ArgumentError, "short option name #{opts[:short].inspect} is already taken; please specify a (different) :short" if @short[opts[:short]]
192
raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ INVALID_SHORT_ARG_REGEX
195
## fill in :default for flags
196
opts[:default] = false if opts[:type] == :flag && opts[:default].nil?
199
opts[:multi] ||= false
202
@long[opts[:long]] = name
203
@short[opts[:short]] = name if opts[:short]
205
@order << [:opt, name]
208
## Sets the version string. If set, the user can request the version
209
## on the commandline. Should be of the form "<program name>
210
## <version number>".
211
def version s=nil; @version = s if s; @version end
213
## Adds text to the help display.
214
def banner s; @order << [:text, s] end
217
## Marks two (or more!) options as requiring each other. Only handles
218
## undirected (i.e., mutual) dependencies. Directed dependencies are
219
## better modeled with Trollop::die.
221
syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
222
@constraints << [:depends, syms]
225
## Marks two (or more!) options as conflicting.
227
syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
228
@constraints << [:conflicts, syms]
231
## Defines a set of words which cause parsing to terminate when encountered,
232
## such that any options to the left of the word are parsed as usual, and
233
## options to the right of the word are left intact.
235
## A typical use case would be for subcommand support, where these would be
236
## set to the list of subcommands. A subsequent Trollop invocation would
237
## then be used to parse subcommand options.
239
@stop_words = [*words].flatten
242
## Similar to stop_on, but stops on any unknown word when encountered (unless
243
## it is a parameter for an argument).
245
@stop_on_unknown = true
248
## yield successive arg, parameter pairs
249
def each_arg args # :nodoc:
253
until i >= args.length
254
if @stop_words.member? args[i]
255
remains += args[i .. -1]
259
when /^--$/ # arg terminator
260
remains += args[(i + 1) .. -1]
262
when /^--(\S+?)=(\S+)$/ # long argument with equals
263
yield "--#{$1}", [$2]
265
when /^--(\S+)$/ # long argument
266
params = collect_argument_parameters(args, i + 1)
268
num_params_taken = yield args[i], params
269
unless num_params_taken
271
remains += args[i + 1 .. -1]
277
i += 1 + num_params_taken
278
else # long argument no parameter
282
when /^-(\S+)$/ # one or more short arguments
283
shortargs = $1.split(//)
284
shortargs.each_with_index do |a, j|
285
if j == (shortargs.length - 1)
286
params = collect_argument_parameters(args, i + 1)
288
num_params_taken = yield "-#{a}", params
289
unless num_params_taken
291
remains += args[i + 1 .. -1]
297
i += 1 + num_params_taken
298
else # argument no parameter
308
remains += args[i .. -1]
320
def parse cmdline #:nodoc:
324
opt :version, "Print version and exit" if @version unless @specs[:version] || @long["version"]
325
opt :help, "Show this message" unless @specs[:help] || @long["help"]
327
@specs.each do |sym, opts|
328
required[sym] = true if opts[:required]
329
vals[sym] = opts[:default]
334
@leftovers = each_arg cmdline do |arg, params|
342
raise CommandlineError, "invalid argument syntax: '#{arg}'"
344
raise CommandlineError, "unknown argument '#{arg}'" unless sym
346
if given_args.include?(sym) && !@specs[sym][:multi]
347
raise CommandlineError, "option '#{arg}' specified multiple times"
350
given_args[sym] ||= {}
352
given_args[sym][:arg] = arg
353
given_args[sym][:params] ||= []
355
# The block returns the number of parameters taken.
359
if SINGLE_ARG_TYPES.include?(@specs[sym][:type])
360
given_args[sym][:params] << params[0, 1] # take the first parameter
362
elsif MULTI_ARG_TYPES.include?(@specs[sym][:type])
363
given_args[sym][:params] << params # take all the parameters
364
num_params_taken = params.size
371
## check for version and help args
372
raise VersionNeeded if given_args.include? :version
373
raise HelpNeeded if given_args.include? :help
375
## check constraint satisfaction
376
@constraints.each do |type, syms|
377
constraint_sym = syms.find { |sym| given_args[sym] }
378
next unless constraint_sym
382
syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} requires --#{@specs[sym][:long]}" unless given_args.include? sym }
384
syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} conflicts with --#{@specs[sym][:long]}" if given_args.include?(sym) && (sym != constraint_sym) }
388
required.each do |sym, val|
389
raise CommandlineError, "option '#{sym}' must be specified" unless given_args.include? sym
393
given_args.each do |sym, given_data|
394
arg = given_data[:arg]
395
params = given_data[:params]
398
raise CommandlineError, "option '#{arg}' needs a parameter" if params.empty? && opts[:type] != :flag
402
vals[sym] = !opts[:default]
404
vals[sym] = params.map { |pg| pg.map { |p| parse_integer_parameter p, arg } }
406
vals[sym] = params.map { |pg| pg.map { |p| parse_float_parameter p, arg } }
407
when :string, :strings
408
vals[sym] = params.map { |pg| pg.map { |p| p.to_s } }
411
if SINGLE_ARG_TYPES.include?(opts[:type])
412
unless opts[:multi] # single parameter
413
vals[sym] = vals[sym][0][0]
414
else # multiple options, each with a single parameter
415
vals[sym] = vals[sym].map { |p| p[0] }
417
elsif MULTI_ARG_TYPES.include?(opts[:type]) && !opts[:multi]
418
vals[sym] = vals[sym][0] # single option, with multiple parameters
420
# else: multiple options, with multiple parameters
426
def parse_integer_parameter param, arg #:nodoc:
427
raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^\d+$/
431
def parse_float_parameter param, arg #:nodoc:
432
raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE
436
def collect_argument_parameters args, start_at #:nodoc:
439
while args[pos] && args[pos] !~ PARAM_RE && !@stop_words.member?(args[pos]) do
463
## Print the help message to 'stream'.
464
def educate stream=$stdout
465
width # just calculate it now; otherwise we have to be careful not to
466
# call this unless the cursor's at the beginning of a line.
469
@specs.each do |name, spec|
470
left[name] = "--#{spec[:long]}" +
471
(spec[:short] ? ", -#{spec[:short]}" : "") +
477
when :strings; " <s+>"
479
when :floats; " <f+>"
483
leftcol_width = left.values.map { |s| s.length }.max || 0
484
rightcol_start = leftcol_width + 6 # spaces
486
unless @order.size > 0 && @order.first.first == :text
487
stream.puts "#@version\n" if @version
488
stream.puts "Options:"
491
@order.each do |what, opt|
493
stream.puts wrap(opt)
498
stream.printf " %#{leftcol_width}s: ", left[opt]
501
if spec[:desc] =~ /\.$/
502
" (Default: #{spec[:default]})"
504
" (default: #{spec[:default]})"
509
stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
513
def wrap_line str, opts={} # :nodoc:
514
prefix = opts[:prefix] || 0
515
width = opts[:width] || (self.width - 1)
518
until start > str.length
520
if start + width >= str.length
523
x = str.rindex(/\s/, start + width)
524
x = str.index(/\s/, start) if x && x < start
527
ret << (ret.empty? ? "" : " " * prefix) + str[start ... nextt]
533
def wrap str, opts={} # :nodoc:
537
str.split("\n").map { |s| wrap_line s, opts }.flatten
541
## instance_eval but with ability to handle block arguments
542
## thanks to why: http://redhanded.hobix.com/inspect/aBlockCostume.html
543
def cloaker &b #:nodoc:
544
(class << self; self; end).class_eval do
545
define_method :cloaker_, &b
546
meth = instance_method :cloaker_
547
remove_method :cloaker_
553
## The top-level entry method into Trollop. Creates a Parser object,
554
## passes the block to it, then parses +args+ with it, handling any
555
## errors or requests for help or version information appropriately
556
## (and then exiting). Modifies +args+ in place. Returns a hash of
559
## The block passed in should contain one or more calls to #opt
560
## (Parser#opt), one or more calls to text (Parser#text), and
561
## probably a call to version (Parser#version).
563
## See the synopsis in README.txt for examples.
564
def options args = ARGV, *a, &b
565
@p = Parser.new(*a, &b)
569
@p.leftovers.each { |l| args << l }
571
rescue CommandlineError => e
572
$stderr.puts "Error: #{e.message}."
573
$stderr.puts "Try --help for help."
584
## Informs the user that their usage of 'arg' was wrong, as detailed by
585
## 'msg', and dies. Example:
588
## opt :volume, :default => 0.0
591
## die :volume, "too loud" if opts[:volume] > 10.0
592
## die :volume, "too soft" if opts[:volume] < 0.1
594
## In the one-argument case, simply print that message, a notice
595
## about -h, and die. Example:
598
## opt :whatever # ...
601
## Trollop::die "need at least one filename" if ARGV.empty?
604
$stderr.puts "Error: argument --#{@p.specs[arg][:long]} #{msg}."
606
$stderr.puts "Error: #{arg}."
608
$stderr.puts "Try --help for help."
612
module_function :options, :die