optimist 3.1.0 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +1 -1
- data/.github/workflows/ci.yaml +3 -1
- data/CHANGELOG.md +222 -2
- data/examples/a_basic_example.rb +10 -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/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 +1 -1
- data/renovate.json +6 -0
- data/test/optimist/alt_names_test.rb +168 -0
- data/test/optimist/command_line_error_test.rb +1 -1
- data/test/optimist/help_needed_test.rb +1 -1
- data/test/optimist/parser_constraint_test.rb +141 -0
- data/test/optimist/parser_educate_test.rb +22 -1
- data/test/optimist/parser_opt_test.rb +1 -1
- data/test/optimist/parser_parse_test.rb +3 -3
- data/test/optimist/parser_permitted_test.rb +121 -0
- data/test/optimist/parser_test.rb +277 -176
- data/test/optimist/version_needed_test.rb +1 -1
- data/test/optimist_test.rb +5 -3
- data/test/support/assert_helpers.rb +6 -0
- metadata +22 -5
- 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.
|
11
|
+
VERSION = "3.2.0"
|
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}" : "")
|
640
824
|
end
|
641
825
|
|
642
|
-
## Format the educate-line description including the default
|
643
|
-
def
|
644
|
-
|
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
|
842
|
+
end
|
843
|
+
|
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 }
|