optimist 3.0.1 → 3.2.0
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 +16 -0
- data/.github/workflows/ci.yaml +36 -0
- data/.rubocop.yml +4 -0
- data/.rubocop_cc.yml +4 -0
- data/.rubocop_local.yml +0 -0
- data/.whitesource +3 -0
- data/CHANGELOG.md +232 -0
- data/README.md +2 -3
- 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 +352 -90
- data/optimist.gemspec +4 -3
- 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 +295 -148
- 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 +57 -20
- data/.travis.yml +0 -14
- 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,9 +8,7 @@
|
|
6
8
|
require 'date'
|
7
9
|
|
8
10
|
module Optimist
|
9
|
-
|
10
|
-
# please change over there too
|
11
|
-
VERSION = "3.0.1"
|
11
|
+
VERSION = "3.2.0"
|
12
12
|
|
13
13
|
## Thrown by Parser in the event of a commandline error. Not needed if
|
14
14
|
## you're using the Optimist::options entry.
|
@@ -37,6 +37,50 @@ FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))([eE][-+]?[\d]+)?$/
|
|
37
37
|
## Regex for parameters
|
38
38
|
PARAM_RE = /^-(-|\.$|[^\d\.])/
|
39
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
|
+
|
40
84
|
## The commandline parser. In typical usage, the methods in this class
|
41
85
|
## will be handled internally by Optimist::options. In this case, only the
|
42
86
|
## #opt, #banner and #version, #depends, and #conflicts methods will
|
@@ -70,8 +114,6 @@ class Parser
|
|
70
114
|
return @registry[lookup].new
|
71
115
|
end
|
72
116
|
|
73
|
-
INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc:
|
74
|
-
|
75
117
|
## The values from the commandline that were not interpreted by #parse.
|
76
118
|
attr_reader :leftovers
|
77
119
|
|
@@ -84,6 +126,12 @@ class Parser
|
|
84
126
|
## ignore options that it does not recognize.
|
85
127
|
attr_accessor :ignore_invalid_options
|
86
128
|
|
129
|
+
DEFAULT_SETTINGS = {
|
130
|
+
exact_match: true,
|
131
|
+
implicit_short_opts: true,
|
132
|
+
suggestions: true
|
133
|
+
}
|
134
|
+
|
87
135
|
## Initializes the parser, and instance-evaluates any block given.
|
88
136
|
def initialize(*a, &b)
|
89
137
|
@version = nil
|
@@ -99,8 +147,17 @@ class Parser
|
|
99
147
|
@synopsis = nil
|
100
148
|
@usage = nil
|
101
149
|
|
102
|
-
|
103
|
-
|
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?
|
104
161
|
end
|
105
162
|
|
106
163
|
## Define an option. +name+ is the option name, a unique identifier
|
@@ -116,6 +173,7 @@ class Parser
|
|
116
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+.
|
117
174
|
## [+:required+] If set to +true+, the argument must be provided on the commandline.
|
118
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.
|
119
177
|
##
|
120
178
|
## Note that there are two types of argument multiplicity: an argument
|
121
179
|
## can take multiple values, e.g. "--arg 1 2 3". An argument can also
|
@@ -150,10 +208,19 @@ class Parser
|
|
150
208
|
o = Option.create(name, desc, opts)
|
151
209
|
|
152
210
|
raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? o.name
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
+
|
157
224
|
@specs[o.name] = o
|
158
225
|
@order << [:opt, o.name]
|
159
226
|
end
|
@@ -190,13 +257,19 @@ class Parser
|
|
190
257
|
## better modeled with Optimist::die.
|
191
258
|
def depends(*syms)
|
192
259
|
syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
|
193
|
-
@constraints <<
|
260
|
+
@constraints << DependConstraint.new(syms)
|
194
261
|
end
|
195
262
|
|
196
263
|
## Marks two (or more!) options as conflicting.
|
197
264
|
def conflicts(*syms)
|
198
265
|
syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
|
199
|
-
@constraints <<
|
266
|
+
@constraints << ConflictConstraint.new(syms)
|
267
|
+
end
|
268
|
+
|
269
|
+
## Marks two (or more!) options as required but mutually exclusive.
|
270
|
+
def either(*syms)
|
271
|
+
syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
|
272
|
+
@constraints << EitherConstraint.new(syms)
|
200
273
|
end
|
201
274
|
|
202
275
|
## Defines a set of words which cause parsing to terminate when
|
@@ -226,6 +299,56 @@ class Parser
|
|
226
299
|
@educate_on_error = true
|
227
300
|
end
|
228
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
|
+
|
229
352
|
## Parses the commandline. Typically called by Optimist::options,
|
230
353
|
## but you can call it directly if you need more control.
|
231
354
|
##
|
@@ -243,7 +366,7 @@ class Parser
|
|
243
366
|
vals[sym] = [] if opts.multi && !opts.default # multi arguments default to [], not nil
|
244
367
|
end
|
245
368
|
|
246
|
-
resolve_default_short_options!
|
369
|
+
resolve_default_short_options! if @settings[:implicit_short_opts]
|
247
370
|
|
248
371
|
## resolve symbols
|
249
372
|
given_args = {}
|
@@ -261,10 +384,15 @@ class Parser
|
|
261
384
|
else raise CommandlineError, "invalid argument syntax: '#{arg}'"
|
262
385
|
end
|
263
386
|
|
264
|
-
|
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
|
265
393
|
|
266
394
|
next nil if ignore_invalid_options && !sym
|
267
|
-
|
395
|
+
handle_unknown_argument(arg, @long.keys, @settings[:suggestions]) unless sym
|
268
396
|
|
269
397
|
if given_args.include?(sym) && !@specs[sym].multi?
|
270
398
|
raise CommandlineError, "option '#{arg}' specified multiple times"
|
@@ -296,20 +424,12 @@ class Parser
|
|
296
424
|
raise HelpNeeded if given_args.include? :help
|
297
425
|
|
298
426
|
## check constraint satisfaction
|
299
|
-
@constraints.each do |
|
300
|
-
|
301
|
-
next unless constraint_sym
|
302
|
-
|
303
|
-
case type
|
304
|
-
when :depends
|
305
|
-
syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym].long} requires --#{@specs[sym].long}" unless given_args.include? sym }
|
306
|
-
when :conflicts
|
307
|
-
syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym].long} conflicts with --#{@specs[sym].long}" if given_args.include?(sym) && (sym != constraint_sym) }
|
308
|
-
end
|
427
|
+
@constraints.each do |const|
|
428
|
+
const.validate(given_args: given_args, specs: @specs)
|
309
429
|
end
|
310
430
|
|
311
431
|
required.each do |sym, val|
|
312
|
-
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
|
313
433
|
end
|
314
434
|
|
315
435
|
## parse parameters
|
@@ -322,6 +442,12 @@ class Parser
|
|
322
442
|
params << (opts.array_default? ? opts.default.clone : [opts.default])
|
323
443
|
end
|
324
444
|
|
445
|
+
if params.first && opts.permitted
|
446
|
+
params.first.each do |val|
|
447
|
+
opts.validate_permitted(arg, val)
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
325
451
|
vals["#{sym}_given".intern] = true # mark argument as specified on the commandline
|
326
452
|
|
327
453
|
vals[sym] = opts.parse(params, negative_given)
|
@@ -382,7 +508,7 @@ class Parser
|
|
382
508
|
|
383
509
|
spec = @specs[opt]
|
384
510
|
stream.printf " %-#{leftcol_width}s ", left[opt]
|
385
|
-
desc = spec.
|
511
|
+
desc = spec.full_description
|
386
512
|
|
387
513
|
stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
|
388
514
|
end
|
@@ -427,7 +553,7 @@ class Parser
|
|
427
553
|
def die(arg, msg = nil, error_code = nil)
|
428
554
|
msg, error_code = nil, msg if msg.kind_of?(Integer)
|
429
555
|
if msg
|
430
|
-
$stderr.puts "Error: argument --#{@specs[arg].long} #{msg}."
|
556
|
+
$stderr.puts "Error: argument --#{@specs[arg].long.long} #{msg}."
|
431
557
|
else
|
432
558
|
$stderr.puts "Error: #{arg}."
|
433
559
|
end
|
@@ -450,7 +576,7 @@ private
|
|
450
576
|
until i >= args.length
|
451
577
|
return remains += args[i..-1] if @stop_words.member? args[i]
|
452
578
|
case args[i]
|
453
|
-
when
|
579
|
+
when "--" # arg terminator
|
454
580
|
return remains += args[(i + 1)..-1]
|
455
581
|
when /^--(\S+?)=(.*)$/ # long argument with equals
|
456
582
|
num_params_taken = yield "--#{$1}", [$2]
|
@@ -475,7 +601,7 @@ private
|
|
475
601
|
end
|
476
602
|
i += 1
|
477
603
|
when /^-(\S+)$/ # one or more short arguments
|
478
|
-
short_remaining =
|
604
|
+
short_remaining = []
|
479
605
|
shortargs = $1.split(//)
|
480
606
|
shortargs.each_with_index do |a, j|
|
481
607
|
if j == (shortargs.length - 1)
|
@@ -485,7 +611,7 @@ private
|
|
485
611
|
unless num_params_taken
|
486
612
|
short_remaining << a
|
487
613
|
if @stop_on_unknown
|
488
|
-
remains << "-#{short_remaining}"
|
614
|
+
remains << "-#{short_remaining.join}"
|
489
615
|
return remains += args[i + 1..-1]
|
490
616
|
end
|
491
617
|
else
|
@@ -495,8 +621,8 @@ private
|
|
495
621
|
unless yield "-#{a}", []
|
496
622
|
short_remaining << a
|
497
623
|
if @stop_on_unknown
|
498
|
-
short_remaining
|
499
|
-
remains << "-#{short_remaining}"
|
624
|
+
short_remaining << shortargs[j + 1..-1].join
|
625
|
+
remains << "-#{short_remaining.join}"
|
500
626
|
return remains += args[i + 1..-1]
|
501
627
|
end
|
502
628
|
end
|
@@ -504,7 +630,7 @@ private
|
|
504
630
|
end
|
505
631
|
|
506
632
|
unless short_remaining.empty?
|
507
|
-
remains << "-#{short_remaining}"
|
633
|
+
remains << "-#{short_remaining.join}"
|
508
634
|
end
|
509
635
|
i += 1
|
510
636
|
else
|
@@ -533,11 +659,10 @@ private
|
|
533
659
|
def resolve_default_short_options!
|
534
660
|
@order.each do |type, name|
|
535
661
|
opts = @specs[name]
|
536
|
-
next if type != :opt || opts.
|
537
|
-
|
538
|
-
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) }
|
539
664
|
if c # found a character to use
|
540
|
-
opts.short
|
665
|
+
opts.short.add c
|
541
666
|
@short[c] = name
|
542
667
|
end
|
543
668
|
end
|
@@ -563,30 +688,94 @@ private
|
|
563
688
|
ret
|
564
689
|
end
|
565
690
|
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
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}"
|
706
|
+
end
|
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
|
574
760
|
end
|
575
761
|
end
|
762
|
+
|
576
763
|
end
|
577
764
|
|
578
765
|
class Option
|
579
766
|
|
580
|
-
attr_accessor :name, :short, :long, :default
|
767
|
+
attr_accessor :name, :short, :long, :default, :permitted, :permitted_response
|
581
768
|
attr_writer :multi_given
|
582
769
|
|
583
770
|
def initialize
|
584
|
-
@long =
|
585
|
-
@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
|
586
773
|
@name = nil
|
587
774
|
@multi_given = false
|
588
775
|
@hidden = false
|
589
776
|
@default = nil
|
777
|
+
@permitted = nil
|
778
|
+
@permitted_response = "option '%{arg}' only accepts %{valid_string}"
|
590
779
|
@optshash = Hash.new()
|
591
780
|
end
|
592
781
|
|
@@ -613,7 +802,7 @@ class Option
|
|
613
802
|
|
614
803
|
def array_default? ; self.default.kind_of?(Array) ; end
|
615
804
|
|
616
|
-
def
|
805
|
+
def doesnt_need_autogen_short ; !short.auto || short.chars.any? ; end
|
617
806
|
|
618
807
|
def callback ; opts(:callback) ; end
|
619
808
|
def desc ; opts(:desc) ; end
|
@@ -628,23 +817,96 @@ class Option
|
|
628
817
|
def type_format ; "" ; end
|
629
818
|
|
630
819
|
def educate
|
631
|
-
|
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}" : "")
|
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
|
632
842
|
end
|
633
843
|
|
634
|
-
##
|
635
|
-
def
|
636
|
-
return desc unless default
|
844
|
+
## Generate the default value string for the educate line
|
845
|
+
private def default_description_str str
|
637
846
|
default_s = case default
|
638
|
-
when $stdout then '<stdout>'
|
639
|
-
when $stdin then '<stdin>'
|
640
|
-
when $stderr then '<stderr>'
|
641
847
|
when Array
|
642
848
|
default.join(', ')
|
643
849
|
else
|
644
|
-
default.to_s
|
850
|
+
format_stdio(default).to_s
|
645
851
|
end
|
646
|
-
defword =
|
647
|
-
|
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})"
|
648
910
|
end
|
649
911
|
|
650
912
|
## Provide a way to register symbol aliases to the Parser
|
@@ -671,10 +933,10 @@ class Option
|
|
671
933
|
opt_inst = (opttype || opttype_from_default || Optimist::BooleanOption.new)
|
672
934
|
|
673
935
|
## fill in :long
|
674
|
-
opt_inst.long
|
936
|
+
opt_inst.long.set(name, opts[:long], opts[:alt])
|
675
937
|
|
676
938
|
## fill in :short
|
677
|
-
opt_inst.short
|
939
|
+
opt_inst.short.add opts[:short]
|
678
940
|
|
679
941
|
## fill in :multi
|
680
942
|
multi_given = opts[:multi] || false
|
@@ -683,8 +945,13 @@ class Option
|
|
683
945
|
## fill in :default for flags
|
684
946
|
defvalue = opts[:default] || opt_inst.default
|
685
947
|
|
948
|
+
## fill in permitted values
|
949
|
+
permitted = opts[:permitted] || nil
|
950
|
+
|
686
951
|
## autobox :default for :multi (multi-occurrence) arguments
|
687
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]
|
688
955
|
opt_inst.default = defvalue
|
689
956
|
opt_inst.name = name
|
690
957
|
opt_inst.opts = opts
|
@@ -723,29 +990,6 @@ class Option
|
|
723
990
|
return Optimist::Parser.registry_getopttype(type_from_default)
|
724
991
|
end
|
725
992
|
|
726
|
-
def self.handle_long_opt(lopt, name)
|
727
|
-
lopt = lopt ? lopt.to_s : name.to_s.gsub("_", "-")
|
728
|
-
lopt = case lopt
|
729
|
-
when /^--([^-].*)$/ then $1
|
730
|
-
when /^[^-]/ then lopt
|
731
|
-
else raise ArgumentError, "invalid long option name #{lopt.inspect}"
|
732
|
-
end
|
733
|
-
end
|
734
|
-
|
735
|
-
def self.handle_short_opt(sopt)
|
736
|
-
sopt = sopt.to_s if sopt && sopt != :none
|
737
|
-
sopt = case sopt
|
738
|
-
when /^-(.)$/ then $1
|
739
|
-
when nil, :none, /^.$/ then sopt
|
740
|
-
else raise ArgumentError, "invalid short option name '#{sopt.inspect}'"
|
741
|
-
end
|
742
|
-
|
743
|
-
if sopt
|
744
|
-
raise ArgumentError, "a short option name can't be a number or a dash" if sopt =~ ::Optimist::Parser::INVALID_SHORT_ARG_REGEX
|
745
|
-
end
|
746
|
-
return sopt
|
747
|
-
end
|
748
|
-
|
749
993
|
end
|
750
994
|
|
751
995
|
# Flag option. Has no arguments. Can be negated with "no-".
|
@@ -765,11 +1009,12 @@ end
|
|
765
1009
|
class FloatOption < Option
|
766
1010
|
register_alias :float, :double
|
767
1011
|
def type_format ; "=<f>" ; end
|
1012
|
+
def as_type(param) ; param.to_f ; end
|
768
1013
|
def parse(paramlist, _neg_given)
|
769
1014
|
paramlist.map do |pg|
|
770
1015
|
pg.map do |param|
|
771
1016
|
raise CommandlineError, "option '#{self.name}' needs a floating-point number" unless param.is_a?(Numeric) || param =~ FLOAT_RE
|
772
|
-
param
|
1017
|
+
as_type(param)
|
773
1018
|
end
|
774
1019
|
end
|
775
1020
|
end
|
@@ -779,11 +1024,12 @@ end
|
|
779
1024
|
class IntegerOption < Option
|
780
1025
|
register_alias :int, :integer, :fixnum
|
781
1026
|
def type_format ; "=<i>" ; end
|
1027
|
+
def as_type(param) ; param.to_i ; end
|
782
1028
|
def parse(paramlist, _neg_given)
|
783
1029
|
paramlist.map do |pg|
|
784
1030
|
pg.map do |param|
|
785
1031
|
raise CommandlineError, "option '#{self.name}' needs an integer" unless param.is_a?(Numeric) || param =~ /^-?[\d_]+$/
|
786
|
-
param
|
1032
|
+
as_type(param)
|
787
1033
|
end
|
788
1034
|
end
|
789
1035
|
end
|
@@ -816,9 +1062,10 @@ end
|
|
816
1062
|
# Option class for handling Strings.
|
817
1063
|
class StringOption < Option
|
818
1064
|
register_alias :string
|
1065
|
+
def as_type(val) ; val.to_s ; end
|
819
1066
|
def type_format ; "=<s>" ; end
|
820
1067
|
def parse(paramlist, _neg_given)
|
821
|
-
paramlist.map { |pg| pg.map(
|
1068
|
+
paramlist.map { |pg| pg.map { |param| as_type(param) } }
|
822
1069
|
end
|
823
1070
|
end
|
824
1071
|
|
@@ -916,7 +1163,22 @@ end
|
|
916
1163
|
## ## if called with --monkey
|
917
1164
|
## p opts # => {:monkey=>true, :name=>nil, :num_limbs=>4, :help=>false, :monkey_given=>true}
|
918
1165
|
##
|
919
|
-
##
|
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
|
920
1182
|
def options(args = ARGV, *a, &b)
|
921
1183
|
@last_parser = Parser.new(*a, &b)
|
922
1184
|
with_standard_exception_handling(@last_parser) { @last_parser.parse args }
|