-
Notifications
You must be signed in to change notification settings - Fork 36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add alt longname and multiple char support #151
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
#!/usr/bin/env ruby | ||
require_relative '../lib/optimist' | ||
|
||
opts = Optimist::options do | ||
# 'short:' can now take more than one short-option character | ||
# you can specify 'short:' as a string/symbol or an array of strings/symbols | ||
# 'alt:' adds additional long-opt choices (over the original name or the long: name) | ||
# you can specify 'alt:' as a string/symbol or an array of strings/symbols. | ||
# | ||
opt :concat, 'concatenate flag', short: ['-C', 'A'], alt: ['cat', '--append'] | ||
opt :array_len, 'set Array length', long: 'size', alt: 'length', type: Integer | ||
end | ||
|
||
p opts | ||
|
||
# $ ./alt_names.rb -h | ||
# Options: | ||
# -C, -A, --concat, --cat, --append concatenate flag | ||
# -s, --size, --length=<i> set Array length | ||
# -h, --help Show this message |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -70,8 +70,6 @@ def self.registry_getopttype(type) | |
return @registry[lookup].new | ||
end | ||
|
||
INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc: | ||
|
||
## The values from the commandline that were not interpreted by #parse. | ||
attr_reader :leftovers | ||
|
||
|
@@ -166,11 +164,19 @@ def opt(name, desc = "", opts = {}, &b) | |
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] | ||
|
||
o.long.names.each do |lng| | ||
raise ArgumentError, "long option name #{lng.inspect} is already taken; please specify a (different) :long/:alt" if @long[lng] | ||
@long[lng] = o.name | ||
end | ||
|
||
o.short.chars.each do |short| | ||
raise ArgumentError, "short option name #{short.inspect} is already taken; please specify a (different) :short" if @short[short] | ||
@short[short] = o.name | ||
end | ||
|
||
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 | ||
@order << [:opt, o.name] | ||
end | ||
|
@@ -619,11 +625,10 @@ def collect_argument_parameters(args, start_at) | |
def resolve_default_short_options! | ||
@order.each do |type, name| | ||
opts = @specs[name] | ||
next if type != :opt || opts.short | ||
|
||
c = opts.long.chars.find { |d| d !~ INVALID_SHORT_ARG_REGEX && !@short.member?(d) } | ||
next if type != :opt || opts.doesnt_need_autogen_short | ||
c = opts.long.long.split(//).find { |d| d !~ Optimist::ShortNames::INVALID_ARG_REGEX && !@short.member?(d) } | ||
if c # found a character to use | ||
opts.short = c | ||
opts.short.add c | ||
@short[c] = name | ||
end | ||
end | ||
|
@@ -651,14 +656,86 @@ def wrap_line(str, opts = {}) | |
|
||
end | ||
|
||
class LongNames | ||
def initialize | ||
@truename = nil | ||
@long = nil | ||
@alts = [] | ||
end | ||
|
||
def make_valid(lopt) | ||
return nil if lopt.nil? | ||
case lopt.to_s | ||
when /^--([^-].*)$/ then $1 | ||
when /^[^-]/ then lopt.to_s | ||
else raise ArgumentError, "invalid long option name #{lopt.inspect}" | ||
end | ||
end | ||
|
||
def set(name, lopt, alts) | ||
@truename = name | ||
lopt = lopt ? lopt.to_s : name.to_s.gsub("_", "-") | ||
@long = make_valid(lopt) | ||
alts = [alts] unless alts.is_a?(Array) # box the value | ||
@alts = alts.map { |alt| make_valid(alt) }.compact | ||
end | ||
|
||
# long specified with :long has precedence over the true-name | ||
def long ; @long || @truename ; end | ||
|
||
# all valid names, including alts | ||
def names | ||
[long] + @alts | ||
end | ||
|
||
end | ||
|
||
class ShortNames | ||
|
||
INVALID_ARG_REGEX = /[\d-]/ #:nodoc: | ||
|
||
def initialize | ||
@chars = [] | ||
@auto = true | ||
end | ||
|
||
attr_reader :chars, :auto | ||
|
||
def add(values) | ||
values = [values] unless values.is_a?(Array) # box the value | ||
values = values.compact | ||
if values.include?(:none) | ||
if values.size == 1 | ||
@auto = false | ||
return | ||
end | ||
raise ArgumentError, "Cannot use :none with any other values in short option: #{values.inspect}" | ||
end | ||
values.each do |val| | ||
strval = val.to_s | ||
sopt = case strval | ||
when /^-(.)$/ then $1 | ||
when /^.$/ then strval | ||
else raise ArgumentError, "invalid short option name '#{val.inspect}'" | ||
end | ||
|
||
if sopt =~ INVALID_ARG_REGEX | ||
raise ArgumentError, "short option name '#{sopt}' can't be a number or a dash" | ||
end | ||
Comment on lines
+722
to
+724
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. numeric options are interesting though. I'm reminded of For this piece of code though I agree that the short opt can't be a number There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a reformat of this
but yeah I agree. Implementing commands like |
||
@chars << sopt | ||
end | ||
end | ||
|
||
end | ||
|
||
class Option | ||
|
||
attr_accessor :name, :short, :long, :default, :permitted | ||
attr_writer :multi_given | ||
|
||
def initialize | ||
@long = nil | ||
@short = nil | ||
@long = LongNames.new | ||
@short = ShortNames.new # can be an Array of one-char strings, a one-char String, nil or :none | ||
@name = nil | ||
@multi_given = false | ||
@hidden = false | ||
|
@@ -690,7 +767,7 @@ def multi_arg? ; false ; end | |
|
||
def array_default? ; self.default.kind_of?(Array) ; end | ||
|
||
def short? ; short && short != :none ; end | ||
def doesnt_need_autogen_short ; !short.auto || short.chars.any? ; end | ||
|
||
def callback ; opts(:callback) ; end | ||
def desc ; opts(:desc) ; end | ||
|
@@ -705,7 +782,10 @@ def parse(_paramlist, _neg_given) | |
def type_format ; "" ; end | ||
|
||
def educate | ||
(short? ? "-#{short}, " : " ") + "--#{long}" + type_format + (flag? && default ? ", --no-#{long}" : "") | ||
optionlist = [] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Noting that this is an undo of the short-argument padding/alignment. |
||
optionlist.concat(short.chars.map { |o| "-#{o}" }) | ||
optionlist.concat(long.names.map { |o| "--#{o}" }) | ||
optionlist.compact.join(', ') + type_format + (flag? && default ? ", --no-#{long}" : "") | ||
end | ||
|
||
## Format the educate-line description including the default and permitted value(s) | ||
|
@@ -770,10 +850,10 @@ def self.create(name, desc="", opts={}, settings={}) | |
opt_inst = (opttype || opttype_from_default || Optimist::BooleanOption.new) | ||
|
||
## fill in :long | ||
opt_inst.long = handle_long_opt(opts[:long], name) | ||
opt_inst.long.set(name, opts[:long], opts[:alt]) | ||
|
||
## fill in :short | ||
opt_inst.short = handle_short_opt(opts[:short]) | ||
opt_inst.short.add opts[:short] | ||
|
||
## fill in :multi | ||
multi_given = opts[:multi] || false | ||
|
@@ -826,29 +906,6 @@ def self.get_klass_from_default(opts, opttype) | |
return Optimist::Parser.registry_getopttype(type_from_default) | ||
end | ||
|
||
def self.handle_long_opt(lopt, name) | ||
lopt = lopt ? lopt.to_s : name.to_s.gsub("_", "-") | ||
lopt = case lopt | ||
when /^--([^-].*)$/ then $1 | ||
when /^[^-]/ then lopt | ||
else raise ArgumentError, "invalid long option name #{lopt.inspect}" | ||
end | ||
end | ||
|
||
def self.handle_short_opt(sopt) | ||
sopt = sopt.to_s if sopt && sopt != :none | ||
sopt = case sopt | ||
when /^-(.)$/ then $1 | ||
when nil, :none, /^.$/ then sopt | ||
else raise ArgumentError, "invalid short option name '#{sopt.inspect}'" | ||
end | ||
|
||
if sopt | ||
raise ArgumentError, "a short option name can't be a number or a dash" if sopt =~ ::Optimist::Parser::INVALID_SHORT_ARG_REGEX | ||
end | ||
return sopt | ||
end | ||
|
||
end | ||
|
||
# Flag option. Has no arguments. Can be negated with "no-". | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
require 'test_helper' | ||
|
||
module Optimist | ||
|
||
class AlternateNamesTest < ::Minitest::Test | ||
|
||
def setup | ||
@p = Parser.new | ||
end | ||
|
||
def get_help_string | ||
err = assert_raises(Optimist::HelpNeeded) do | ||
@p.parse(%w(--help)) | ||
end | ||
sio = StringIO.new "w" | ||
@p.educate sio | ||
sio.string | ||
end | ||
|
||
def test_altshort | ||
@p.opt :catarg, "desc", :short => ["c", "-C"] | ||
opts = @p.parse %w(-c) | ||
assert_equal true, opts[:catarg] | ||
opts = @p.parse %w(-C) | ||
assert_equal true, opts[:catarg] | ||
assert_raises(CommandlineError) { @p.parse %w(-c -C) } | ||
assert_raises(CommandlineError) { @p.parse %w(-cC) } | ||
end | ||
|
||
def test_altshort_invalid_none | ||
assert_raises(ArgumentError) { | ||
@p.opt :something, "some opt", :short => [:s, :none] | ||
} | ||
assert_raises(ArgumentError) { | ||
@p.opt :something, "some opt", :short => [:none, :s] | ||
} | ||
assert_raises(ArgumentError) { | ||
@p.opt :zumthing, "some opt", :short => [:none, :none] | ||
} | ||
end | ||
|
||
def test_altshort_with_multi | ||
@p.opt :flag, "desc", :short => ["-c", "C", :x], :multi => true | ||
@p.opt :num, "desc", :short => ["-n", "N"], :multi => true, type: Integer | ||
@p.parse %w(-c) | ||
@p.parse %w(-C -c -x) | ||
@p.parse %w(-c -C) | ||
@p.parse %w(-c -C -c -C) | ||
opts = @p.parse %w(-ccCx) | ||
assert_equal true, opts[:flag] | ||
@p.parse %w(-c) | ||
@p.parse %w(-N 1 -n 3) | ||
@p.parse %w(-n 2 -N 4) | ||
opts = @p.parse %w(-n 4 -N 3 -n 2 -N 1) | ||
assert_equal [4, 3, 2, 1], opts[:num] | ||
end | ||
|
||
def test_altlong | ||
@p.opt "goodarg0", "desc", :alt => "zero" | ||
@p.opt "goodarg1", "desc", :long => "newone", :alt => "one" | ||
@p.opt "goodarg2", "desc", :alt => "--two" | ||
@p.opt "goodarg3", "desc", :alt => ["three", "--four", :five] | ||
|
||
[%w[--goodarg0], %w[--zero]].each do |a| | ||
opts = @p.parse(a) | ||
assert opts.goodarg0 | ||
end | ||
|
||
[%w[--newone], %w[-n], %w[--one]].each do |a| | ||
opts = @p.parse(a) | ||
assert opts.goodarg1 | ||
end | ||
|
||
[%w[--two]].each do |a| | ||
opts = @p.parse(a) | ||
assert opts.goodarg2 | ||
end | ||
|
||
[%w[--three], %w[--four], %w[--five]].each do |a| | ||
opts = @p.parse(a) | ||
assert opts.goodarg3 | ||
end | ||
|
||
[%w[--goodarg1], %w[--missing], %w[-a]].each do |a| | ||
assert_raises(Optimist::CommandlineError) { @p.parse(a) } | ||
end | ||
|
||
["", '--', '-bad', '---threedash'].each do |altitem| | ||
assert_raises(ArgumentError) { @p.opt "badarg", "desc", :alt => altitem } | ||
end | ||
end | ||
|
||
def test_altshort_help | ||
@p.opt :cat, 'cat', short: ['c','C','a','T'] | ||
outstring = get_help_string | ||
# expect mutliple short-opts to be in the help | ||
assert_match(/-c, -C, -a, -T, --cat/, outstring) | ||
end | ||
|
||
|
||
def test_altlong_help | ||
@p.opt :cat, 'a cat', alt: :feline | ||
@p.opt :dog, 'a dog', alt: ['Pooch', :canine] | ||
@p.opt :fruit, 'a fruit', long: :fig, alt: ['peach', :pear, "--apple"], short: :none | ||
@p.opt :veg, "gemuse", long: :gemuse, alt: [:groente] | ||
outstring = get_help_string | ||
|
||
assert_match(/^\s*-c, --cat, --feline/, outstring) | ||
assert_match(/^\s*-d, --dog, --Pooch, --canine/, outstring) | ||
|
||
# expect long-opt to shadow the actual name | ||
assert_match(/^\s*--fig, --peach, --pear, --apple/, outstring) | ||
assert_match(/^\s*-g, --gemuse, --groente/, outstring) | ||
|
||
end | ||
|
||
def test_alt_duplicates | ||
# alt duplicates named option | ||
assert_raises(ArgumentError) { | ||
@p.opt :cat, 'desc', :alt => :cat | ||
} | ||
# alt duplicates :long | ||
assert_raises(ArgumentError) { | ||
@p.opt :cat, 'desc', :long => :feline, :alt => [:feline] | ||
} | ||
# alt duplicates itself | ||
assert_raises(ArgumentError) { | ||
@p.opt :abc, 'desc', :alt => [:aaa, :aaa] | ||
} | ||
end | ||
|
||
def test_altlong_collisions | ||
@p.opt :fat, 'desc' | ||
@p.opt :raton, 'desc', :long => :rat | ||
@p.opt :bat, 'desc', :alt => [:baton, :twirl] | ||
|
||
# :alt collision with named option | ||
assert_raises(ArgumentError) { | ||
@p.opt :cat, 'desc', :alt => :fat | ||
} | ||
|
||
# :alt collision with :long option | ||
assert_raises(ArgumentError) { | ||
@p.opt :cat, 'desc', :alt => :rat | ||
} | ||
|
||
# :named option collision with existing :alt option | ||
assert_raises(ArgumentError) { | ||
@p.opt :baton, 'desc' | ||
} | ||
|
||
# :long option collision with existing :alt option | ||
assert_raises(ArgumentError) { | ||
@p.opt :whirl, 'desc', :long => 'twirl' | ||
} | ||
|
||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated to prevent
:none
from being used with any other value (including itself)