class Optimist::Parser
# Optimist::with_standard_exception_handling.
# and consider calling it from within
# argument-parsing logic), call #parse to actually produce the output hash,
# If you want to instantiate this class yourself (for more complicated
#
# typically be called.
# #opt, #banner and #version, #depends, and #conflicts methods will
# will be handled internally by Optimist::options. In this case, only the
# The commandline parser. In typical usage, the methods in this class
def self.register(lookup, klass)
def self.register(lookup, klass) @registry[lookup.to_sym] = klass end
def self.registry_getopttype(type)
# Gets the class from the registry.
def self.registry_getopttype(type) return nil unless type if type.respond_to?(:name) type = type.name lookup = type.downcase.to_sym else lookup = type.to_sym end raise ArgumentError, "Unsupported argument type '#{type}', registry lookup '#{lookup}'" unless @registry.has_key?(lookup) return @registry[lookup].new end
def banner(s)
# Adds text to the help display. Can be interspersed with calls to
def banner(s) @order << [:text, s] end
def cloaker(&b)
# instance_eval but with ability to handle block arguments
def cloaker(&b) (class << self; self; end).class_eval do define_method :cloaker_, &b meth = instance_method :cloaker_ remove_method :cloaker_ meth end end
def collect_argument_parameters(args, start_at)
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 conflicts(*syms)
def conflicts(*syms) syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] } @constraints << [:conflicts, syms] end
def depends(*syms)
# undirected (i.e., mutual) dependencies. Directed dependencies are
# Marks two (or more!) options as requiring each other. Only handles
def depends(*syms) syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] } @constraints << [:depends, syms] end
def die(arg, msg = nil, error_code = nil)
def die(arg, msg = nil, error_code = nil) msg, error_code = nil, msg if msg.kind_of?(Integer) if msg $stderr.puts "Error: argument --#{@specs[arg].long} #{msg}." else $stderr.puts "Error: #{arg}." end if @educate_on_error $stderr.puts educate $stderr else $stderr.puts "Try --help for help." end exit(error_code || -1) end
def each_arg(args)
def each_arg(args) remains = [] i = 0 until i >= args.length return remains += args[i..-1] if @stop_words.member? args[i] case args[i] when /^--$/ # arg terminator return remains += args[(i + 1)..-1] when /^--(\S+?)=(.*)$/ # long argument with equals num_params_taken = yield "--#{$1}", [$2] if num_params_taken.nil? remains << args[i] if @stop_on_unknown return remains += args[i + 1..-1] end end i += 1 when /^--(\S+)$/ # long argument params = collect_argument_parameters(args, i + 1) num_params_taken = yield args[i], params if num_params_taken.nil? remains << args[i] if @stop_on_unknown return remains += args[i + 1..-1] end else i += num_params_taken end i += 1 when /^-(\S+)$/ # one or more short arguments short_remaining = "" shortargs = $1.split(//) shortargs.each_with_index do |a, j| if j == (shortargs.length - 1) params = collect_argument_parameters(args, i + 1) num_params_taken = yield "-#{a}", params unless num_params_taken short_remaining << a if @stop_on_unknown remains << "-#{short_remaining}" return remains += args[i + 1..-1] end else i += num_params_taken end else unless yield "-#{a}", [] short_remaining << a if @stop_on_unknown short_remaining += shortargs[j + 1..-1].join remains << "-#{short_remaining}" return remains += args[i + 1..-1] end end end end unless short_remaining.empty? remains << "-#{short_remaining}" end i += 1 else if @stop_on_unknown return remains += args[i..-1] else remains << args[i] i += 1 end end end remains end
def educate(stream = $stdout)
def educate(stream = $stdout) width # hack: 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 { |name, spec| left[name] = spec.educate } leftcol_width = left.values.map(&:length).max || 0 rightcol_start = leftcol_width + 6 # spaces unless @order.size > 0 && @order.first.first == :text command_name = File.basename($0).gsub(/\.[^.]+$/, '') stream.puts "Usage: #{command_name} #{@usage}\n" if @usage stream.puts "#{@synopsis}\n" if @synopsis stream.puts if @usage || @synopsis 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.description_with_default stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start) end end
def educate_on_error
# Instead of displaying "Try --help for help." on an error
def educate_on_error @educate_on_error = true end
def either(*syms)
def either(*syms) syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] } @constraints << [:conflicts, syms] @constraints << [:either, syms] end
def initialize(*a, &b)
def initialize(*a, &b) @version = nil @leftovers = [] @specs = {} @long = {} @short = {} @order = [] @constraints = [] @stop_words = [] @stop_on_unknown = false @educate_on_error = false @synopsis = nil @usage = nil # instance_eval(&b) if b # can't take arguments cloaker(&b).bind(self).call(*a) if b end
def legacy_width
def legacy_width # Support for older Rubies where io/console is not available `tput cols`.to_i rescue Errno::ENOENT 80 end
def method_missing(m, *_args)
def method_missing(m, *_args) self[m] || self[m.to_s] end
def opt(name, desc = "", opts = {}, &b)
def opt(name, desc = "", opts = {}, &b) opts[:callback] ||= b if block_given? opts[:desc] ||= desc o = Option.create(name, desc, opts) raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? o.name raise ArgumentError, "long option name #{o.long.inspect} is already taken; please specify a (different) :long" if @long[o.long] raise ArgumentError, "short option name #{o.short.inspect} is already taken; please specify a (different) :short" if @short[o.short] @long[o.long] = o.name @short[o.short] = o.name if o.short? @specs[o.name] = o @order << [:opt, o.name] end
def parse(cmdline = ARGV)
#
# but you can call it directly if you need more control.
# Parses the commandline. Typically called by Optimist::options,
def parse(cmdline = ARGV) vals = {} required = {} opt :version, "Print version and exit" if @version && ! (@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| ## handle --no- forms arg, negative_given = if arg =~ /^--no-([^-]\S*)$/ ["--#{$1}", true] else [arg, false] end sym = case arg when /^-([^-])$/ then @short[$1] when /^--([^-]\S*)$/ then @long[$1] || @long["no-#{$1}"] else raise CommandlineError, "invalid argument syntax: '#{arg}'" end sym = nil if arg =~ /--no-/ # explicitly invalidate --no-no- arguments next nil if ignore_invalid_options && !sym 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][:negative_given] = negative_given given_args[sym][:params] ||= [] # The block returns the number of parameters taken. num_params_taken = 0 unless params.empty? if @specs[sym].single_arg? given_args[sym][:params] << params[0, 1] # take the first parameter num_params_taken = 1 elsif @specs[sym].multi_arg? 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] } case type when :depends next unless constraint_sym syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym].long} requires --#{@specs[sym].long}" unless given_args.include? sym } when :conflicts next unless constraint_sym syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym].long} conflicts with --#{@specs[sym].long}" if given_args.include?(sym) && (sym != constraint_sym) } when :either raise CommandlineError, "one of #{syms.map { |sym| "--#{@specs[sym].long}" }.join(', ') } is required" if (syms & given_args.keys).size != 1 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, params, negative_given = given_data.values_at :arg, :params, :negative_given opts = @specs[sym] if params.empty? && !opts.flag? raise CommandlineError, "option '#{arg}' needs a parameter" unless opts.default params << (opts.array_default? ? opts.default.clone : [opts.default]) end vals["#{sym}_given".intern] = true # mark argument as specified on the commandline vals[sym] = opts.parse(params, negative_given) if opts.single_arg? if opts.multi? # multiple options, each with a single parameter vals[sym] = vals[sym].map { |p| p[0] } else # single parameter vals[sym] = vals[sym][0][0] end elsif opts.multi_arg? && !opts.multi? vals[sym] = vals[sym][0] # single option, with multiple parameters end # else: multiple options, with multiple parameters opts.callback.call(vals[sym]) if opts.callback 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 resolve_default_short_options!
def resolve_default_short_options! @order.each do |type, name| opts = @specs[name] next if type != :opt || 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 stop_on(*words)
# invocation would then be used to parse subcommand options, after
# would be set to the list of subcommands. A subsequent Optimist
# A typical use case would be for subcommand support, where these
#
# intact.
# parsed as usual, and options to the right of the word are left
# encountered, such that any options to the left of the word are
# Defines a set of words which cause parsing to terminate when
def stop_on(*words) @stop_words = [*words].flatten end
def stop_on_unknown
# cases where you don't know the set of subcommands ahead of time,
# (unless it is a parameter for an argument). This is useful for
# Similar to #stop_on, but stops on any unknown word when encountered
def stop_on_unknown @stop_on_unknown = true end
def synopsis(s = nil)
# Adds a synopsis (command summary description) right below the
def synopsis(s = nil) s ? @synopsis = s : @synopsis end
def usage(s = nil)
# first line in the help (educate) output and ending in two new
# Sets the usage string. If set the message will be printed as the
def usage(s = nil) s ? @usage = s : @usage end
def version(s = nil)
# on the commandline. Should probably be of the form "
# Sets the version string. If set, the user can request the version
def version(s = nil) s ? @version = s : @version end
def width #:nodoc:
def width #:nodoc: @width ||= if $stdout.tty? begin require 'io/console' w = IO.console.winsize.last w.to_i > 0 ? w : 80 rescue LoadError, NoMethodError, Errno::ENOTTY, Errno::EBADF, Errno::EINVAL legacy_width end else 80 end end
def wrap(str, opts = {}) # :nodoc:
def wrap(str, opts = {}) # :nodoc: if str == "" [""] else inner = false str.split("\n").map do |s| line = wrap_line s, opts.merge(:inner => inner) inner = true line end.flatten end end
def wrap_line(str, opts = {})
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? && !opts[:inner]) ? "" : " " * prefix) + str[start...nextt] start = nextt + 1 end ret end