class Trollop::Parser

The commandline parser. In typical usage, the methods in this class will be handled internally by Trollop::options. In this case, only the opt, banner and version, depends, and conflicts methods will typically be called.

If you want to instantiate this class yourself (for more complicated argument-parsing logic), call parse to actually produce the output hash, and consider calling it from within Trollop::with_standard_exception_handling.

Constants

FLAG_TYPES

The set of values that indicate a flag option when passed as the :type parameter of opt.

MULTI_ARG_TYPES

The set of values that indicate a multiple-parameter option (i.e., that takes multiple space-separated values on the commandline) when passed as the :type parameter of opt.

SINGLE_ARG_TYPES

The set of values that indicate a single-parameter (normal) option when passed as the :type parameter of opt.

A value of io corresponds to a readable IO resource, including a filename, URI, or the strings 'stdin' or '-'.

TYPES

The complete set of legal values for the :type parameter of opt.

Attributes

leftovers[R]

The values from the commandline that were not interpreted by parse.

order[R]
specs[R]

The complete configuration hashes for each option. (Mainly useful for testing.)

Public Class Methods

new(*a, &b) click to toggle source

Initializes the parser, and instance-evaluates any block given.

# File lib/convoy/trollop.rb, line 75
def initialize *a, &b
    @version         = nil
    @leftovers       = []
    @specs           = {}
    @long            = {}
    @short           = {}
    @order           = []
    @constraints     = []
    @stop_words      = []
    @stop_on_unknown = false
    @help_formatter  = nil

    #instance_eval(&b) if b # can't take arguments
    cloaker(&b).bind(self).call(*a) if b
end

Public Instance Methods

banner(s;) click to toggle source

Adds text to the help display. Can be interspersed with calls to opt to build a multi-section help page.

Also aliased as: text
conflicts(*syms) click to toggle source

Marks two (or more!) options as conflicting.

# File lib/convoy/trollop.rb, line 299
def conflicts *syms
    syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
    @constraints << [:conflicts, syms]
end
current_help_formatter() click to toggle source
# File lib/convoy/trollop.rb, line 286
def current_help_formatter
    @help_formatter
end
depends(*syms) click to toggle source

Marks two (or more!) options as requiring each other. Only handles undirected (i.e., mutual) dependencies. Directed dependencies are better modeled with Trollop::die.

# File lib/convoy/trollop.rb, line 293
def depends *syms
    syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
    @constraints << [:depends, syms]
end
die(arg, msg) click to toggle source

The per-parser version of Trollop::die (see that for documentation).

# File lib/convoy/trollop.rb, line 597
def die arg, msg
    if msg
        $stderr.puts "Error: argument --#{@specs[arg][:long]} #{msg}."
    else
        $stderr.puts "Error: #{arg}."
    end
    $stderr.puts "Try --help for help."
    exit(1)
end
educate(stream=$stdout) click to toggle source

Print the help message to stream.

# File lib/convoy/trollop.rb, line 494
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 do |name, spec|
        left[name] = "--#{spec[:long]}" +
            (spec[:type] == :flag && spec[:default] ? ", --no-#{spec[:long]}" : "") +
            (spec[:short] && spec[:short] != :none ? ", -#{spec[:short]}" : "") +
            case spec[:type]
                when :flag;
                    ""
                when :int;
                    " <i>"
                when :ints;
                    " <i+>"
                when :string;
                    " <s>"
                when :strings;
                    " <s+>"
                when :float;
                    " <f>"
                when :floats;
                    " <f+>"
                when :io;
                    " <filename/uri>"
                when :ios;
                    " <filename/uri+>"
                when :date;
                    " <date>"
                when :dates;
                    " <date+>"
            end
    end

    leftcol_width  = left.values.map { |s| s.length }.max || 0
    rightcol_start = leftcol_width + 6 # spaces

    unless @order.size > 0 && @order.first.first == :text
        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[:desc] + begin
            default_s = case spec[:default]
                            when $stdout;
                                "<stdout>"
                            when $stdin;
                                "<stdin>"
                            when $stderr;
                                "<stderr>"
                            when Array
                                spec[:default].join(", ")
                            else
                                spec[:default].to_s
                        end

            if spec[:default]
                if spec[:desc] =~ /\.$/
                    " (Default: #{default_s})"
                else
                    " (default: #{default_s})"
                end
            else
                ""
            end
        end
        stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
    end
end
help_formatter(formatter) click to toggle source
# File lib/convoy/trollop.rb, line 282
def help_formatter(formatter)
    @help_formatter = formatter
end
opt(name, desc="", opts={}) click to toggle source

Define an option. name is the option name, a unique identifier for the option that you will use internally, which should be a symbol or a string. desc is a string description which will be displayed in help messages.

Takes the following optional arguments:

:long

Specify the long form of the argument, i.e. the form with two dashes. If unspecified, will be automatically derived based on the argument name by turning the name option into a string, and replacing any _'s by -'s.

:short

Specify the short form of the argument, i.e. the form with one dash. If unspecified, will be automatically derived from name.

:type

Require that the argument take a parameter or parameters of type type. For a single parameter, the value can be a member of SINGLE_ARG_TYPES, or a corresponding Ruby class (e.g. Integer for :int). For multiple-argument parameters, the value can be any member of MULTI_ARG_TYPES constant. If unset, the default argument type is :flag, meaning that the argument does not take a parameter. The specification of :type is not necessary if a :default is given.

:default

Set the default value for an argument. Without a default value, the hash returned by parse (and thus Trollop::options) will have a nil value for this key unless the argument is given on the commandline. The argument type is derived automatically from the class of the default value given, so specifying a :type is not necessary if a :default is given. (But see below for an important caveat when :multi: is specified too.) If the argument is a flag, and the default is set to true, then if it is specified on the the commandline the value will be false.

:required

If set to true, the argument must be provided on the commandline.

:multi

If set to true, allows multiple occurrences of the option on the commandline. Otherwise, only a single instance of the option is allowed. (Note that this is different from taking multiple parameters. See below.)

Note that there are two types of argument multiplicity: an argument can take multiple values, e.g. “–arg 1 2 3”. An argument can also be allowed to occur multiple times, e.g. “–arg 1 –arg 2”.

Arguments that take multiple values should have a :type parameter drawn from MULTI_ARG_TYPES (e.g. :strings), or a :default: value of an array of the correct type (e.g. [String]). The value of this argument will be an array of the parameters on the commandline.

Arguments that can occur multiple times should be marked with :multi => true. The value of this argument will also be an array. In contrast with regular non-multi options, if not specified on the commandline, the default value will be [], not nil.

These two attributes can be combined (e.g. :type => :strings, :multi => true), in which case the value of the argument will be an array of arrays.

There's one ambiguous case to be aware of: when :multi: is true and a :default is set to an array (of something), it's ambiguous whether this is a multi-value argument as well as a multi-occurrence argument. In thise case, Trollop assumes that it's not a multi-value argument. If you want a multi-value, multi-occurrence argument with a default value, you must specify :type as well.

# File lib/convoy/trollop.rb, line 131
def opt name, desc="", opts={}
    raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name

    ## fill in :type
    opts[:type] = # normalize
        case opts[:type]
            when :boolean, :bool;
                :flag
            when :integer;
                :int
            when :integers;
                :ints
            when :double;
                :float
            when :doubles;
                :floats
            when Class
                case opts[:type].name
                    when 'TrueClass', 'FalseClass';
                        :flag
                    when 'String';
                        :string
                    when 'Integer';
                        :int
                    when 'Float';
                        :float
                    when 'IO';
                        :io
                    when 'Date';
                        :date
                    else
                        raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'"
                end
            when nil;
                nil
            else
                raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type])
                opts[:type]
        end

    ## for options with :multi => true, an array default doesn't imply
    ## a multi-valued argument. for that you have to specify a :type
    ## as well. (this is how we disambiguate an ambiguous situation;
    ## see the docs for Parser#opt for details.)
    disambiguated_default = if opts[:multi] && opts[:default].is_a?(Array) && !opts[:type]
                                opts[:default].first
                            else
                                opts[:default]
                            end

    type_from_default =
        case disambiguated_default
            when Integer;
                :int
            when Numeric;
                :float
            when TrueClass, FalseClass;
                :flag
            when String;
                :string
            when IO;
                :io
            when Date;
                :date
            when Array
                if opts[:default].empty?
                    raise ArgumentError, "multiple argument type cannot be deduced from an empty array for '#{opts[:default][0].class.name}'"
                end
                case opts[:default][0] # the first element determines the types
                    when Integer;
                        :ints
                    when Numeric;
                        :floats
                    when String;
                        :strings
                    when IO;
                        :ios
                    when Date;
                        :dates
                    else
                        raise ArgumentError, "unsupported multiple argument type '#{opts[:default][0].class.name}'"
                end
            when nil;
                nil
            else
                raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'"
        end

    raise ArgumentError, ":type specification and default type don't match (default type is #{type_from_default})" if opts[:type] && type_from_default && opts[:type] != type_from_default

    opts[:type] = opts[:type] || type_from_default || :flag

    ## fill in :long
    opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-")
    opts[:long] = case opts[:long]
                      when /^--([^-].*)$/;
                          $1
                      when /^[^-]/;
                          opts[:long]
                      else
                          ; raise ArgumentError, "invalid long option name #{opts[:long].inspect}"
                  end
    raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]]

    ## fill in :short
    opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none
    opts[:short] = case opts[:short]
                       when /^-(.)$/;
                           $1
                       when nil, :none, /^.$/;
                           opts[:short]
                       else
                           raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'"
                   end

    if opts[:short]
        raise ArgumentError, "short option name #{opts[:short].inspect} is already taken; please specify a (different) :short" if @short[opts[:short]]
        raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ INVALID_SHORT_ARG_REGEX
    end

    ## fill in :default for flags
    opts[:default] = false if opts[:type] == :flag && opts[:default].nil?

    ## autobox :default for :multi (multi-occurrence) arguments
    opts[:default] = [opts[:default]] if opts[:default] && opts[:multi] && !opts[:default].is_a?(Array)

    ## fill in :multi
    opts[:multi] ||= false

    opts[:desc]          ||= desc
    @long[opts[:long]]   = name
    @short[opts[:short]] = name if opts[:short] && opts[:short] != :none
    @specs[name]         = opts
    @order << [:opt, name]
end
parse(cmdline=ARGV) click to toggle source

Parses the commandline. Typically called by Trollop::options, but you can call it directly if you need more control.

throws CommandlineError, HelpNeeded, and VersionNeeded exceptions.

# File lib/convoy/trollop.rb, line 329
def parse cmdline=ARGV
    vals     = {}
    required = {}

    opt :version, 'Prints version and exits' if @version unless @specs[:version] || @long['version']
    opt :help, "\x1B[38;5;222mShows help message for current command\x1B[0m" 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 /^-([^-])$/;
                      @short[$1]
                  when /^--([^-]\S*)$/;
                      @long[$1] || @long["no-#{$1}"]
                  else
                      #  raise CommandlineError, "invalid argument syntax: '#{arg}'"
                      puts "\n    \x1B[48;5;196m Error \x1B[0m \xe2\x86\x92 Invalid argument syntax: #{arg}\n\n"
                      exit
              end

        sym = nil if arg =~ /--no-/ # explicitly invalidate --no-no- arguments

        # raise CommandlineError, "unknown argument '#{arg}'" unless sym
        unless sym
            puts "\n    \x1B[48;5;196m Error \x1B[0m \xe2\x86\x92 Unknown flag: #{arg}\n\n"
            exit
        end

        if given_args.include?(sym) && !@specs[sym][:multi]
            # raise CommandlineError, "option '#{arg}' specified multiple times"
            puts "\n    \x1B[48;5;196m Error \x1B[0m \xe2\x86\x92 Flag specified multiple times: #{arg}\n\n"
            exit
        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.nil?
            if SINGLE_ARG_TYPES.include?(@specs[sym][:type])
                given_args[sym][:params] << params[0, 1] # take the first parameter
                num_params_taken = 1
            elsif MULTI_ARG_TYPES.include?(@specs[sym][:type])
                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] }
        next unless constraint_sym

        case type
            when :depends
                syms.each { |sym|
                    Blufin::Terminal::error('Missing constraint', "--#{@specs[constraint_sym][:long]} requires --#{@specs[sym][:long]}") unless given_args.include? sym
                }
            when :conflicts
                syms.each { |sym|
                    Blufin::Terminal::error('Conflicting constraint', "--#{@specs[constraint_sym][:long]} conflicts with --#{@specs[sym][:long]}") if given_args.include?(sym) && (sym != constraint_sym)
                }
        end
    end

    required.each do |sym, val|
        unless given_args.include? sym
            Blufin::Terminal::error('Missing option', "option --#{@specs[sym][:long]} must be specified")
        end
    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[:type] != :flag
            Blufin::Terminal::error('Missing parameter', "option '#{arg}' needs a parameter")
        end

        vals["#{sym}_given".intern] = true # mark argument as specified on the commandline

        case opts[:type]
            when :flag
                vals[sym] = (sym.to_s =~ /^no_/ ? negative_given : !negative_given)
            when :int, :ints
                vals[sym] = params.map { |pg| pg.map { |p| parse_integer_parameter p, arg } }
            when :float, :floats
                vals[sym] = params.map { |pg| pg.map { |p| parse_float_parameter p, arg } }
            when :string, :strings
                vals[sym] = params.map { |pg| pg.map { |p| p.to_s } }
            when :io, :ios
                vals[sym] = params.map { |pg| pg.map { |p| parse_io_parameter p, arg } }
            when :date, :dates
                vals[sym] = params.map { |pg| pg.map { |p| parse_date_parameter p, arg } }
        end

        if SINGLE_ARG_TYPES.include?(opts[:type])
            unless opts[:multi] # single parameter
                vals[sym] = vals[sym][0][0]
            else # multiple options, each with a single parameter
                vals[sym] = vals[sym].map { |p| p[0] }
            end
        elsif MULTI_ARG_TYPES.include?(opts[:type]) && !opts[:multi]
            vals[sym] = vals[sym][0] # single option, with multiple parameters
        end
        # else: multiple options, with multiple parameters
    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
stop_on(*words) click to toggle source

Defines a set of words which cause parsing to terminate when encountered, such that any options to the left of the word are parsed as usual, and options to the right of the word are left intact.

A typical use case would be for subcommand support, where these would be set to the list of subcommands. A subsequent Trollop invocation would then be used to parse subcommand options, after shifting the subcommand off of ARGV.

# File lib/convoy/trollop.rb, line 313
def stop_on *words
    @stop_words = [*words].flatten
end
stop_on_unknown() click to toggle source

Similar to stop_on, but stops on any unknown word when encountered (unless it is a parameter for an argument). This is useful for cases where you don't know the set of subcommands ahead of time, i.e., without first parsing the global options.

# File lib/convoy/trollop.rb, line 321
def stop_on_unknown
    @stop_on_unknown = true
end
text(s;)
Alias for: banner
version(s=nil;) click to toggle source

Sets the version string. If set, the user can request the version on the commandline. Should probably be of the form “<program name> <version number>”.

# File lib/convoy/trollop.rb, line 270
def version s=nil;
    @version = s if s; @version
end

Private Instance Methods

cloaker(&b) click to toggle source

instance_eval but with ability to handle block arguments thanks to _why: redhanded.hobix.com/inspect/aBlockCostume.html

# File lib/convoy/trollop.rb, line 751
def cloaker &b
    (
    class << self;
        self;
    end).class_eval do
        define_method :cloaker_, &b
        meth = instance_method :cloaker_
        remove_method :cloaker_
        meth
    end
end
collect_argument_parameters(args, start_at) click to toggle source
# File lib/convoy/trollop.rb, line 705
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
each_arg(args) { |"--#{$1}", [$2]| ... } click to toggle source

yield successive arg, parameter pairs

# File lib/convoy/trollop.rb, line 610
def each_arg args
    remains = []
    i       = 0

    until i >= args.length
        if @stop_words.member? args[i]
            remains += args[i .. -1]
            return remains
        end
        case args[i]
            when /^--$/ # arg terminator
                remains += args[(i + 1) .. -1]
                return remains
            when /^--(\S+?)=(.*)$/ # long argument with equals
                yield "--#{$1}", [$2]
                i += 1
            when /^--(\S+)$/ # long argument
                params = collect_argument_parameters(args, i + 1)
                unless params.empty?
                    num_params_taken = yield args[i], params
                    unless num_params_taken
                        if @stop_on_unknown
                            remains += args[i + 1 .. -1]
                            return remains
                        else
                            remains += params
                        end
                    end
                    i += 1 + num_params_taken
                else # long argument no parameter
                    yield args[i], nil
                    i += 1
                end
            when /^-(\S+)$/ # one or more short arguments
                shortargs = $1.split(//)
                shortargs.each_with_index do |a, j|
                    if j == (shortargs.length - 1)
                        params = collect_argument_parameters(args, i + 1)
                        unless params.empty?
                            num_params_taken = yield "-#{a}", params
                            unless num_params_taken
                                if @stop_on_unknown
                                    remains += args[i + 1 .. -1]
                                    return remains
                                else
                                    remains += params
                                end
                            end
                            i += 1 + num_params_taken
                        else # argument no parameter
                            yield "-#{a}", nil
                            i += 1
                        end
                    else
                        yield "-#{a}", nil
                    end
                end
            else
                if @stop_on_unknown
                    remains += args[i .. -1]
                    return remains
                else
                    remains << args[i]
                    i += 1
                end
        end
    end

    remains
end
parse_float_parameter(param, arg) click to toggle source
# File lib/convoy/trollop.rb, line 686
def parse_float_parameter param, arg
    raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE
    param.to_f
end
parse_integer_parameter(param, arg) click to toggle source
# File lib/convoy/trollop.rb, line 681
def parse_integer_parameter param, arg
    raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^\d+$/
    param.to_i
end
parse_io_parameter(param, arg) click to toggle source
# File lib/convoy/trollop.rb, line 691
def parse_io_parameter param, arg
    case param
        when /^(stdin|-)$/i;
            $stdin
        else
            require 'open-uri'
            begin
                open param
            rescue SystemCallError => e
                raise CommandlineError, "file or url for option '#{arg}' cannot be opened: #{e.message}"
            end
    end
end
resolve_default_short_options!() click to toggle source
# File lib/convoy/trollop.rb, line 715
def resolve_default_short_options!
    @order.each do |type, name|
        next unless type == :opt
        opts = @specs[name]
        next if 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
wrap_line(str, opts={}) click to toggle source
# File lib/convoy/trollop.rb, line 729
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? ? "" : " " * prefix) + str[start ... nextt]
        start = nextt + 1
    end
    ret
end