Skip to content

Commit

Permalink
Merge pull request #147 from akhoury6/permitted
Browse files Browse the repository at this point in the history
Using permitted: restricts the allowed values that a end-user inputs to a pre-defined list
  • Loading branch information
Fryguy authored May 1, 2024
2 parents ea2af09 + 72f88dd commit 3757b70
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 7 deletions.
47 changes: 40 additions & 7 deletions lib/optimist.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ def initialize(*a, &b)
## [+:default+] Set the default value for an argument. Without a default value, the hash returned by #parse (and thus Optimist::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.)
## [+:permitted+] Specify an Array of permitted values for an option. If the user provides a value outside this list, an error is thrown.
##
## 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
Expand Down Expand Up @@ -163,6 +164,7 @@ def opt(name, desc = "", opts = {}, &b)
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]
raise ArgumentError, "permitted values for option #{o.long.inspect} must be either nil or an array;" unless o.permitted.nil? or o.permitted.is_a? Array
@long[o.long] = o.name
@short[o.short] = o.name if o.short?
@specs[o.name] = o
Expand Down Expand Up @@ -380,6 +382,10 @@ def parse(cmdline = ARGV)
params << (opts.array_default? ? opts.default.clone : [opts.default])
end

params[0].each do |p|
raise CommandlineError, "option '#{arg}' only accepts one of: #{opts.permitted.join(', ')}" unless opts.permitted.include? p
end unless opts.permitted.nil?

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

vals[sym] = opts.parse(params, negative_given)
Expand Down Expand Up @@ -440,7 +446,7 @@ def educate(stream = $stdout)

spec = @specs[opt]
stream.printf " %-#{leftcol_width}s ", left[opt]
desc = spec.description_with_default
desc = spec.full_description

stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
end
Expand Down Expand Up @@ -625,7 +631,7 @@ def wrap_line(str, opts = {})

class Option

attr_accessor :name, :short, :long, :default
attr_accessor :name, :short, :long, :default, :permitted
attr_writer :multi_given

def initialize
Expand All @@ -635,6 +641,7 @@ def initialize
@multi_given = false
@hidden = false
@default = nil
@permitted = nil
@optshash = Hash.new()
end

Expand Down Expand Up @@ -679,9 +686,16 @@ def educate
(short? ? "-#{short}, " : " ") + "--#{long}" + type_format + (flag? && default ? ", --no-#{long}" : "")
end

## Format the educate-line description including the default-value(s)
def description_with_default
return desc unless default
## Format the educate-line description including the default and permitted value(s)
def full_description
desc_str = desc
desc_str += default_description_str(desc) if default
desc_str += permitted_description_str(desc) if permitted
desc_str
end

## Generate the default value string for the educate line
private def default_description_str str
default_s = case default
when $stdout then '<stdout>'
when $stdin then '<stdin>'
Expand All @@ -691,8 +705,23 @@ def description_with_default
else
default.to_s
end
defword = desc.end_with?('.') ? 'Default' : 'default'
return "#{desc} (#{defword}: #{default_s})"
defword = str.end_with?('.') ? 'Default' : 'default'
" (#{defword}: #{default_s})"
end

## Generate the permitted values string for the educate line
private def permitted_description_str str
permitted_s = permitted.map do |p|
case p
when $stdout then '<stdout>'
when $stdin then '<stdin>'
when $stderr then '<stderr>'
else
p.to_s
end
end.join(', ')
permword = str.end_with?('.') ? 'Permitted' : 'permitted'
" (#{permword}: #{permitted_s})"
end

## Provide a way to register symbol aliases to the Parser
Expand Down Expand Up @@ -731,8 +760,12 @@ def self.create(name, desc="", opts={}, settings={})
## fill in :default for flags
defvalue = opts[:default] || opt_inst.default

## fill in permitted values
permitted = opts[:permitted] || nil

## autobox :default for :multi (multi-occurrence) arguments
defvalue = [defvalue] if defvalue && multi_given && !defvalue.kind_of?(Array)
opt_inst.permitted = permitted
opt_inst.default = defvalue
opt_inst.name = name
opt_inst.opts = opts
Expand Down
11 changes: 11 additions & 0 deletions test/optimist/parser_educate_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,17 @@ def test_help_has_grammatical_default_text
assert help[1] =~ /Default/
assert help[2] =~ /default/
end

def test_help_has_grammatical_permitted_text
parser.opt :arg1, 'description with period.', :type => :strings, :permitted => %w(foo bar)
parser.opt :arg2, 'description without period', :type => :strings, :permitted => %w(foo bar)
sio = StringIO.new 'w'
parser.educate sio

help = sio.string.split "\n"
assert help[1] =~ /Permitted/
assert help[2] =~ /permitted/
end
############

private
Expand Down
8 changes: 8 additions & 0 deletions test/optimist/parser_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ def test_required_flags_are_required
assert_raises(CommandlineError) { @p.parse(%w(--arg2 --arg3)) }
end

def test_permitted_flags_filter_inputs
@p.opt "arg", "desc", :type => :strings, :permitted => %w(foo bar)

result = @p.parse(%w(--arg foo))
assert_equal ["foo"], result["arg"]
assert_raises(CommandlineError) { @p.parse(%w(--arg baz)) }
end

## flags that take an argument error unless given one
def test_argflags_demand_args
@p.opt "goodarg", "desc", :type => String
Expand Down

0 comments on commit 3757b70

Please sign in to comment.