tag release 1.7
[trollop:mainline.git] / lib / trollop.rb
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
5
6 module Trollop
7
8 VERSION = "1.5"
9
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
13   
14 ## Thrown by Parser if the user passes in '-h' or '--help'. Handled
15 ## automatically by Trollop#options.
16 class HelpNeeded < StandardError; end
17
18 ## Thrown by Parser if the user passes in '-h' or '--version'. Handled
19 ## automatically by Trollop#options.
20 class VersionNeeded < StandardError; end
21
22 ## Regex for floating point numbers
23 FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))$/
24
25 ## Regex for parameters
26 PARAM_RE = /^-(-|\.$|[^\d\.])/
27
28 ## The commandline parser. In typical usage, the methods in this class
29 ## will be handled internally by Trollop#options, in which case only
30 ## the methods #opt, #banner and #version will be called.
31 class Parser
32   ## The set of values specifiable as the :type parameter to #opt.
33   TYPES = [:flag, :boolean, :bool, :int, :integer, :string, :double, :float]
34
35   ## The values from the commandline that were not interpreted by #parse.
36   attr_reader :leftovers
37
38   ## The complete configuration hashes for each option. (Mainly useful
39   ## for testing.)
40   attr_reader :specs
41
42   ## Initializes the parser, and instance-evaluates any block given.
43   def initialize *a, &b
44     @version = nil
45     @leftovers = []
46     @specs = {}
47     @long = {}
48     @short = {}
49     @order = []
50     @constraints = []
51
52     #instance_eval(&b) if b # can't take arguments
53     cloaker(&b).bind(self).call(*a) if b
54   end
55
56   ## Add an option. 'name' is the argument name, a unique identifier
57   ## for the option that you will use internally. 'desc' a string
58   ## description which will be displayed in help messages. Takes the
59   ## following optional arguments:
60   ##
61   ## * :long: Specify the long form of the argument, i.e. the form
62   ##   with two dashes. If unspecified, will be automatically derived
63   ##   based on the argument name.
64   ## * :short: Specify the short form of the argument, i.e. the form
65   ##   with one dash. If unspecified, will be automatically derived
66   ##   based on the argument name.
67   ## * :type: Require that the argument take a parameter of type
68   ##   'type'. Can by any member of the TYPES constant or a
69   ##   corresponding class (e.g. Integer for :int). If unset, the
70   ##   default argument type is :flag, meaning the argument does not
71   ##   take a parameter. Not necessary if :default: is specified.
72   ## * :default: Set the default value for an argument. Without a
73   ##   default value, the hash returned by #parse (and thus
74   ##   Trollop#options) will not contain the argument unless it is
75   ##   given on the commandline. The argument type is derived
76   ##   automatically from the class of the default value given, if
77   ##   any. Specifying a :flag argument on the commandline whose
78   ##   default value is true will change its value to false.
79   ## * :required: if set to true, the argument must be provided on the
80   ##   commandline.
81   def opt name, desc="", opts={}
82     raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name
83
84     ## fill in :type
85     opts[:type] = 
86       case opts[:type]
87       when :flag, :boolean, :bool: :flag
88       when :int, :integer: :int
89       when :string: :string
90       when :double, :float: :float
91       when Class
92         case opts[:type].to_s # sigh... there must be a better way to do this
93         when 'TrueClass', 'FalseClass': :flag
94         when 'String': :string
95         when 'Integer': :int
96         when 'Float': :float
97         else
98           raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'"
99         end
100       when nil: nil
101       else
102         raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type])
103       end
104
105     type_from_default =
106       case opts[:default]
107       when Integer: :int
108       when Numeric: :float
109       when TrueClass, FalseClass: :flag
110       when String: :string
111       when nil: nil
112       else
113         raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'"
114       end
115
116     raise ArgumentError, ":type specification and default type don't match" if opts[:type] && type_from_default && opts[:type] != type_from_default
117
118     opts[:type] = (opts[:type] || type_from_default || :flag)
119
120     ## fill in :long
121     opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-")
122     opts[:long] =
123       case opts[:long]
124       when /^--([^-].*)$/
125         $1
126       when /^[^-]/
127         opts[:long]
128       else
129         raise ArgumentError, "invalid long option name #{opts[:long].inspect}"
130       end
131     raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]]
132
133     ## fill in :short
134     opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none
135     opts[:short] =
136       case opts[:short]
137       when nil
138         c = opts[:long].split(//).find { |c| c !~ /[\d]/ && !@short.member?(c) }
139         raise ArgumentError, "can't generate a short option name for #{opts[:long].inspect}: out of unique characters" unless c
140         c
141       when /^-(.)$/
142         $1
143       when /^.$/
144         opts[:short]
145       when :none
146         nil
147       else
148         raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'"
149       end
150     if opts[:short]
151       raise ArgumentError, "short option name #{opts[:short].inspect} is already taken; please specify a (different) :short" if @short[opts[:short]]
152       raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ /[\d-]/
153     end
154
155     ## fill in :default for flags
156     opts[:default] = false if opts[:type] == :flag && opts[:default].nil?
157
158     opts[:desc] ||= desc
159     @long[opts[:long]] = name
160     @short[opts[:short]] = name if opts[:short]
161     @specs[name] = opts
162     @order << [:opt, name]
163   end
164
165   ## Sets the version string. If set, the user can request the version
166   ## on the commandline. Should be of the form "<program name>
167   ## <version number>".
168   def version s=nil; @version = s if s; @version end
169
170   ## Adds text to the help display.
171   def banner s; @order << [:text, s] end
172   alias :text :banner
173
174   ## Marks two (or more!) options as requiring each other. Only
175   ## handles undirected dependcies. Directed dependencies are better
176   ## modeled with #die.
177   def depends *syms
178     syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
179     @constraints << [:depends, syms]
180   end
181   
182   ## Marks two (or more!) options as conflicting.
183   def conflicts *syms
184     syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
185     @constraints << [:conflicts, syms]
186   end
187
188   ## yield successive arg, parameter pairs
189   def each_arg args # :nodoc:
190     remains = []
191     i = 0
192
193     until i >= args.length
194       case args[i]
195       when /^--$/ # arg terminator
196         remains += args[(i + 1) .. -1]
197         break
198       when /^--(\S+?)=(\S+)$/ # long argument with equals
199         yield "--#{$1}", $2
200         i += 1
201       when /^--(\S+)$/ # long argument
202         if args[i + 1] && args[i + 1] !~ PARAM_RE
203           remains << args[i + 1] unless yield args[i], args[i + 1]
204           i += 2
205         else # long argument no parameter
206           yield args[i], nil
207           i += 1
208         end
209       when /^-(\S+)$/ # one or more short arguments
210         shortargs = $1.split(//)
211         shortargs.each_with_index do |a, j|
212           if j == (shortargs.length - 1) && args[i + 1] && args[i + 1] !~ PARAM_RE
213             remains << args[i + 1] unless yield "-#{a}", args[i + 1]
214             i += 1 # once more below
215           else
216             yield "-#{a}", nil
217           end
218         end
219         i += 1
220       else
221         remains << args[i]
222         i += 1
223       end
224     end
225     remains
226   end
227
228   def parse cmdline #:nodoc:
229     vals = {}
230     required = {}
231     found = {}
232
233     opt :version, "Print version and exit" if @version unless @specs[:version] || @long["version"]
234     opt :help, "Show this message" unless @specs[:help] || @long["help"]
235
236     @specs.each do |sym, opts|
237       required[sym] = true if opts[:required]
238       vals[sym] = opts[:default]
239     end
240
241     ## resolve symbols
242     args = []
243     @leftovers = each_arg cmdline do |arg, param|
244       sym = 
245         case arg
246         when /^-([^-])$/
247           @short[$1]
248         when /^--([^-]\S*)$/
249           @long[$1]
250         else
251           raise CommandlineError, "invalid argument syntax: '#{arg}'"
252         end
253       raise CommandlineError, "unknown argument '#{arg}'" unless sym
254       raise CommandlineError, "option '#{arg}' specified multiple times" if found[sym]
255       args << [sym, arg, param]
256       found[sym] = true
257
258       @specs[sym][:type] != :flag # take params on all except flags
259     end
260
261     ## check for version and help args
262     raise VersionNeeded if args.any? { |sym, *a| sym == :version }
263     raise HelpNeeded if args.any? { |sym, *a| sym == :help }
264
265     ## check constraint satisfaction
266     @constraints.each do |type, syms|
267       constraint_sym = syms.find { |sym| found[sym] }
268       next unless constraint_sym
269
270       case type
271       when :depends
272         syms.each { |sym| raise CommandlineError, "--#{@long[constraint_sym]} requires --#{@long[sym]}" unless found[sym] }
273       when :conflicts
274         syms.each { |sym| raise CommandlineError, "--#{@long[constraint_sym]} conflicts with --#{@long[sym]}" if found[sym] && sym != constraint_sym }
275       end
276     end
277
278     raise CommandlineError, "option '#{required.keys.first}' must be specified" if required.any? { |sym, x| !found[sym] }
279
280     ## parse parameters
281     args.each do |sym, arg, param|
282       opts = @specs[sym]
283
284       raise CommandlineError, "option '#{arg}' needs a parameter" unless param || opts[:type] == :flag
285
286       case opts[:type]
287       when :flag
288         vals[sym] = !opts[:default]
289       when :int
290         raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^\d+$/
291         vals[sym] = param.to_i
292       when :float
293         raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE
294         vals[sym] = param.to_f
295       when :string
296         vals[sym] = param.to_s
297       end
298     end
299
300     vals
301   end
302
303   def width #:nodoc:
304     @width ||= 
305       begin
306         require 'curses'
307         Curses::init_screen
308         x = Curses::cols
309         Curses::close_screen
310         x
311       rescue Exception
312         80
313       end
314   end
315
316   ## Print the help message to 'stream'.
317   def educate stream=$stdout
318     width # just calculate it now; otherwise we have to be careful not to
319           # call this unless the cursor's at the beginning of a line.
320
321     left = {}
322     @specs.each do |name, spec| 
323       left[name] = "--#{spec[:long]}" +
324         (spec[:short] ? ", -#{spec[:short]}" : "") +
325         case spec[:type]
326         when :flag
327           ""
328         when :int
329           " <i>"
330         when :string
331           " <s>"
332         when :float
333           " <f>"
334         end
335     end
336
337     leftcol_width = left.values.map { |s| s.length }.max || 0
338     rightcol_start = leftcol_width + 6 # spaces
339
340     unless @order.size > 0 && @order.first.first == :text
341       stream.puts "#@version\n" if @version
342       stream.puts "Options:"
343     end
344
345     @order.each do |what, opt|
346       if what == :text
347         stream.puts wrap(opt)
348         next
349       end
350
351       spec = @specs[opt]
352       stream.printf "  %#{leftcol_width}s:   ", left[opt]
353       desc = spec[:desc] + 
354         if spec[:default]
355           if spec[:desc] =~ /\.$/
356             " (Default: #{spec[:default]})"
357           else
358             " (default: #{spec[:default]})"
359           end
360         else
361           ""
362         end
363       stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
364     end
365   end
366
367   def wrap_line str, opts={} # :nodoc:
368     prefix = opts[:prefix] || 0
369     width = opts[:width] || (self.width - 1)
370     start = 0
371     ret = []
372     until start > str.length
373       nextt = 
374         if start + width >= str.length
375           str.length
376         else
377           x = str.rindex(/\s/, start + width)
378           x = str.index(/\s/, start) if x && x < start
379           x || str.length
380         end
381       ret << (ret.empty? ? "" : " " * prefix) + str[start ... nextt]
382       start = nextt + 1
383     end
384     ret
385   end
386
387   def wrap str, opts={} # :nodoc:
388     if str == ""
389       [""]
390     else
391       str.split("\n").map { |s| wrap_line s, opts }.flatten
392     end
393   end
394
395   ## instance_eval but with ability to handle block arguments
396   ## thanks to why: http://redhanded.hobix.com/inspect/aBlockCostume.html
397   def cloaker &b #:nodoc:
398     (class << self; self; end).class_eval do
399       define_method :cloaker_, &b
400       meth = instance_method :cloaker_
401       remove_method :cloaker_
402       meth
403     end
404   end
405 end
406
407 ## The top-level entry method into Trollop. Creates a Parser object,
408 ## passes the block to it, then parses ARGV with it, handling any
409 ## errors or requests for help or version information appropriately
410 ## (and then exiting). Modifies ARGV in place. Returns a hash of
411 ## option values.
412 ##
413 ## The block passed in should contain one or more calls to #opt
414 ## (Parser#opt), one or more calls to text (Parser#text), and
415 ## probably a call to version (Parser#version).
416 ##
417 ## See the synopsis in README.txt for examples.
418 def options *a, &b
419   @p = Parser.new(*a, &b)
420   begin
421     vals = @p.parse ARGV
422     ARGV.clear
423     @p.leftovers.each { |l| ARGV << l }
424     vals
425   rescue CommandlineError => e
426     $stderr.puts "Error: #{e.message}."
427     $stderr.puts "Try --help for help."
428     exit(-1)
429   rescue HelpNeeded
430     @p.educate
431     exit
432   rescue VersionNeeded
433     puts @p.version
434     exit
435   end
436 end
437
438 ## Informs the user that their usage of 'arg' was wrong, as detailed by
439 ## 'msg', and dies. Example:
440 ##
441 ##   options do
442 ##     opt :volume, :default => 0.0
443 ##   end
444 ##
445 ##   die :volume, "too loud" if opts[:volume] > 10.0
446 ##   die :volume, "too soft" if opts[:volume] < 0.1
447 ##
448 ## In the one-argument case, simply print that message, a notice
449 ## about -h, and die. Example:
450 ##
451 ##   options do
452 ##     opt :whatever # ...
453 ##   end
454 ##
455 ##   Trollop::die "need at least one filename" if ARGV.empty?
456 def die arg, msg=nil
457   if msg
458     $stderr.puts "Error: argument --#{@p.specs[arg][:long]} #{msg}."
459   else
460     $stderr.puts "Error: #{arg}."
461   end
462   $stderr.puts "Try --help for help."
463   exit(-1)
464 end
465
466 module_function :options, :die
467
468 end # module