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.
33 ## The set of values specifiable as the :type parameter to #opt.
34 TYPES = [:flag, :boolean, :bool, :int, :integer, :string, :double, :float]
36 ## The values from the commandline that were not interpreted by #parse.
37 attr_reader :leftovers
39 ## The complete configuration hashes for each option. (Mainly useful
43 ## Initializes the parser, and instance-evaluates any block given.
53 #instance_eval(&b) if b # can't take arguments
54 cloaker(&b).bind(self).call(*a) if b
57 ## Add an option. 'name' is the argument name, a unique identifier
58 ## for the option that you will use internally. 'desc' a string
59 ## description which will be displayed in help messages. Takes the
60 ## following optional arguments:
62 ## * :long: Specify the long form of the argument, i.e. the form
63 ## with two dashes. If unspecified, will be automatically derived
64 ## based on the argument name.
65 ## * :short: Specify the short form of the argument, i.e. the form
66 ## with one dash. If unspecified, will be automatically derived
67 ## based on the argument name.
68 ## * :type: Require that the argument take a parameter of type
69 ## 'type'. Can by any member of the TYPES constant or a
70 ## corresponding class (e.g. Integer for :int). If unset, the
71 ## default argument type is :flag, meaning the argument does not
72 ## take a parameter. Not necessary if :default: is specified.
73 ## * :default: Set the default value for an argument. Without a
74 ## default value, the hash returned by #parse (and thus
75 ## Trollop#options) will not contain the argument unless it is
76 ## given on the commandline. The argument type is derived
77 ## automatically from the class of the default value given, if
78 ## any. Specifying a :flag argument on the commandline whose
79 ## default value is true will change its value to false.
80 ## * :required: if set to true, the argument must be provided on the
82 def opt name, desc="", opts={}
83 raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name
88 when :flag, :boolean, :bool: :flag
89 when :int, :integer: :int
91 when :double, :float: :float
93 case opts[:type].to_s # sigh... there must be a better way to do this
94 when 'TrueClass', 'FalseClass': :flag
95 when 'String': :string
99 raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'"
103 raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type])
110 when TrueClass, FalseClass: :flag
114 raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'"
117 raise ArgumentError, ":type specification and default type don't match" if opts[:type] && type_from_default && opts[:type] != type_from_default
119 opts[:type] = (opts[:type] || type_from_default || :flag)
122 opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-")
130 raise ArgumentError, "invalid long option name #{opts[:long].inspect}"
132 raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]]
135 opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none
139 c = opts[:long].split(//).find { |c| c !~ /[\d]/ && !@short.member?(c) }
140 raise ArgumentError, "can't generate a short option name for #{opts[:long].inspect}: out of unique characters" unless c
149 raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'"
152 raise ArgumentError, "short option name #{opts[:short].inspect} is already taken; please specify a (different) :short" if @short[opts[:short]]
153 raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ /[\d-]/
156 ## fill in :default for flags
157 opts[:default] = false if opts[:type] == :flag && opts[:default].nil?
160 @long[opts[:long]] = name
161 @short[opts[:short]] = name if opts[:short]
163 @order << [:opt, name]
166 ## Sets the version string. If set, the user can request the version
167 ## on the commandline. Should be of the form "<program name>
168 ## <version number>".
169 def version s=nil; @version = s if s; @version end
171 ## Adds text to the help display.
172 def banner s; @order << [:text, s] end
175 ## Marks two (or more!) options as requiring each other. Only handles
176 ## undirected (i.e., mutual) dependencies. Directed dependencies are
177 ## better modeled with Trollop::die.
179 syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
180 @constraints << [:depends, syms]
183 ## Marks two (or more!) options as conflicting.
185 syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
186 @constraints << [:conflicts, syms]
189 ## yield successive arg, parameter pairs
190 def each_arg args # :nodoc:
194 until i >= args.length
196 when /^--$/ # arg terminator
197 remains += args[(i + 1) .. -1]
199 when /^--(\S+?)=(\S+)$/ # long argument with equals
202 when /^--(\S+)$/ # long argument
203 if args[i + 1] && args[i + 1] !~ PARAM_RE
204 remains << args[i + 1] unless yield args[i], args[i + 1]
206 else # long argument no parameter
210 when /^-(\S+)$/ # one or more short arguments
211 shortargs = $1.split(//)
212 shortargs.each_with_index do |a, j|
213 if j == (shortargs.length - 1) && args[i + 1] && args[i + 1] !~ PARAM_RE
214 remains << args[i + 1] unless yield "-#{a}", args[i + 1]
215 i += 1 # once more below
229 def parse cmdline #:nodoc:
234 opt :version, "Print version and exit" if @version unless @specs[:version] || @long["version"]
235 opt :help, "Show this message" unless @specs[:help] || @long["help"]
237 @specs.each do |sym, opts|
238 required[sym] = true if opts[:required]
239 vals[sym] = opts[:default]
244 @leftovers = each_arg cmdline do |arg, param|
252 raise CommandlineError, "invalid argument syntax: '#{arg}'"
254 raise CommandlineError, "unknown argument '#{arg}'" unless sym
255 raise CommandlineError, "option '#{arg}' specified multiple times" if found[sym]
256 args << [sym, arg, param]
259 @specs[sym][:type] != :flag # take params on all except flags
262 ## check for version and help args
263 raise VersionNeeded if args.any? { |sym, *a| sym == :version }
264 raise HelpNeeded if args.any? { |sym, *a| sym == :help }
266 ## check constraint satisfaction
267 @constraints.each do |type, syms|
268 constraint_sym = syms.find { |sym| found[sym] }
269 next unless constraint_sym
273 syms.each { |sym| raise CommandlineError, "--#{@long[constraint_sym]} requires --#{@long[sym]}" unless found[sym] }
275 syms.each { |sym| raise CommandlineError, "--#{@long[constraint_sym]} conflicts with --#{@long[sym]}" if found[sym] && sym != constraint_sym }
279 required.each do |sym, val|
280 raise CommandlineError, "option '#{sym}' must be specified" unless found[sym]
284 args.each do |sym, arg, param|
287 raise CommandlineError, "option '#{arg}' needs a parameter" unless param || opts[:type] == :flag
291 vals[sym] = !opts[:default]
293 raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^\d+$/
294 vals[sym] = param.to_i
296 raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE
297 vals[sym] = param.to_f
299 vals[sym] = param.to_s
323 ## Print the help message to 'stream'.
324 def educate stream=$stdout
325 width # just calculate it now; otherwise we have to be careful not to
326 # call this unless the cursor's at the beginning of a line.
329 @specs.each do |name, spec|
330 left[name] = "--#{spec[:long]}" +
331 (spec[:short] ? ", -#{spec[:short]}" : "") +
344 leftcol_width = left.values.map { |s| s.length }.max || 0
345 rightcol_start = leftcol_width + 6 # spaces
347 unless @order.size > 0 && @order.first.first == :text
348 stream.puts "#@version\n" if @version
349 stream.puts "Options:"
352 @order.each do |what, opt|
354 stream.puts wrap(opt)
359 stream.printf " %#{leftcol_width}s: ", left[opt]
362 if spec[:desc] =~ /\.$/
363 " (Default: #{spec[:default]})"
365 " (default: #{spec[:default]})"
370 stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
374 def wrap_line str, opts={} # :nodoc:
375 prefix = opts[:prefix] || 0
376 width = opts[:width] || (self.width - 1)
379 until start > str.length
381 if start + width >= str.length
384 x = str.rindex(/\s/, start + width)
385 x = str.index(/\s/, start) if x && x < start
388 ret << (ret.empty? ? "" : " " * prefix) + str[start ... nextt]
394 def wrap str, opts={} # :nodoc:
398 str.split("\n").map { |s| wrap_line s, opts }.flatten
402 ## instance_eval but with ability to handle block arguments
403 ## thanks to why: http://redhanded.hobix.com/inspect/aBlockCostume.html
404 def cloaker &b #:nodoc:
405 (class << self; self; end).class_eval do
406 define_method :cloaker_, &b
407 meth = instance_method :cloaker_
408 remove_method :cloaker_
414 ## The top-level entry method into Trollop. Creates a Parser object,
415 ## passes the block to it, then parses ARGV with it, handling any
416 ## errors or requests for help or version information appropriately
417 ## (and then exiting). Modifies ARGV in place. Returns a hash of
420 ## The block passed in should contain one or more calls to #opt
421 ## (Parser#opt), one or more calls to text (Parser#text), and
422 ## probably a call to version (Parser#version).
424 ## See the synopsis in README.txt for examples.
426 @p = Parser.new(*a, &b)
430 @p.leftovers.each { |l| ARGV << l }
432 rescue CommandlineError => e
433 $stderr.puts "Error: #{e.message}."
434 $stderr.puts "Try --help for help."
445 ## Informs the user that their usage of 'arg' was wrong, as detailed by
446 ## 'msg', and dies. Example:
449 ## opt :volume, :default => 0.0
452 ## die :volume, "too loud" if opts[:volume] > 10.0
453 ## die :volume, "too soft" if opts[:volume] < 0.1
455 ## In the one-argument case, simply print that message, a notice
456 ## about -h, and die. Example:
459 ## opt :whatever # ...
462 ## Trollop::die "need at least one filename" if ARGV.empty?
465 $stderr.puts "Error: argument --#{@p.specs[arg][:long]} #{msg}."
467 $stderr.puts "Error: #{arg}."
469 $stderr.puts "Try --help for help."
473 module_function :options, :die