lots of documentation improvements
[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.7.1"
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 the
30 ## methods #opt, #banner and #version, #depends, and #conflicts will
31 ## typically be called.
32 class Parser
33   ## The set of values specifiable as the :type parameter to #opt.
34   TYPES = [:flag, :boolean, :bool, :int, :integer, :string, :double, :float]
35
36   ## The values from the commandline that were not interpreted by #parse.
37   attr_reader :leftovers
38
39   ## The complete configuration hashes for each option. (Mainly useful
40   ## for testing.)
41   attr_reader :specs
42
43   ## Initializes the parser, and instance-evaluates any block given.
44   def initialize *a, &b
45     @version = nil
46     @leftovers = []
47     @specs = {}
48     @long = {}
49     @short = {}
50     @order = []
51     @constraints = []
52
53     #instance_eval(&b) if b # can't take arguments
54     cloaker(&b).bind(self).call(*a) if b
55   end
56
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:
61   ##
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
81   ##   commandline.
82   def opt name, desc="", opts={}
83     raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name
84
85     ## fill in :type
86     opts[:type] = 
87       case opts[:type]
88       when :flag, :boolean, :bool: :flag
89       when :int, :integer: :int
90       when :string: :string
91       when :double, :float: :float
92       when Class
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
96         when 'Integer': :int
97         when 'Float': :float
98         else
99           raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'"
100         end
101       when nil: nil
102       else
103         raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type])
104       end
105
106     type_from_default =
107       case opts[:default]
108       when Integer: :int
109       when Numeric: :float
110       when TrueClass, FalseClass: :flag
111       when String: :string
112       when nil: nil
113       else
114         raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'"
115       end
116
117     raise ArgumentError, ":type specification and default type don't match" if opts[:type] && type_from_default && opts[:type] != type_from_default
118
119     opts[:type] = (opts[:type] || type_from_default || :flag)
120
121     ## fill in :long
122     opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-")
123     opts[:long] =
124       case opts[:long]
125       when /^--([^-].*)$/
126         $1
127       when /^[^-]/
128         opts[:long]
129       else
130         raise ArgumentError, "invalid long option name #{opts[:long].inspect}"
131       end
132     raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]]
133
134     ## fill in :short
135     opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none
136     opts[:short] =
137       case opts[:short]
138       when nil
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
141         c
142       when /^-(.)$/
143         $1
144       when /^.$/
145         opts[:short]
146       when :none
147         nil
148       else
149         raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'"
150       end
151     if opts[:short]
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-]/
154     end
155
156     ## fill in :default for flags
157     opts[:default] = false if opts[:type] == :flag && opts[:default].nil?
158
159     opts[:desc] ||= desc
160     @long[opts[:long]] = name
161     @short[opts[:short]] = name if opts[:short]
162     @specs[name] = opts
163     @order << [:opt, name]
164   end
165
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
170
171   ## Adds text to the help display.
172   def banner s; @order << [:text, s] end
173   alias :text :banner
174
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.
178   def depends *syms
179     syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
180     @constraints << [:depends, syms]
181   end
182   
183   ## Marks two (or more!) options as conflicting.
184   def conflicts *syms
185     syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
186     @constraints << [:conflicts, syms]
187   end
188
189   ## yield successive arg, parameter pairs
190   def each_arg args # :nodoc:
191     remains = []
192     i = 0
193
194     until i >= args.length
195       case args[i]
196       when /^--$/ # arg terminator
197         remains += args[(i + 1) .. -1]
198         break
199       when /^--(\S+?)=(\S+)$/ # long argument with equals
200         yield "--#{$1}", $2
201         i += 1
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]
205           i += 2
206         else # long argument no parameter
207           yield args[i], nil
208           i += 1
209         end
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
216           else
217             yield "-#{a}", nil
218           end
219         end
220         i += 1
221       else
222         remains << args[i]
223         i += 1
224       end
225     end
226     remains
227   end
228
229   def parse cmdline #:nodoc:
230     vals = {}
231     required = {}
232     found = {}
233
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"]
236
237     @specs.each do |sym, opts|
238       required[sym] = true if opts[:required]
239       vals[sym] = opts[:default]
240     end
241
242     ## resolve symbols
243     args = []
244     @leftovers = each_arg cmdline do |arg, param|
245       sym = 
246         case arg
247         when /^-([^-])$/
248           @short[$1]
249         when /^--([^-]\S*)$/
250           @long[$1]
251         else
252           raise CommandlineError, "invalid argument syntax: '#{arg}'"
253         end
254       raise CommandlineError, "unknown argument '#{arg}'" unless sym
255       raise CommandlineError, "option '#{arg}' specified multiple times" if found[sym]
256       args << [sym, arg, param]
257       found[sym] = true
258
259       @specs[sym][:type] != :flag # take params on all except flags
260     end
261
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 }
265
266     ## check constraint satisfaction
267     @constraints.each do |type, syms|
268       constraint_sym = syms.find { |sym| found[sym] }
269       next unless constraint_sym
270
271       case type
272       when :depends
273         syms.each { |sym| raise CommandlineError, "--#{@long[constraint_sym]} requires --#{@long[sym]}" unless found[sym] }
274       when :conflicts
275         syms.each { |sym| raise CommandlineError, "--#{@long[constraint_sym]} conflicts with --#{@long[sym]}" if found[sym] && sym != constraint_sym }
276       end
277     end
278
279     required.each do |sym, val|
280       raise CommandlineError, "option '#{sym}' must be specified" unless found[sym]
281     end
282
283     ## parse parameters
284     args.each do |sym, arg, param|
285       opts = @specs[sym]
286
287       raise CommandlineError, "option '#{arg}' needs a parameter" unless param || opts[:type] == :flag
288
289       case opts[:type]
290       when :flag
291         vals[sym] = !opts[:default]
292       when :int
293         raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^\d+$/
294         vals[sym] = param.to_i
295       when :float
296         raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE
297         vals[sym] = param.to_f
298       when :string
299         vals[sym] = param.to_s
300       end
301     end
302
303     vals
304   end
305
306   def width #:nodoc:
307     @width ||= 
308       if $stdout.tty?
309         begin
310           require 'curses'
311           Curses::init_screen
312           x = Curses::cols
313           Curses::close_screen
314           x
315         rescue Exception
316           80
317         end
318       else
319         80
320       end
321   end
322
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.
327
328     left = {}
329     @specs.each do |name, spec| 
330       left[name] = "--#{spec[:long]}" +
331         (spec[:short] ? ", -#{spec[:short]}" : "") +
332         case spec[:type]
333         when :flag
334           ""
335         when :int
336           " <i>"
337         when :string
338           " <s>"
339         when :float
340           " <f>"
341         end
342     end
343
344     leftcol_width = left.values.map { |s| s.length }.max || 0
345     rightcol_start = leftcol_width + 6 # spaces
346
347     unless @order.size > 0 && @order.first.first == :text
348       stream.puts "#@version\n" if @version
349       stream.puts "Options:"
350     end
351
352     @order.each do |what, opt|
353       if what == :text
354         stream.puts wrap(opt)
355         next
356       end
357
358       spec = @specs[opt]
359       stream.printf "  %#{leftcol_width}s:   ", left[opt]
360       desc = spec[:desc] + 
361         if spec[:default]
362           if spec[:desc] =~ /\.$/
363             " (Default: #{spec[:default]})"
364           else
365             " (default: #{spec[:default]})"
366           end
367         else
368           ""
369         end
370       stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
371     end
372   end
373
374   def wrap_line str, opts={} # :nodoc:
375     prefix = opts[:prefix] || 0
376     width = opts[:width] || (self.width - 1)
377     start = 0
378     ret = []
379     until start > str.length
380       nextt = 
381         if start + width >= str.length
382           str.length
383         else
384           x = str.rindex(/\s/, start + width)
385           x = str.index(/\s/, start) if x && x < start
386           x || str.length
387         end
388       ret << (ret.empty? ? "" : " " * prefix) + str[start ... nextt]
389       start = nextt + 1
390     end
391     ret
392   end
393
394   def wrap str, opts={} # :nodoc:
395     if str == ""
396       [""]
397     else
398       str.split("\n").map { |s| wrap_line s, opts }.flatten
399     end
400   end
401
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_
409       meth
410     end
411   end
412 end
413
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
418 ## option values.
419 ##
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).
423 ##
424 ## See the synopsis in README.txt for examples.
425 def options *a, &b
426   @p = Parser.new(*a, &b)
427   begin
428     vals = @p.parse ARGV
429     ARGV.clear
430     @p.leftovers.each { |l| ARGV << l }
431     vals
432   rescue CommandlineError => e
433     $stderr.puts "Error: #{e.message}."
434     $stderr.puts "Try --help for help."
435     exit(-1)
436   rescue HelpNeeded
437     @p.educate
438     exit
439   rescue VersionNeeded
440     puts @p.version
441     exit
442   end
443 end
444
445 ## Informs the user that their usage of 'arg' was wrong, as detailed by
446 ## 'msg', and dies. Example:
447 ##
448 ##   options do
449 ##     opt :volume, :default => 0.0
450 ##   end
451 ##
452 ##   die :volume, "too loud" if opts[:volume] > 10.0
453 ##   die :volume, "too soft" if opts[:volume] < 0.1
454 ##
455 ## In the one-argument case, simply print that message, a notice
456 ## about -h, and die. Example:
457 ##
458 ##   options do
459 ##     opt :whatever # ...
460 ##   end
461 ##
462 ##   Trollop::die "need at least one filename" if ARGV.empty?
463 def die arg, msg=nil
464   if msg
465     $stderr.puts "Error: argument --#{@p.specs[arg][:long]} #{msg}."
466   else
467     $stderr.puts "Error: #{arg}."
468   end
469   $stderr.puts "Try --help for help."
470   exit(-1)
471 end
472
473 module_function :options, :die
474
475 end # module