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.
- checksums.yaml +4 -4
- data/.codeclimate.yml +1 -1
- data/.github/workflows/ci.yaml +4 -5
- data/CHANGELOG.md +235 -2
- data/examples/a_basic_example.rb +9 -0
- data/examples/alt_names.rb +20 -0
- data/examples/banners1.rb +11 -0
- data/examples/banners2.rb +12 -0
- data/examples/banners3.rb +14 -0
- data/examples/boolean.rb +9 -0
- data/examples/constraints.rb +28 -0
- data/examples/didyoumean.rb +26 -0
- data/examples/medium_example.rb +15 -0
- data/examples/partialmatch.rb +18 -0
- data/examples/permitted.rb +29 -0
- data/examples/types_custom.rb +43 -0
- data/lib/optimist.rb +347 -93
- data/optimist.gemspec +4 -4
- data/renovate.json +5 -0
- data/test/optimist/alt_names_test.rb +168 -0
- data/test/optimist/command_line_error_test.rb +2 -2
- data/test/optimist/help_needed_test.rb +2 -2
- data/test/optimist/parser_constraint_test.rb +141 -0
- data/test/optimist/parser_educate_test.rb +45 -12
- data/test/optimist/parser_opt_test.rb +2 -2
- data/test/optimist/parser_parse_test.rb +4 -4
- data/test/optimist/parser_permitted_test.rb +121 -0
- data/test/optimist/parser_test.rb +277 -176
- data/test/optimist/version_needed_test.rb +2 -2
- data/test/optimist_test.rb +6 -4
- data/test/support/assert_helpers.rb +10 -3
- metadata +27 -9
- data/History.txt +0 -175
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
|
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
|
-
|
101
|
-
|
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
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
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 <<
|
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 <<
|
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 <<
|
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
|
-
|
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
|
-
|
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 |
|
305
|
-
|
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.
|
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
|
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
|
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.
|
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
|
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
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
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 =
|
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
|
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
|
-
|
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
|
-
##
|
643
|
-
def
|
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 =
|
655
|
-
|
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
|
936
|
+
opt_inst.long.set(name, opts[:long], opts[:alt])
|
683
937
|
|
684
938
|
## fill in :short
|
685
|
-
opt_inst.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
|
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
|
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(
|
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
|
-
##
|
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 }
|