optimist 3.1.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/optimist.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # lib/optimist.rb -- optimist command-line processing library
2
4
  # Copyright (c) 2008-2014 William Morgan.
3
5
  # Copyright (c) 2014 Red Hat, Inc.
@@ -6,7 +8,7 @@
6
8
  require 'date'
7
9
 
8
10
  module Optimist
9
- VERSION = "3.1.0"
11
+ VERSION = "3.2.1"
10
12
 
11
13
  ## Thrown by Parser in the event of a commandline error. Not needed if
12
14
  ## you're using the Optimist::options entry.
@@ -35,6 +37,50 @@ FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))([eE][-+]?[\d]+)?$/
35
37
  ## Regex for parameters
36
38
  PARAM_RE = /^-(-|\.$|[^\d\.])/
37
39
 
40
+ # Abstract class for a constraint. Do not use by itself.
41
+ class Constraint
42
+ def initialize(syms)
43
+ @idents = syms
44
+ end
45
+ def validate(given_args:, specs:)
46
+ overlap = @idents & given_args.keys
47
+ if error_condition(overlap.size)
48
+ longargs = @idents.map { |sym| "--#{specs[sym].long.long}" }
49
+ raise CommandlineError, error_message(longargs)
50
+ end
51
+ end
52
+ end
53
+
54
+ # A Dependency constraint. Useful when Option A requires Option B also be used.
55
+ class DependConstraint < Constraint
56
+ def error_condition(overlap_size)
57
+ (overlap_size != 0) && (overlap_size != @idents.size)
58
+ end
59
+ def error_message(longargs)
60
+ "#{longargs.join(', ')} have a dependency and must be given together"
61
+ end
62
+ end
63
+
64
+ # A Conflict constraint. Useful when Option A cannot be used with Option B.
65
+ class ConflictConstraint < Constraint
66
+ def error_condition(overlap_size)
67
+ (overlap_size != 0) && (overlap_size != 1)
68
+ end
69
+ def error_message(longargs)
70
+ "only one of #{longargs.join(', ')} can be given"
71
+ end
72
+ end
73
+
74
+ # An Either-Or constraint. For Mutually exclusive options
75
+ class EitherConstraint < Constraint
76
+ def error_condition(overlap_size)
77
+ overlap_size != 1
78
+ end
79
+ def error_message(longargs)
80
+ "one and only one of #{longargs.join(', ')} is required"
81
+ end
82
+ end
83
+
38
84
  ## The commandline parser. In typical usage, the methods in this class
39
85
  ## will be handled internally by Optimist::options. In this case, only the
40
86
  ## #opt, #banner and #version, #depends, and #conflicts methods will
@@ -68,8 +114,6 @@ class Parser
68
114
  return @registry[lookup].new
69
115
  end
70
116
 
71
- INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc:
72
-
73
117
  ## The values from the commandline that were not interpreted by #parse.
74
118
  attr_reader :leftovers
75
119
 
@@ -82,6 +126,12 @@ class Parser
82
126
  ## ignore options that it does not recognize.
83
127
  attr_accessor :ignore_invalid_options
84
128
 
129
+ DEFAULT_SETTINGS = {
130
+ exact_match: true,
131
+ implicit_short_opts: true,
132
+ suggestions: true
133
+ }
134
+
85
135
  ## Initializes the parser, and instance-evaluates any block given.
86
136
  def initialize(*a, &b)
87
137
  @version = nil
@@ -97,8 +147,17 @@ class Parser
97
147
  @synopsis = nil
98
148
  @usage = nil
99
149
 
100
- # instance_eval(&b) if b # can't take arguments
101
- cloaker(&b).bind(self).call(*a) if b
150
+ ## allow passing settings through Parser.new as an optional hash.
151
+ ## but keep compatibility with non-hashy args, though.
152
+ begin
153
+ settings_hash = Hash[*a]
154
+ @settings = DEFAULT_SETTINGS.merge(settings_hash)
155
+ a=[] ## clear out args if using as settings-hash
156
+ rescue ArgumentError
157
+ @settings = DEFAULT_SETTINGS
158
+ end
159
+
160
+ self.instance_exec(*a, &b) if block_given?
102
161
  end
103
162
 
104
163
  ## Define an option. +name+ is the option name, a unique identifier
@@ -114,6 +173,7 @@ class Parser
114
173
  ## [+: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+.
115
174
  ## [+:required+] If set to +true+, the argument must be provided on the commandline.
116
175
  ## [+: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.)
176
+ ## [+:permitted+] Specify an Array of permitted values for an option. If the user provides a value outside this list, an error is thrown.
117
177
  ##
118
178
  ## Note that there are two types of argument multiplicity: an argument
119
179
  ## can take multiple values, e.g. "--arg 1 2 3". An argument can also
@@ -148,10 +208,19 @@ class Parser
148
208
  o = Option.create(name, desc, opts)
149
209
 
150
210
  raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? o.name
151
- raise ArgumentError, "long option name #{o.long.inspect} is already taken; please specify a (different) :long" if @long[o.long]
152
- raise ArgumentError, "short option name #{o.short.inspect} is already taken; please specify a (different) :short" if @short[o.short]
153
- @long[o.long] = o.name
154
- @short[o.short] = o.name if o.short?
211
+
212
+ o.long.names.each do |lng|
213
+ raise ArgumentError, "long option name #{lng.inspect} is already taken; please specify a (different) :long/:alt" if @long[lng]
214
+ @long[lng] = o.name
215
+ end
216
+
217
+ o.short.chars.each do |short|
218
+ raise ArgumentError, "short option name #{short.inspect} is already taken; please specify a (different) :short" if @short[short]
219
+ @short[short] = o.name
220
+ end
221
+
222
+ raise ArgumentError, "permitted values for option #{o.long.long.inspect} must be either nil, Range, Regexp or an Array;" unless o.permitted_type_valid?
223
+
155
224
  @specs[o.name] = o
156
225
  @order << [:opt, o.name]
157
226
  end
@@ -188,20 +257,19 @@ class Parser
188
257
  ## better modeled with Optimist::die.
189
258
  def depends(*syms)
190
259
  syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
191
- @constraints << [:depends, syms]
260
+ @constraints << DependConstraint.new(syms)
192
261
  end
193
262
 
194
263
  ## Marks two (or more!) options as conflicting.
195
264
  def conflicts(*syms)
196
265
  syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
197
- @constraints << [:conflicts, syms]
266
+ @constraints << ConflictConstraint.new(syms)
198
267
  end
199
268
 
200
269
  ## Marks two (or more!) options as required but mutually exclusive.
201
270
  def either(*syms)
202
271
  syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
203
- @constraints << [:conflicts, syms]
204
- @constraints << [:either, syms]
272
+ @constraints << EitherConstraint.new(syms)
205
273
  end
206
274
 
207
275
  ## Defines a set of words which cause parsing to terminate when
@@ -231,6 +299,56 @@ class Parser
231
299
  @educate_on_error = true
232
300
  end
233
301
 
302
+ ## Match long variables with inexact match.
303
+ ## If we hit a complete match, then use that, otherwise see how many long-options partially match.
304
+ ## If only one partially matches, then we can safely use that.
305
+ ## Otherwise, we raise an error that the partially given option was ambiguous.
306
+ def perform_inexact_match(arg, partial_match) # :nodoc:
307
+ return @long[partial_match] if @long.has_key?(partial_match)
308
+ partially_matched_keys = @long.keys.select { |opt| opt.start_with?(partial_match) }
309
+ case partially_matched_keys.size
310
+ when 0 ; nil
311
+ when 1 ; @long[partially_matched_keys.first]
312
+ else ; raise CommandlineError, "ambiguous option '#{arg}' matched keys (#{partially_matched_keys.join(',')})"
313
+ end
314
+ end
315
+ private :perform_inexact_match
316
+
317
+ def handle_unknown_argument(arg, candidates, suggestions)
318
+ errstring = "unknown argument '#{arg}'"
319
+ if (suggestions &&
320
+ Module::const_defined?("DidYouMean") &&
321
+ Module::const_defined?("DidYouMean::JaroWinkler") &&
322
+ Module::const_defined?("DidYouMean::Levenshtein"))
323
+ input = arg.sub(/^[-]*/,'')
324
+
325
+ # Code borrowed from did_you_mean gem
326
+ jw_threshold = 0.75
327
+ seed = candidates.select {|candidate| DidYouMean::JaroWinkler.distance(candidate, input) >= jw_threshold } \
328
+ .sort_by! {|candidate| DidYouMean::JaroWinkler.distance(candidate.to_s, input) } \
329
+ .reverse!
330
+ # Correct mistypes
331
+ threshold = (input.length * 0.25).ceil
332
+ has_mistype = seed.rindex {|c| DidYouMean::Levenshtein.distance(c, input) <= threshold }
333
+ corrections = if has_mistype
334
+ seed.take(has_mistype + 1)
335
+ else
336
+ # Correct misspells
337
+ seed.select do |candidate|
338
+ length = input.length < candidate.length ? input.length : candidate.length
339
+
340
+ DidYouMean::Levenshtein.distance(candidate, input) < length
341
+ end.first(1)
342
+ end
343
+ unless corrections.empty?
344
+ dashdash_corrections = corrections.map{|s| "--#{s}" }
345
+ errstring += ". Did you mean: [#{dashdash_corrections.join(', ')}] ?"
346
+ end
347
+ end
348
+ raise CommandlineError, errstring
349
+ end
350
+ private :handle_unknown_argument
351
+
234
352
  ## Parses the commandline. Typically called by Optimist::options,
235
353
  ## but you can call it directly if you need more control.
236
354
  ##
@@ -248,7 +366,7 @@ class Parser
248
366
  vals[sym] = [] if opts.multi && !opts.default # multi arguments default to [], not nil
249
367
  end
250
368
 
251
- resolve_default_short_options!
369
+ resolve_default_short_options! if @settings[:implicit_short_opts]
252
370
 
253
371
  ## resolve symbols
254
372
  given_args = {}
@@ -266,10 +384,15 @@ class Parser
266
384
  else raise CommandlineError, "invalid argument syntax: '#{arg}'"
267
385
  end
268
386
 
269
- sym = nil if arg =~ /--no-/ # explicitly invalidate --no-no- arguments
387
+ if arg.start_with?("--no-") # explicitly invalidate --no-no- arguments
388
+ sym = nil
389
+ ## Support inexact matching of long-arguments like perl's Getopt::Long
390
+ elsif !sym && !@settings[:exact_match] && arg.match(/^--(\S+)$/)
391
+ sym = perform_inexact_match(arg, $1)
392
+ end
270
393
 
271
394
  next nil if ignore_invalid_options && !sym
272
- raise CommandlineError, "unknown argument '#{arg}'" unless sym
395
+ handle_unknown_argument(arg, @long.keys, @settings[:suggestions]) unless sym
273
396
 
274
397
  if given_args.include?(sym) && !@specs[sym].multi?
275
398
  raise CommandlineError, "option '#{arg}' specified multiple times"
@@ -301,23 +424,12 @@ class Parser
301
424
  raise HelpNeeded if given_args.include? :help
302
425
 
303
426
  ## check constraint satisfaction
304
- @constraints.each do |type, syms|
305
- constraint_sym = syms.find { |sym| given_args[sym] }
306
-
307
- case type
308
- when :depends
309
- next unless constraint_sym
310
- syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym].long} requires --#{@specs[sym].long}" unless given_args.include? sym }
311
- when :conflicts
312
- next unless constraint_sym
313
- syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym].long} conflicts with --#{@specs[sym].long}" if given_args.include?(sym) && (sym != constraint_sym) }
314
- when :either
315
- raise CommandlineError, "one of #{syms.map { |sym| "--#{@specs[sym].long}" }.join(', ') } is required" if (syms & given_args.keys).size != 1
316
- end
427
+ @constraints.each do |const|
428
+ const.validate(given_args: given_args, specs: @specs)
317
429
  end
318
430
 
319
431
  required.each do |sym, val|
320
- raise CommandlineError, "option --#{@specs[sym].long} must be specified" unless given_args.include? sym
432
+ raise CommandlineError, "option --#{@specs[sym].long.long} must be specified" unless given_args.include? sym
321
433
  end
322
434
 
323
435
  ## parse parameters
@@ -330,6 +442,12 @@ class Parser
330
442
  params << (opts.array_default? ? opts.default.clone : [opts.default])
331
443
  end
332
444
 
445
+ if params.first && opts.permitted
446
+ params.first.each do |val|
447
+ opts.validate_permitted(arg, val)
448
+ end
449
+ end
450
+
333
451
  vals["#{sym}_given".intern] = true # mark argument as specified on the commandline
334
452
 
335
453
  vals[sym] = opts.parse(params, negative_given)
@@ -390,7 +508,7 @@ class Parser
390
508
 
391
509
  spec = @specs[opt]
392
510
  stream.printf " %-#{leftcol_width}s ", left[opt]
393
- desc = spec.description_with_default
511
+ desc = spec.full_description
394
512
 
395
513
  stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
396
514
  end
@@ -435,7 +553,7 @@ class Parser
435
553
  def die(arg, msg = nil, error_code = nil)
436
554
  msg, error_code = nil, msg if msg.kind_of?(Integer)
437
555
  if msg
438
- $stderr.puts "Error: argument --#{@specs[arg].long} #{msg}."
556
+ $stderr.puts "Error: argument --#{@specs[arg].long.long} #{msg}."
439
557
  else
440
558
  $stderr.puts "Error: #{arg}."
441
559
  end
@@ -458,7 +576,7 @@ private
458
576
  until i >= args.length
459
577
  return remains += args[i..-1] if @stop_words.member? args[i]
460
578
  case args[i]
461
- when /^--$/ # arg terminator
579
+ when "--" # arg terminator
462
580
  return remains += args[(i + 1)..-1]
463
581
  when /^--(\S+?)=(.*)$/ # long argument with equals
464
582
  num_params_taken = yield "--#{$1}", [$2]
@@ -483,7 +601,7 @@ private
483
601
  end
484
602
  i += 1
485
603
  when /^-(\S+)$/ # one or more short arguments
486
- short_remaining = ""
604
+ short_remaining = []
487
605
  shortargs = $1.split(//)
488
606
  shortargs.each_with_index do |a, j|
489
607
  if j == (shortargs.length - 1)
@@ -493,7 +611,7 @@ private
493
611
  unless num_params_taken
494
612
  short_remaining << a
495
613
  if @stop_on_unknown
496
- remains << "-#{short_remaining}"
614
+ remains << "-#{short_remaining.join}"
497
615
  return remains += args[i + 1..-1]
498
616
  end
499
617
  else
@@ -503,8 +621,8 @@ private
503
621
  unless yield "-#{a}", []
504
622
  short_remaining << a
505
623
  if @stop_on_unknown
506
- short_remaining += shortargs[j + 1..-1].join
507
- remains << "-#{short_remaining}"
624
+ short_remaining << shortargs[j + 1..-1].join
625
+ remains << "-#{short_remaining.join}"
508
626
  return remains += args[i + 1..-1]
509
627
  end
510
628
  end
@@ -512,7 +630,7 @@ private
512
630
  end
513
631
 
514
632
  unless short_remaining.empty?
515
- remains << "-#{short_remaining}"
633
+ remains << "-#{short_remaining.join}"
516
634
  end
517
635
  i += 1
518
636
  else
@@ -541,11 +659,10 @@ private
541
659
  def resolve_default_short_options!
542
660
  @order.each do |type, name|
543
661
  opts = @specs[name]
544
- next if type != :opt || opts.short
545
-
546
- c = opts.long.split(//).find { |d| d !~ INVALID_SHORT_ARG_REGEX && !@short.member?(d) }
662
+ next if type != :opt || opts.doesnt_need_autogen_short
663
+ c = opts.long.long.split(//).find { |d| d !~ Optimist::ShortNames::INVALID_ARG_REGEX && !@short.member?(d) }
547
664
  if c # found a character to use
548
- opts.short = c
665
+ opts.short.add c
549
666
  @short[c] = name
550
667
  end
551
668
  end
@@ -571,30 +688,94 @@ private
571
688
  ret
572
689
  end
573
690
 
574
- ## instance_eval but with ability to handle block arguments
575
- ## thanks to _why: http://redhanded.hobix.com/inspect/aBlockCostume.html
576
- def cloaker(&b)
577
- (class << self; self; end).class_eval do
578
- define_method :cloaker_, &b
579
- meth = instance_method :cloaker_
580
- remove_method :cloaker_
581
- meth
691
+ end
692
+
693
+ class LongNames
694
+ def initialize
695
+ @truename = nil
696
+ @long = nil
697
+ @alts = []
698
+ end
699
+
700
+ def make_valid(lopt)
701
+ return nil if lopt.nil?
702
+ case lopt.to_s
703
+ when /^--([^-].*)$/ then $1
704
+ when /^[^-]/ then lopt.to_s
705
+ else raise ArgumentError, "invalid long option name #{lopt.inspect}"
582
706
  end
583
707
  end
708
+
709
+ def set(name, lopt, alts)
710
+ @truename = name
711
+ lopt = lopt ? lopt.to_s : name.to_s.gsub("_", "-")
712
+ @long = make_valid(lopt)
713
+ alts = [alts] unless alts.is_a?(Array) # box the value
714
+ @alts = alts.map { |alt| make_valid(alt) }.compact
715
+ end
716
+
717
+ # long specified with :long has precedence over the true-name
718
+ def long ; @long || @truename ; end
719
+
720
+ # all valid names, including alts
721
+ def names
722
+ [long] + @alts
723
+ end
724
+
725
+ end
726
+
727
+ class ShortNames
728
+
729
+ INVALID_ARG_REGEX = /[\d-]/ #:nodoc:
730
+
731
+ def initialize
732
+ @chars = []
733
+ @auto = true
734
+ end
735
+
736
+ attr_reader :chars, :auto
737
+
738
+ def add(values)
739
+ values = [values] unless values.is_a?(Array) # box the value
740
+ values = values.compact
741
+ if values.include?(:none)
742
+ if values.size == 1
743
+ @auto = false
744
+ return
745
+ end
746
+ raise ArgumentError, "Cannot use :none with any other values in short option: #{values.inspect}"
747
+ end
748
+ values.each do |val|
749
+ strval = val.to_s
750
+ sopt = case strval
751
+ when /^-(.)$/ then $1
752
+ when /^.$/ then strval
753
+ else raise ArgumentError, "invalid short option name '#{val.inspect}'"
754
+ end
755
+
756
+ if sopt =~ INVALID_ARG_REGEX
757
+ raise ArgumentError, "short option name '#{sopt}' can't be a number or a dash"
758
+ end
759
+ @chars << sopt
760
+ end
761
+ end
762
+
584
763
  end
585
764
 
586
765
  class Option
587
766
 
588
- attr_accessor :name, :short, :long, :default
767
+ attr_accessor :name, :short, :long, :default, :permitted, :permitted_response
589
768
  attr_writer :multi_given
590
769
 
591
770
  def initialize
592
- @long = nil
593
- @short = nil
771
+ @long = LongNames.new
772
+ @short = ShortNames.new # can be an Array of one-char strings, a one-char String, nil or :none
594
773
  @name = nil
595
774
  @multi_given = false
596
775
  @hidden = false
597
776
  @default = nil
777
+ @permitted = nil
778
+ @permitted_response = "option '%{arg}' only accepts %{valid_string}"
598
779
  @optshash = Hash.new()
599
780
  end
600
781
 
@@ -621,7 +802,7 @@ class Option
621
802
 
622
803
  def array_default? ; self.default.kind_of?(Array) ; end
623
804
 
624
- def short? ; short && short != :none ; end
805
+ def doesnt_need_autogen_short ; !short.auto || short.chars.any? ; end
625
806
 
626
807
  def callback ; opts(:callback) ; end
627
808
  def desc ; opts(:desc) ; end
@@ -636,23 +817,96 @@ class Option
636
817
  def type_format ; "" ; end
637
818
 
638
819
  def educate
639
- (short? ? "-#{short}, " : "") + "--#{long}" + type_format + (flag? && default ? ", --no-#{long}" : "")
820
+ optionlist = []
821
+ optionlist.concat(short.chars.map { |o| "-#{o}" })
822
+ optionlist.concat(long.names.map { |o| "--#{o}" })
823
+ optionlist.compact.join(', ') + type_format + (flag? && default ? ", --no-#{long.long}" : "")
824
+ end
825
+
826
+ ## Format the educate-line description including the default and permitted value(s)
827
+ def full_description
828
+ desc_str = desc
829
+ desc_str += default_description_str(desc) if default
830
+ desc_str += permitted_description_str(desc) if permitted
831
+ desc_str
832
+ end
833
+
834
+ ## Format stdio like objects to a string
835
+ def format_stdio(obj)
836
+ case obj
837
+ when $stdout then '<stdout>'
838
+ when $stdin then '<stdin>'
839
+ when $stderr then '<stderr>'
840
+ else obj # pass-through-case
841
+ end
640
842
  end
641
843
 
642
- ## Format the educate-line description including the default-value(s)
643
- def description_with_default
644
- return desc unless default
844
+ ## Generate the default value string for the educate line
845
+ private def default_description_str str
645
846
  default_s = case default
646
- when $stdout then '<stdout>'
647
- when $stdin then '<stdin>'
648
- when $stderr then '<stderr>'
649
847
  when Array
650
848
  default.join(', ')
651
849
  else
652
- default.to_s
850
+ format_stdio(default).to_s
653
851
  end
654
- defword = desc.end_with?('.') ? 'Default' : 'default'
655
- return "#{desc} (#{defword}: #{default_s})"
852
+ defword = str.end_with?('.') ? 'Default' : 'default'
853
+ " (#{defword}: #{default_s})"
854
+ end
855
+
856
+ def permitted_valid_string
857
+ case permitted
858
+ when Array
859
+ return "one of: " + permitted.to_a.map(&:to_s).join(', ')
860
+ when Range
861
+ return "value in range of: #{permitted}"
862
+ when Regexp
863
+ return "value matching: #{permitted.inspect}"
864
+ end
865
+ raise NotImplementedError, "invalid branch"
866
+ end
867
+
868
+ def permitted_type_valid?
869
+ case permitted
870
+ when NilClass, Array, Range, Regexp then true
871
+ else false
872
+ end
873
+ end
874
+
875
+ def validate_permitted(arg, value)
876
+ return true if permitted.nil?
877
+ unless permitted_value?(value)
878
+ format_hash = {arg: arg, given: value, value: value, valid_string: permitted_valid_string(), permitted: permitted }
879
+ raise CommandlineError, permitted_response % format_hash
880
+ end
881
+ true
882
+ end
883
+
884
+ # incoming values from the command-line should be strings, so we should
885
+ # stringify any permitted types as the basis of comparison.
886
+ def permitted_value?(val)
887
+ case permitted
888
+ when nil then true
889
+ when Regexp then val.match? permitted
890
+ when Range then permitted.include? as_type(val)
891
+ when Array then permitted.map(&:to_s).include? val
892
+ else false
893
+ end
894
+ end
895
+
896
+ ## Generate the permitted values string for the educate line
897
+ private def permitted_description_str str
898
+ permitted_s = case permitted
899
+ when Array
900
+ permitted.map do |p|
901
+ format_stdio(p).to_s
902
+ end.join(', ')
903
+ when Range, Regexp
904
+ permitted.inspect
905
+ else
906
+ raise NotImplementedError
907
+ end
908
+ permword = str.end_with?('.') ? 'Permitted' : 'permitted'
909
+ " (#{permword}: #{permitted_s})"
656
910
  end
657
911
 
658
912
  ## Provide a way to register symbol aliases to the Parser
@@ -679,10 +933,10 @@ class Option
679
933
  opt_inst = (opttype || opttype_from_default || Optimist::BooleanOption.new)
680
934
 
681
935
  ## fill in :long
682
- opt_inst.long = handle_long_opt(opts[:long], name)
936
+ opt_inst.long.set(name, opts[:long], opts[:alt])
683
937
 
684
938
  ## fill in :short
685
- opt_inst.short = handle_short_opt(opts[:short])
939
+ opt_inst.short.add opts[:short]
686
940
 
687
941
  ## fill in :multi
688
942
  multi_given = opts[:multi] || false
@@ -691,8 +945,13 @@ class Option
691
945
  ## fill in :default for flags
692
946
  defvalue = opts[:default] || opt_inst.default
693
947
 
948
+ ## fill in permitted values
949
+ permitted = opts[:permitted] || nil
950
+
694
951
  ## autobox :default for :multi (multi-occurrence) arguments
695
952
  defvalue = [defvalue] if defvalue && multi_given && !defvalue.kind_of?(Array)
953
+ opt_inst.permitted = permitted
954
+ opt_inst.permitted_response = opts[:permitted_response] if opts[:permitted_response]
696
955
  opt_inst.default = defvalue
697
956
  opt_inst.name = name
698
957
  opt_inst.opts = opts
@@ -731,29 +990,6 @@ class Option
731
990
  return Optimist::Parser.registry_getopttype(type_from_default)
732
991
  end
733
992
 
734
- def self.handle_long_opt(lopt, name)
735
- lopt = lopt ? lopt.to_s : name.to_s.gsub("_", "-")
736
- lopt = case lopt
737
- when /^--([^-].*)$/ then $1
738
- when /^[^-]/ then lopt
739
- else raise ArgumentError, "invalid long option name #{lopt.inspect}"
740
- end
741
- end
742
-
743
- def self.handle_short_opt(sopt)
744
- sopt = sopt.to_s if sopt && sopt != :none
745
- sopt = case sopt
746
- when /^-(.)$/ then $1
747
- when nil, :none, /^.$/ then sopt
748
- else raise ArgumentError, "invalid short option name '#{sopt.inspect}'"
749
- end
750
-
751
- if sopt
752
- raise ArgumentError, "a short option name can't be a number or a dash" if sopt =~ ::Optimist::Parser::INVALID_SHORT_ARG_REGEX
753
- end
754
- return sopt
755
- end
756
-
757
993
  end
758
994
 
759
995
  # Flag option. Has no arguments. Can be negated with "no-".
@@ -773,11 +1009,12 @@ end
773
1009
  class FloatOption < Option
774
1010
  register_alias :float, :double
775
1011
  def type_format ; "=<f>" ; end
1012
+ def as_type(param) ; param.to_f ; end
776
1013
  def parse(paramlist, _neg_given)
777
1014
  paramlist.map do |pg|
778
1015
  pg.map do |param|
779
1016
  raise CommandlineError, "option '#{self.name}' needs a floating-point number" unless param.is_a?(Numeric) || param =~ FLOAT_RE
780
- param.to_f
1017
+ as_type(param)
781
1018
  end
782
1019
  end
783
1020
  end
@@ -787,11 +1024,12 @@ end
787
1024
  class IntegerOption < Option
788
1025
  register_alias :int, :integer, :fixnum
789
1026
  def type_format ; "=<i>" ; end
1027
+ def as_type(param) ; param.to_i ; end
790
1028
  def parse(paramlist, _neg_given)
791
1029
  paramlist.map do |pg|
792
1030
  pg.map do |param|
793
1031
  raise CommandlineError, "option '#{self.name}' needs an integer" unless param.is_a?(Numeric) || param =~ /^-?[\d_]+$/
794
- param.to_i
1032
+ as_type(param)
795
1033
  end
796
1034
  end
797
1035
  end
@@ -824,9 +1062,10 @@ end
824
1062
  # Option class for handling Strings.
825
1063
  class StringOption < Option
826
1064
  register_alias :string
1065
+ def as_type(val) ; val.to_s ; end
827
1066
  def type_format ; "=<s>" ; end
828
1067
  def parse(paramlist, _neg_given)
829
- paramlist.map { |pg| pg.map(&:to_s) }
1068
+ paramlist.map { |pg| pg.map { |param| as_type(param) } }
830
1069
  end
831
1070
  end
832
1071
 
@@ -924,7 +1163,22 @@ end
924
1163
  ## ## if called with --monkey
925
1164
  ## p opts # => {:monkey=>true, :name=>nil, :num_limbs=>4, :help=>false, :monkey_given=>true}
926
1165
  ##
927
- ## See more examples at http://optimist.rubyforge.org.
1166
+ ## Settings:
1167
+ ## Optimist::options and Optimist::Parser.new accept +settings+ to control how
1168
+ ## options are interpreted. These settings are given as hash arguments, e.g:
1169
+ ##
1170
+ ## opts = Optimist::options(ARGV, exact_match: false) do
1171
+ ## opt :foobar, 'messed up'
1172
+ ## opt :forget, 'forget it'
1173
+ ## end
1174
+ ##
1175
+ ## +settings+ include:
1176
+ ## * :exact_match : (default=true) Allow minimum unambigous number of characters to match a long option
1177
+ ## * :suggestions : (default=true) Enables suggestions when unknown arguments are given and DidYouMean is installed. DidYouMean comes standard with Ruby 2.3+
1178
+ ## * :implicit_short_opts : (default=true) Short options will only be created where explicitly defined. If you do not like short-options, this will prevent having to define :short=> :none for all of your options.
1179
+ ## Because Optimist::options uses a default argument for +args+, you must pass that argument when using the settings feature.
1180
+ ##
1181
+ ## See more examples at https://www.manageiq.org/optimist
928
1182
  def options(args = ARGV, *a, &b)
929
1183
  @last_parser = Parser.new(*a, &b)
930
1184
  with_standard_exception_handling(@last_parser) { @last_parser.parse args }