optimist_xl 3.1.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.
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/optimist_xl'
3
+
4
+ opts = OptimistXL::options do
5
+ version "cool-script v0.3.2 (code-name: apple-cake)"
6
+ banner self.version ## print out the version in the banner
7
+ banner "drinks"
8
+ opt :juice, "use juice"
9
+ opt :milk, "use milk"
10
+ banner "drink control" ## can be used for categories
11
+ opt :litres, "quantity of liquid", :default => 2.0
12
+ opt :brand, "brand name of the liquid", :type => :string
13
+ banner "other controls"
14
+ end
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/optimist_xl'
3
+
4
+ opts = OptimistXL::options do
5
+ version "cool-script v0.1 (code-name: bananas foster)"
6
+ banner "This script is pretty cool."
7
+ opt :juice, "use juice"
8
+ opt :milk, "use milk"
9
+ opt :litres, "quantity of liquid", :default => 2.0
10
+ opt :brand, "brand name of the liquid", :type => :string
11
+ opt :config, "config file path", :type => String, :required => true
12
+ opt :drinkers, "number of people drinking the liquid", :default => 6
13
+ end
14
+ OptimistXL::die :drinkers, "must be value a greater than zero" if opts[:drinkers] < 1
15
+ OptimistXL::die :config, "must point to an existing file" unless File.exist?(opts[:config]) if opts[:config]
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/optimist_xl'
3
+
4
+ opts = OptimistXL::options do
5
+ opt :french, "starts with french", type: String,
6
+ permitted: %w(fries toast),
7
+ permitted_response: "option %{arg} must be something that starts " +
8
+ "with french, e.g. %{permitted} but you gave '%{given}'"
9
+ opt :dog, "starts with dog", permitted: %r/(house|bone|tail)/, type: String
10
+ opt :zipcode, "zipcode", permitted: %r/^[0-9]{5}$/, default: '39759',
11
+ permitted_response: "option %{arg} must be a zipcode, a five-digit number from 00000..99999"
12
+ end
13
+
14
+ p opts
15
+
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative "../lib/optimist_xl"
3
+
4
+ result = OptimistXL::options do
5
+ opt :global_flag, 'Some global flag'
6
+ subcmd :list, "Show the to-do list" do
7
+ opt :recent, 'list only N-recent items', type: Integer, default: 5
8
+ opt :all, 'list all the things', type: :boolean
9
+ end
10
+ subcmd "create", "Create a to-do item" do
11
+ opt :name, 'item name', type: String
12
+ end
13
+ end
14
+
15
+ p result
data/examples/types.rb ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/optimist_xl'
3
+
4
+ opts = OptimistXL::options do
5
+ opt :xx, "x opt", :type => :string
6
+ opt :yy, "y opt", :type => :float
7
+ opt :zz, "z opt", :type => :integer
8
+ end
9
+ p opts
10
+ puts "xx class is #{opts[:xx].class}"
11
+ puts "yy class is #{opts[:yy].class}"
12
+ puts "zz class is #{opts[:zz].class}"
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+ require_relative '../lib/optimist_xl'
3
+
4
+ class ZipCode
5
+ REGEXP = %r/^(?<zip>[0-9]{5})(\-(?<plusfour>[0-9]{4}))?$/
6
+ def initialize(zipstring)
7
+ matcher = REGEXP.match(zipstring)
8
+ raise "Invalid zip-code" unless matcher
9
+ @zip = matcher[:zip]
10
+ @plusfour = matcher[:plusfour]
11
+ end
12
+ end
13
+
14
+ #module OptimistXL
15
+ class ZipCodeOption < OptimistXL::Option
16
+ # register_alias registers with the global parser.
17
+ register_alias :zipcode
18
+ def type_format ; "=<zip>" ; end # optional for use with help-message
19
+ def parse(paramlist, _neg_given)
20
+ paramlist.map do |plist|
21
+ plist.map do |param_string|
22
+ raise OptimistXL::CommandlineError, "option '#{self.name}' should be formatted as a zipcode" unless param_string=~ ZipCode::REGEXP
23
+ ZipCode.new(param_string)
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ opts = OptimistXL::options do
30
+ opt :zipcode, "United states postal code", :type => :zipcode
31
+ end
32
+
33
+ p opts[:zipcode]
34
+
@@ -0,0 +1,1297 @@
1
+ # lib/optimist_xl.rb -- OptimistXL command-line processing library
2
+ # Copyright (c) 2008-2014 William Morgan.
3
+ # Copyright (c) 2014 Red Hat, Inc.
4
+ # Copyright (c) 2019 Ben Bowers
5
+ # OptimistXL is licensed under the MIT license.
6
+
7
+ require 'date'
8
+
9
+ module OptimistXL
10
+ # note: this is duplicated in gemspec
11
+ # please change over there too
12
+ VERSION = "3.1.1"
13
+
14
+ ## Thrown by Parser in the event of a commandline error. Not needed if
15
+ ## you're using the OptimistXL::options entry.
16
+ class CommandlineError < StandardError
17
+ attr_reader :error_code
18
+
19
+ def initialize(msg, error_code = nil)
20
+ super(msg)
21
+ @error_code = error_code
22
+ end
23
+ end
24
+
25
+ ## Thrown by Parser if the user passes in '-h' or '--help'. Handled
26
+ ## automatically by OptimistXL#options.
27
+ class HelpNeeded < StandardError
28
+ attr_reader :parser
29
+ def initialize(msg=nil, parser: nil)
30
+ super(msg)
31
+ @parser = parser
32
+ end
33
+ end
34
+
35
+ ## Thrown by Parser if the user passes in '-v' or '--version'. Handled
36
+ ## automatically by OptimistXL#options.
37
+ class VersionNeeded < StandardError
38
+ end
39
+
40
+ ## Regex for floating point numbers
41
+ FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))([eE][-+]?[\d]+)?$/
42
+
43
+ ## Regex for parameters
44
+ PARAM_RE = /^-(-|\.$|[^\d\.])/
45
+
46
+ ## The commandline parser. In typical usage, the methods in this class
47
+ ## will be handled internally by OptimistXL::options. In this case, only the
48
+ ## #opt, #banner and #version, #depends, and #conflicts methods will
49
+ ## typically be called.
50
+ ##
51
+ ## If you want to instantiate this class yourself (for more complicated
52
+ ## argument-parsing logic), call #parse to actually produce the output hash,
53
+ ## and consider calling it from within
54
+ ## OptimistXL::with_standard_exception_handling.
55
+ class Parser
56
+
57
+ ## The registry is a class-instance-variable map of option aliases to their subclassed Option class.
58
+ @registry = {}
59
+
60
+ ## The Option subclasses are responsible for registering themselves using this function.
61
+ def self.register(lookup, klass)
62
+ @registry[lookup.to_sym] = klass
63
+ end
64
+
65
+ ## Gets the class from the registry.
66
+ ## Can be given either a class-name, e.g. Integer, a string, e.g "integer", or a symbol, e.g :integer
67
+ def self.registry_getopttype(type)
68
+ return nil unless type
69
+ if type.respond_to?(:name)
70
+ type = type.name
71
+ lookup = type.downcase.to_sym
72
+ else
73
+ lookup = type.to_sym
74
+ end
75
+ raise ArgumentError, "Unsupported argument type '#{type}', registry lookup '#{lookup}'" unless @registry.has_key?(lookup)
76
+ return @registry[lookup].new
77
+ end
78
+
79
+ INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc:
80
+
81
+ ## The values from the commandline that were not interpreted by #parse.
82
+ attr_reader :leftovers
83
+
84
+ ## The complete configuration hashes for each option. (Mainly useful
85
+ ## for testing.)
86
+ attr_reader :specs
87
+
88
+ ## A flag that determines whether or not to raise an error if the parser is passed one or more
89
+ ## options that were not registered ahead of time. If 'true', then the parser will simply
90
+ ## ignore options that it does not recognize.
91
+ attr_accessor :ignore_invalid_options
92
+
93
+
94
+ DEFAULT_SETTINGS = { exact_match: false,
95
+ explicit_short_opts: false,
96
+ suggestions: true
97
+ }
98
+
99
+ ## Initializes the parser, and instance-evaluates any block given.
100
+ def initialize(*a, &b)
101
+ @version = nil
102
+ @leftovers = []
103
+ @specs = {}
104
+ @long = {}
105
+ @short = {}
106
+ @order = []
107
+ @constraints = []
108
+ @stop_words = []
109
+ @stop_on_unknown = false
110
+ @educate_on_error = false
111
+ @synopsis = nil
112
+ @usage = nil
113
+ @subcommand_parsers = {}
114
+
115
+ ## allow passing settings through Parser.new as an optional hash.
116
+ ## but keep compatibility with non-hashy args, though.
117
+ begin
118
+ settings_hash = Hash[*a]
119
+ @settings = DEFAULT_SETTINGS.merge(settings_hash)
120
+ a=[] ## clear out args if using as settings-hash
121
+ rescue ArgumentError
122
+ @settings = DEFAULT_SETTINGS
123
+ end
124
+
125
+ # instance_eval(&b) if b # can't take arguments
126
+ #cloaker(&b).bind(self).call(*a) if b
127
+ self.instance_exec(*a, &b) if block_given?
128
+ end
129
+
130
+ ## Define an option. +name+ is the option name, a unique identifier
131
+ ## for the option that you will use internally, which should be a
132
+ ## symbol or a string. +desc+ is a string description which will be
133
+ ## displayed in help messages.
134
+ ##
135
+ ## Takes the following optional arguments:
136
+ ##
137
+ ## [+:long+] Specify the long form of the argument, i.e. the form with two dashes. If unspecified, will be automatically derived based on the argument name by turning the +name+ option into a string, and replacing any _'s by -'s.
138
+ ## [+:short+] Specify the short form of the argument, i.e. the form with one dash. If unspecified, will be automatically derived from +name+. Use :none: to not have a short value.
139
+ ## [+:type+] Require that the argument take a parameter or parameters of type +type+. For a single parameter, the value can be a member of +SINGLE_ARG_TYPES+, or a corresponding Ruby class (e.g. +Integer+ for +:int+). For multiple-argument parameters, the value can be any member of +MULTI_ARG_TYPES+ constant. If unset, the default argument type is +:flag+, meaning that the argument does not take a parameter. The specification of +:type+ is not necessary if a +:default+ is given.
140
+ ## [+:default+] Set the default value for an argument. Without a default value, the hash returned by #parse (and thus OptimistXL::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+.
141
+ ## [+:required+] If set to +true+, the argument must be provided on the commandline.
142
+ ## [+: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.)
143
+ ##
144
+ ## Note that there are two types of argument multiplicity: an argument
145
+ ## can take multiple values, e.g. "--arg 1 2 3". An argument can also
146
+ ## be allowed to occur multiple times, e.g. "--arg 1 --arg 2".
147
+ ##
148
+ ## Arguments that take multiple values should have a +:type+ parameter
149
+ ## drawn from +MULTI_ARG_TYPES+ (e.g. +:strings+), or a +:default:+
150
+ ## value of an array of the correct type (e.g. [String]). The
151
+ ## value of this argument will be an array of the parameters on the
152
+ ## commandline.
153
+ ##
154
+ ## Arguments that can occur multiple times should be marked with
155
+ ## +:multi+ => +true+. The value of this argument will also be an array.
156
+ ## In contrast with regular non-multi options, if not specified on
157
+ ## the commandline, the default value will be [], not nil.
158
+ ##
159
+ ## These two attributes can be combined (e.g. +:type+ => +:strings+,
160
+ ## +:multi+ => +true+), in which case the value of the argument will be
161
+ ## an array of arrays.
162
+ ##
163
+ ## There's one ambiguous case to be aware of: when +:multi+: is true and a
164
+ ## +:default+ is set to an array (of something), it's ambiguous whether this
165
+ ## is a multi-value argument as well as a multi-occurrence argument.
166
+ ## In thise case, OptimistXL assumes that it's not a multi-value argument.
167
+ ## If you want a multi-value, multi-occurrence argument with a default
168
+ ## value, you must specify +:type+ as well.
169
+
170
+ def opt(name, desc = "", opts = {}, &b)
171
+ opts[:callback] ||= b if block_given?
172
+ opts[:desc] ||= desc
173
+
174
+ o = Option.create(name, desc, opts)
175
+
176
+ raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? o.name
177
+ raise ArgumentError, "long option name #{o.long.inspect} is already taken; please specify a (different) :long" if @long[o.long]
178
+ raise ArgumentError, "short option name #{o.short.inspect} is already taken; please specify a (different) :short" if @short[o.short]
179
+ raise ArgumentError, "permitted values for option #{o.long.inspect} must be either nil, Range, Regexp or an Array;" unless o.permitted_type_valid?
180
+ @long[o.long] = o.name
181
+ @short[o.short] = o.name if o.short?
182
+ @specs[o.name] = o
183
+ @order << [:opt, o.name]
184
+ end
185
+
186
+ def subcmd(name, desc=nil, args = {}, &b)
187
+ sc = SubcommandParser.new(name, desc, *args, &b)
188
+ @subcommand_parsers[name.to_sym] = sc
189
+ return sc
190
+ end
191
+
192
+ ## Sets the version string. If set, the user can request the version
193
+ ## on the commandline. Should probably be of the form "<program name>
194
+ ## <version number>".
195
+ def version(s = nil)
196
+ s ? @version = s : @version
197
+ end
198
+
199
+ ## Sets the usage string. If set the message will be printed as the
200
+ ## first line in the help (educate) output and ending in two new
201
+ ## lines.
202
+ def usage(s = nil)
203
+ s ? @usage = s : @usage
204
+ end
205
+
206
+ ## Adds a synopsis (command summary description) right below the
207
+ ## usage line, or as the first line if usage isn't specified.
208
+ def synopsis(s = nil)
209
+ s ? @synopsis = s : @synopsis
210
+ end
211
+
212
+ ## Adds text to the help display. Can be interspersed with calls to
213
+ ## #opt to build a multi-section help page.
214
+ def banner(s)
215
+ @order << [:text, s]
216
+ end
217
+ alias_method :text, :banner
218
+
219
+ ## Marks two (or more!) options as requiring each other. Only handles
220
+ ## undirected (i.e., mutual) dependencies. Directed dependencies are
221
+ ## better modeled with OptimistXL::die.
222
+ def depends(*syms)
223
+ syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
224
+ @constraints << [:depends, syms]
225
+ end
226
+
227
+ ## Marks two (or more!) options as conflicting.
228
+ def conflicts(*syms)
229
+ syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
230
+ @constraints << [:conflicts, syms]
231
+ end
232
+
233
+ ## Defines a set of words which cause parsing to terminate when
234
+ ## encountered, such that any options to the left of the word are
235
+ ## parsed as usual, and options to the right of the word are left
236
+ ## intact.
237
+ ##
238
+ ## A typical use case would be for subcommand support, where these
239
+ ## would be set to the list of subcommands. A subsequent OptimistXL
240
+ ## invocation would then be used to parse subcommand options, after
241
+ ## shifting the subcommand off of ARGV.
242
+ def stop_on(*words)
243
+ @stop_words = [*words].flatten
244
+ end
245
+
246
+ ## Similar to #stop_on, but stops on any unknown word when encountered
247
+ ## (unless it is a parameter for an argument). This is useful for
248
+ ## cases where you don't know the set of subcommands ahead of time,
249
+ ## i.e., without first parsing the global options.
250
+ def stop_on_unknown
251
+ @stop_on_unknown = true
252
+ end
253
+
254
+ ## Instead of displaying "Try --help for help." on an error
255
+ ## display the usage (via educate)
256
+ def educate_on_error
257
+ @educate_on_error = true
258
+ end
259
+
260
+ ## Match long variables with inexact match.
261
+ ## If we hit a complete match, then use that, otherwise see how many long-options partially match.
262
+ ## If only one partially matches, then we can safely use that.
263
+ ## Otherwise, we raise an error that the partially given option was ambiguous.
264
+ def perform_inexact_match(arg, partial_match) # :nodoc:
265
+ return @long[partial_match] if @long.has_key?(partial_match)
266
+ partially_matched_keys = @long.keys.grep(/^#{partial_match}/)
267
+ return case partially_matched_keys.size
268
+ when 0 ; nil
269
+ when 1 ; @long[partially_matched_keys.first]
270
+ else ; raise CommandlineError, "ambiguous option '#{arg}' matched keys (#{partially_matched_keys.join(',')})"
271
+ end
272
+ end
273
+ private :perform_inexact_match
274
+
275
+ def handle_unknown_argument(arg, candidates, suggestions)
276
+ errstring = "unknown argument '#{arg}'"
277
+ errstring += " for command '#{subcommand_name}'" if self.respond_to?(:subcommand_name)
278
+ if (suggestions &&
279
+ Module::const_defined?("DidYouMean") &&
280
+ Module::const_defined?("DidYouMean::JaroWinkler") &&
281
+ Module::const_defined?("DidYouMean::Levenshtein"))
282
+ input = arg.sub(/^[-]*/,'')
283
+
284
+ # Code borrowed from did_you_mean gem
285
+ jw_threshold = 0.75
286
+ seed = candidates.select {|candidate| DidYouMean::JaroWinkler.distance(candidate, input) >= jw_threshold } \
287
+ .sort_by! {|candidate| DidYouMean::JaroWinkler.distance(candidate.to_s, input) } \
288
+ .reverse!
289
+ # Correct mistypes
290
+ threshold = (input.length * 0.25).ceil
291
+ has_mistype = seed.rindex {|c| DidYouMean::Levenshtein.distance(c, input) <= threshold }
292
+ corrections = if has_mistype
293
+ seed.take(has_mistype + 1)
294
+ else
295
+ # Correct misspells
296
+ seed.select do |candidate|
297
+ length = input.length < candidate.length ? input.length : candidate.length
298
+
299
+ DidYouMean::Levenshtein.distance(candidate, input) < length
300
+ end.first(1)
301
+ end
302
+ unless corrections.empty?
303
+ dashdash_corrections = corrections.map{|s| "--#{s}" }
304
+ errstring << ". Did you mean: [#{dashdash_corrections.join(', ')}] ?"
305
+ end
306
+ end
307
+ raise CommandlineError, errstring
308
+ end
309
+ private :handle_unknown_argument
310
+
311
+ # Provide a list of given subcommands.
312
+ # List will be empty if subcmd was never given.
313
+ def subcommands
314
+ @subcommand_parsers.keys
315
+ end
316
+
317
+ ## Parses the commandline. Typically called by OptimistXL::options,
318
+ ## but you can call it directly if you need more control.
319
+ ##
320
+ ## throws CommandlineError, HelpNeeded, and VersionNeeded exceptions.
321
+ def parse(cmdline = ARGV)
322
+ if subcommands.empty?
323
+ parse_base(cmdline)
324
+ else
325
+ # set state for subcommand-parse
326
+ @stop_words += subcommands
327
+ @stop_on_unknown = true
328
+ # parse global options
329
+ global_result = parse_base(cmdline)
330
+ # grab subcommand
331
+ cmd = cmdline.shift
332
+ raise CommandlineError.new('no subcommand provided') unless cmd
333
+ # parse subcommand options
334
+ subcmd_parser = @subcommand_parsers[cmd.to_sym]
335
+ raise CommandlineError.new("unknown subcommand '#{cmd}'") unless subcmd_parser
336
+ subcmd_result = subcmd_parser.parse_base(cmdline)
337
+ SubcommandResult.new(subcommand: cmd,
338
+ global_options: global_result,
339
+ subcommand_options: subcmd_result,
340
+ leftovers: subcmd_parser.leftovers)
341
+ end
342
+ end
343
+
344
+ def parse_base(cmdline = ARGV)
345
+ vals = {}
346
+ required = {}
347
+
348
+ # create default version/help options if not already defined
349
+ opt :version, "Print version and exit" if @version && ! (@specs[:version] || @long["version"])
350
+ opt :help, "Show this message" unless @specs[:help] || @long["help"]
351
+
352
+ @specs.each do |sym, opts|
353
+ required[sym] = true if opts.required?
354
+ vals[sym] = opts.default
355
+ vals[sym] = [] if opts.multi && !opts.default # multi arguments default to [], not nil
356
+ end
357
+
358
+ resolve_default_short_options! unless @settings[:explicit_short_opts]
359
+
360
+ ## resolve symbols
361
+ given_args = {}
362
+ @leftovers = each_arg cmdline do |original_arg, params|
363
+ ## handle --no- forms
364
+ arg, negative_given = if original_arg =~ /^--no-([^-]\S*)$/
365
+ ["--#{$1}", true]
366
+ else
367
+ [original_arg, false]
368
+ end
369
+
370
+ sym = case arg
371
+ when /^-([^-])$/ then @short[$1]
372
+ when /^--([^-]\S*)$/ then @long[$1] || @long["no-#{$1}"]
373
+ else raise CommandlineError, "invalid argument syntax: '#{arg}'"
374
+ end
375
+
376
+ if arg =~ /--no-/ # explicitly invalidate --no-no- arguments
377
+ sym = nil
378
+ elsif !sym && !@settings[:exact_match] && arg.match(/^--(\S*)$/)
379
+ # If sym is not already found in the short/long lookup then
380
+ # support inexact matching of long-arguments like perl's Getopt::Long
381
+ sym = perform_inexact_match(arg, $1)
382
+ end
383
+
384
+ next nil if ignore_invalid_options && !sym
385
+
386
+ handle_unknown_argument(arg, @long.keys, @settings[:suggestions]) unless sym
387
+
388
+ if given_args.include?(sym) && !@specs[sym].multi?
389
+ raise CommandlineError, "option '#{arg}' specified multiple times"
390
+ end
391
+
392
+ given_args[sym] ||= {}
393
+ given_args[sym][:arg] = arg
394
+ given_args[sym][:negative_given] = negative_given
395
+ given_args[sym][:params] ||= []
396
+
397
+ # The block returns the number of parameters taken.
398
+ num_params_taken = 0
399
+
400
+ unless params.empty?
401
+ if @specs[sym].single_arg?
402
+ given_args[sym][:params] << params[0, 1] # take the first parameter
403
+ num_params_taken = 1
404
+ elsif @specs[sym].multi_arg?
405
+ given_args[sym][:params] << params # take all the parameters
406
+ num_params_taken = params.size
407
+ end
408
+ end
409
+
410
+ num_params_taken
411
+ end
412
+
413
+ ## check for version and help args, and raise if set.
414
+ ## HelpNeeded should pass the parser object so we know how to educate
415
+ ## if we are in a global-command or subcommand
416
+ raise VersionNeeded if given_args.include? :version
417
+ raise HelpNeeded.new(nil, parser: self) if given_args.include? :help
418
+
419
+ ## check constraint satisfaction
420
+ @constraints.each do |type, syms|
421
+ constraint_sym = syms.find { |sym| given_args[sym] }
422
+ next unless constraint_sym
423
+
424
+ case type
425
+ when :depends
426
+ syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym].long} requires --#{@specs[sym].long}" unless given_args.include? sym }
427
+ when :conflicts
428
+ syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym].long} conflicts with --#{@specs[sym].long}" if given_args.include?(sym) && (sym != constraint_sym) }
429
+ end
430
+ end
431
+
432
+ required.each do |sym, val|
433
+ raise CommandlineError, "option --#{@specs[sym].long} must be specified" unless given_args.include? sym
434
+ end
435
+
436
+ ## parse parameters
437
+ given_args.each do |sym, given_data|
438
+ arg, params, negative_given = given_data.values_at :arg, :params, :negative_given
439
+
440
+ opts = @specs[sym]
441
+ if params.empty? && !opts.flag?
442
+ raise CommandlineError, "option '#{arg}' needs a parameter" unless opts.default
443
+ params << (opts.array_default? ? opts.default.clone : [opts.default])
444
+ end
445
+
446
+ if params.first && opts.permitted
447
+ params.first.each do |val|
448
+ opts.validate_permitted(arg, val)
449
+ end
450
+ end
451
+
452
+ vals["#{sym}_given".intern] = true # mark argument as specified on the commandline
453
+
454
+ vals[sym] = opts.parse(params, negative_given)
455
+
456
+ if opts.single_arg?
457
+ if opts.multi? # multiple options, each with a single parameter
458
+ vals[sym] = vals[sym].map { |p| p[0] }
459
+ else # single parameter
460
+ vals[sym] = vals[sym][0][0]
461
+ end
462
+ elsif opts.multi_arg? && !opts.multi?
463
+ vals[sym] = vals[sym][0] # single option, with multiple parameters
464
+ end
465
+ # else: multiple options, with multiple parameters
466
+
467
+ opts.callback.call(vals[sym]) if opts.callback
468
+ end
469
+
470
+ ## modify input in place with only those
471
+ ## arguments we didn't process
472
+ cmdline.clear
473
+ @leftovers.each { |l| cmdline << l }
474
+
475
+ ## allow openstruct-style accessors
476
+ class << vals
477
+ def method_missing(m, *_args)
478
+ self[m] || self[m.to_s]
479
+ end
480
+ end
481
+ vals
482
+ end
483
+
484
+ # Create default text banner in a string so we can override it
485
+ # in the SubcommandParser class.
486
+ def default_banner
487
+ command_name = File.basename($0).gsub(/\.[^.]+$/, '')
488
+ bannertext = ''
489
+ bannertext << "Usage: #{command_name} #{@usage}\n" if @usage
490
+ bannertext << "#{@synopsis}\n" if @synopsis
491
+ bannertext << "\n" if @usage || @synopsis
492
+ bannertext << "#{@version}\n" if @version
493
+ unless subcommands.empty?
494
+ bannertext << "\n" if @version
495
+ bannertext << "Commands:\n"
496
+ @subcommand_parsers.each_value do |scmd|
497
+ bannertext << sprintf(" %-20s %s\n", scmd.name, scmd.desc)
498
+ end
499
+ bannertext << "\n"
500
+ end
501
+ bannertext << "Options:\n"
502
+ return bannertext
503
+ end
504
+
505
+
506
+ ## Print the help message to +stream+.
507
+ def educate(stream = $stdout)
508
+ width # hack: calculate it now; otherwise we have to be careful not to
509
+ # call this unless the cursor's at the beginning of a line.
510
+
511
+ left = {}
512
+ @specs.each { |name, spec| left[name] = spec.educate }
513
+
514
+ leftcol_width = left.values.map(&:length).max || 0
515
+ rightcol_start = leftcol_width + 6 # spaces
516
+
517
+ # print a default banner here if there is no text/banner
518
+ unless @order.size > 0 && @order.first.first == :text
519
+ stream.puts default_banner()
520
+ end
521
+
522
+ @order.each do |what, opt|
523
+ if what == :text
524
+ # print text/banner here
525
+ stream.puts wrap(opt)
526
+ next
527
+ end
528
+
529
+ spec = @specs[opt]
530
+ stream.printf " %-#{leftcol_width}s ", left[opt]
531
+ desc = spec.full_description
532
+
533
+ stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
534
+ end
535
+ end
536
+
537
+ def width #:nodoc:
538
+ @width ||= if $stdout.tty?
539
+ begin
540
+ require 'io/console'
541
+ w = IO.console.winsize.last
542
+ w.to_i > 0 ? w : 80
543
+ rescue LoadError, NoMethodError, Errno::ENOTTY, Errno::EBADF, Errno::EINVAL
544
+ legacy_width
545
+ end
546
+ else
547
+ 80
548
+ end
549
+ end
550
+
551
+ def legacy_width
552
+ # Support for older Rubies where io/console is not available
553
+ `tput cols`.to_i
554
+ rescue Errno::ENOENT
555
+ 80
556
+ end
557
+ private :legacy_width
558
+
559
+ def wrap(str, opts = {}) # :nodoc:
560
+ if str == ""
561
+ [""]
562
+ else
563
+ inner = false
564
+ str.split("\n").map do |s|
565
+ line = wrap_line s, opts.merge(:inner => inner)
566
+ inner = true
567
+ line
568
+ end.flatten
569
+ end
570
+ end
571
+
572
+ ## The per-parser version of OptimistXL::die (see that for documentation).
573
+ def die(arg, msg = nil, error_code = nil)
574
+ msg, error_code = nil, msg if msg.kind_of?(Integer)
575
+ if msg
576
+ $stderr.puts "Error: argument --#{@specs[arg].long} #{msg}."
577
+ else
578
+ $stderr.puts "Error: #{arg}."
579
+ end
580
+ if @educate_on_error
581
+ $stderr.puts
582
+ educate $stderr
583
+ else
584
+ $stderr.puts "Try --help for help."
585
+ end
586
+ exit(error_code || -1)
587
+ end
588
+
589
+ private
590
+
591
+ ## yield successive arg, parameter pairs
592
+ def each_arg(args)
593
+ remains = []
594
+ i = 0
595
+
596
+ until i >= args.length
597
+ return remains += args[i..-1] if @stop_words.member? args[i]
598
+ case args[i]
599
+ when /^--$/ # arg terminator
600
+ return remains += args[(i + 1)..-1]
601
+ when /^--(\S+?)=(.*)$/ # long argument with equals
602
+ num_params_taken = yield "--#{$1}", [$2]
603
+ if num_params_taken.nil?
604
+ remains << args[i]
605
+ if @stop_on_unknown
606
+ return remains += args[i + 1..-1]
607
+ end
608
+ end
609
+ i += 1
610
+ when /^--(\S+)$/ # long argument
611
+ params = collect_argument_parameters(args, i + 1)
612
+ num_params_taken = yield args[i], params
613
+
614
+ if num_params_taken.nil?
615
+ remains << args[i]
616
+ if @stop_on_unknown
617
+ return remains += args[i + 1..-1]
618
+ end
619
+ else
620
+ i += num_params_taken
621
+ end
622
+ i += 1
623
+ when /^-(\S+)$/ # one or more short arguments
624
+ short_remaining = ""
625
+ shortargs = $1.split(//)
626
+ shortargs.each_with_index do |a, j|
627
+ if j == (shortargs.length - 1)
628
+ params = collect_argument_parameters(args, i + 1)
629
+
630
+ num_params_taken = yield "-#{a}", params
631
+ unless num_params_taken
632
+ short_remaining << a
633
+ if @stop_on_unknown
634
+ remains << "-#{short_remaining}"
635
+ return remains += args[i + 1..-1]
636
+ end
637
+ else
638
+ i += num_params_taken
639
+ end
640
+ else
641
+ unless yield "-#{a}", []
642
+ short_remaining << a
643
+ if @stop_on_unknown
644
+ short_remaining += shortargs[j + 1..-1].join
645
+ remains << "-#{short_remaining}"
646
+ return remains += args[i + 1..-1]
647
+ end
648
+ end
649
+ end
650
+ end
651
+
652
+ unless short_remaining.empty?
653
+ remains << "-#{short_remaining}"
654
+ end
655
+ i += 1
656
+ else
657
+ if @stop_on_unknown
658
+ return remains += args[i..-1]
659
+ else
660
+ remains << args[i]
661
+ i += 1
662
+ end
663
+ end
664
+ end
665
+
666
+ remains
667
+ end
668
+
669
+ def collect_argument_parameters(args, start_at)
670
+ params = []
671
+ pos = start_at
672
+ while args[pos] && args[pos] !~ PARAM_RE && !@stop_words.member?(args[pos]) do
673
+ params << args[pos]
674
+ pos += 1
675
+ end
676
+ params
677
+ end
678
+
679
+ def resolve_default_short_options!
680
+ @order.each do |type, name|
681
+ opts = @specs[name]
682
+ next if type != :opt || opts.short
683
+
684
+ c = opts.long.split(//).find { |d| d !~ INVALID_SHORT_ARG_REGEX && !@short.member?(d) }
685
+ if c # found a character to use
686
+ opts.short = c
687
+ @short[c] = name
688
+ end
689
+ end
690
+ end
691
+
692
+ def wrap_line(str, opts = {})
693
+ prefix = opts[:prefix] || 0
694
+ width = opts[:width] || (self.width - 1)
695
+ start = 0
696
+ ret = []
697
+ until start > str.length
698
+ nextt =
699
+ if start + width >= str.length
700
+ str.length
701
+ else
702
+ x = str.rindex(/\s/, start + width)
703
+ x = str.index(/\s/, start) if x && x < start
704
+ x || str.length
705
+ end
706
+ ret << ((ret.empty? && !opts[:inner]) ? "" : " " * prefix) + str[start...nextt]
707
+ start = nextt + 1
708
+ end
709
+ ret
710
+ end
711
+
712
+ ## instance_eval but with ability to handle block arguments
713
+ ## thanks to _why: http://redhanded.hobix.com/inspect/aBlockCostume.html
714
+ def cloaker(&b)
715
+ (class << self; self; end).class_eval do
716
+ define_method :cloaker_, &b
717
+ meth = instance_method :cloaker_
718
+ remove_method :cloaker_
719
+ meth
720
+ end
721
+ end
722
+ end
723
+
724
+ # If used with subcommands, then return this object instead of a Hash.
725
+ class SubcommandResult
726
+ def initialize(subcommand: nil, global_options: {}, subcommand_options: {}, leftovers: [])
727
+ @subcommand = subcommand
728
+ @global_options = global_options
729
+ @subcommand_options = subcommand_options
730
+ @leftovers = leftovers
731
+ end
732
+ attr_accessor :subcommand, :global_options, :subcommand_options, :leftovers
733
+ end
734
+
735
+ class SubcommandParser < Parser
736
+ attr_reader :name, :desc
737
+ def initialize(name, desc, *a, &b)
738
+ super(a, &b)
739
+ @name = name
740
+ @desc = desc
741
+ end
742
+
743
+ # alias to make referencing more obvious.
744
+ def subcommand_name
745
+ @name
746
+ end
747
+
748
+ def default_banner()
749
+ command_name = File.basename($0).gsub(/\.[^.]+$/, '')
750
+ bannertext = ''
751
+ bannertext << "Usage: #{command_name} #{@name} #{@usage}\n\n" if @usage
752
+ bannertext << "#{@synopsis}\n\n" if @synopsis
753
+ bannertext << "#{desc}\n\n" if @desc
754
+ bannertext << "Options:\n"
755
+ return bannertext
756
+ end
757
+
758
+ end
759
+
760
+ class Option
761
+
762
+ attr_accessor :name, :short, :long, :default, :permitted, :permitted_response
763
+ attr_writer :multi_given
764
+
765
+ def initialize
766
+ @long = nil
767
+ @short = nil
768
+ @name = nil
769
+ @multi_given = false
770
+ @hidden = false
771
+ @default = nil
772
+ @permitted = nil
773
+ @permitted_response = "option '%{arg}' only accepts %{valid_string}"
774
+ @optshash = Hash.new()
775
+ end
776
+
777
+ def opts(key)
778
+ @optshash[key]
779
+ end
780
+
781
+ def opts=(o)
782
+ @optshash = o
783
+ end
784
+
785
+ ## Indicates a flag option, which is an option without an argument
786
+ def flag? ; false ; end
787
+ def single_arg?
788
+ !self.multi_arg? && !self.flag?
789
+ end
790
+
791
+ def multi ; @multi_given ; end
792
+ alias multi? multi
793
+
794
+ ## Indicates that this is a multivalued (Array type) argument
795
+ def multi_arg? ; false ; end
796
+ ## note: Option-Types with both multi_arg? and flag? false are single-parameter (normal) options.
797
+
798
+ def array_default? ; self.default.kind_of?(Array) ; end
799
+
800
+ def short? ; short && short != :none ; end
801
+
802
+ def callback ; opts(:callback) ; end
803
+ def desc ; opts(:desc) ; end
804
+
805
+ def required? ; opts(:required) ; end
806
+
807
+ def parse(_paramlist, _neg_given)
808
+ raise NotImplementedError, "parse must be overridden for newly registered type"
809
+ end
810
+
811
+ # provide type-format string. default to empty, but user should probably override it
812
+ def type_format ; "" ; end
813
+
814
+ def educate
815
+ (short? ? "-#{short}, " : "") + "--#{long}" + type_format + (flag? && default ? ", --no-#{long}" : "")
816
+ end
817
+
818
+ ## Format the educate-line description including the default and permitted value(s)
819
+ def full_description
820
+ desc_str = desc
821
+ desc_str = description_with_default desc_str if default
822
+ desc_str = description_with_permitted desc_str if permitted
823
+ desc_str
824
+ end
825
+
826
+ ## Format stdio like objects to a string
827
+ def format_stdio(obj)
828
+ case obj
829
+ when $stdout then '<stdout>'
830
+ when $stdin then '<stdin>'
831
+ when $stderr then '<stderr>'
832
+ else obj # pass-through-case
833
+ end
834
+ end
835
+
836
+ ## Format the educate-line description including the default-value(s)
837
+ def description_with_default(str)
838
+ return str unless default
839
+ default_s = case default
840
+ when Array
841
+ default.join(', ')
842
+ else
843
+ format_stdio(default).to_s
844
+ end
845
+ return "#{str} (Default: #{default_s})"
846
+ end
847
+
848
+ ## Format the educate-line description including the permitted-value(s)
849
+ def description_with_permitted(str)
850
+ permitted_s = case permitted
851
+ when Array
852
+ permitted.map do |p|
853
+ format_stdio(p).to_s
854
+ end.join(', ')
855
+ when Range
856
+ permitted.to_a.map(&:to_s).join(', ')
857
+ when Regexp
858
+ permitted.to_s
859
+ end
860
+ return "#{str} (Permitted: #{permitted_s})"
861
+ end
862
+
863
+ def permitted_valid_string
864
+ case permitted
865
+ when Array
866
+ return "one of: " + permitted.to_a.map(&:to_s).join(', ')
867
+ when Range
868
+ return "value in range of: #{permitted.to_s}"
869
+ when Regexp
870
+ return "value matching: #{permitted.inspect}"
871
+ end
872
+ raise StandardError, "invalid branch"
873
+ end
874
+
875
+ def permitted_type_valid?
876
+ return true if permitted.nil?
877
+ return true if permitted.is_a? Array
878
+ return true if permitted.is_a? Range
879
+ return true if permitted.is_a? Regexp
880
+ false
881
+ end
882
+
883
+ def validate_permitted(arg, value)
884
+ return true if permitted.nil?
885
+ unless permitted_value?(value)
886
+ format_hash = {arg: arg, given: value, value: value, valid_string: permitted_valid_string(), permitted: permitted }
887
+ raise CommandlineError, permitted_response % format_hash
888
+ end
889
+ true
890
+ end
891
+
892
+ # incoming values from the command-line should be strings, so we should
893
+ # stringify any permitted types as the basis of comparison.
894
+ def permitted_value?(val)
895
+ case permitted
896
+ when nil then true
897
+ when Regexp then val.match permitted
898
+ when Range then permitted.to_a.map(&:to_s).include? val
899
+ when Array then permitted.map(&:to_s).include? val
900
+ else false
901
+ end
902
+ end
903
+
904
+
905
+ ## Provide a way to register symbol aliases to the Parser
906
+ def self.register_alias(*alias_keys)
907
+ alias_keys.each do |alias_key|
908
+ # pass in the alias-key and the class
909
+ Parser.register(alias_key, self)
910
+ end
911
+ end
912
+
913
+ ## Factory class methods ...
914
+
915
+ # Determines which type of object to create based on arguments passed
916
+ # to +OptimistXL::opt+. This is trickier in OptimistXL, than other cmdline
917
+ # parsers (e.g. Slop) because we allow the +default:+ to be able to
918
+ # set the option's type.
919
+ def self.create(name, desc="", opts={}, settings={})
920
+
921
+ opttype = OptimistXL::Parser.registry_getopttype(opts[:type])
922
+ opttype_from_default = get_klass_from_default(opts, opttype)
923
+
924
+ raise ArgumentError, ":type specification and default type don't match (default type is #{opttype_from_default.class})" if opttype && opttype_from_default && (opttype.class != opttype_from_default.class)
925
+
926
+ opt_inst = (opttype || opttype_from_default || OptimistXL::BooleanOption.new)
927
+
928
+ ## fill in :long
929
+ opt_inst.long = handle_long_opt(opts[:long], name)
930
+
931
+ ## fill in :short
932
+ opt_inst.short = handle_short_opt(opts[:short])
933
+
934
+ ## fill in :multi
935
+ multi_given = opts[:multi] || false
936
+ opt_inst.multi_given = multi_given
937
+
938
+ ## fill in :default for flags
939
+ defvalue = opts[:default] || opt_inst.default
940
+
941
+ ## fill in permitted values
942
+ permitted = opts[:permitted] || nil
943
+
944
+ ## autobox :default for :multi (multi-occurrence) arguments
945
+ defvalue = [defvalue] if defvalue && multi_given && !defvalue.kind_of?(Array)
946
+ opt_inst.permitted = permitted
947
+ opt_inst.permitted_response = opts[:permitted_response] if opts[:permitted_response]
948
+ opt_inst.default = defvalue
949
+ opt_inst.name = name
950
+ opt_inst.opts = opts
951
+ opt_inst
952
+ end
953
+
954
+ private
955
+
956
+ def self.get_type_from_disdef(optdef, opttype, disambiguated_default)
957
+ if disambiguated_default.is_a? Array
958
+ return(optdef.first.class.name.downcase + "s") if !optdef.empty?
959
+ if opttype
960
+ raise ArgumentError, "multiple argument type must be plural" unless opttype.multi_arg?
961
+ return nil
962
+ else
963
+ raise ArgumentError, "multiple argument type cannot be deduced from an empty array"
964
+ end
965
+ end
966
+ return disambiguated_default.class.name.downcase
967
+ end
968
+
969
+ def self.get_klass_from_default(opts, opttype)
970
+ ## for options with :multi => true, an array default doesn't imply
971
+ ## a multi-valued argument. for that you have to specify a :type
972
+ ## as well. (this is how we disambiguate an ambiguous situation;
973
+ ## see the docs for Parser#opt for details.)
974
+
975
+ disambiguated_default = if opts[:multi] && opts[:default].is_a?(Array) && opttype.nil?
976
+ opts[:default].first
977
+ else
978
+ opts[:default]
979
+ end
980
+
981
+ return nil if disambiguated_default.nil?
982
+ type_from_default = get_type_from_disdef(opts[:default], opttype, disambiguated_default)
983
+ return OptimistXL::Parser.registry_getopttype(type_from_default)
984
+ end
985
+
986
+ def self.handle_long_opt(lopt, name)
987
+ lopt = lopt ? lopt.to_s : name.to_s.gsub("_", "-")
988
+ lopt = case lopt
989
+ when /^--([^-].*)$/ then $1
990
+ when /^[^-]/ then lopt
991
+ else raise ArgumentError, "invalid long option name #{lopt.inspect}"
992
+ end
993
+ end
994
+
995
+ def self.handle_short_opt(sopt)
996
+ sopt = sopt.to_s if sopt && sopt != :none
997
+ sopt = case sopt
998
+ when /^-(.)$/ then $1
999
+ when nil, :none, /^.$/ then sopt
1000
+ else raise ArgumentError, "invalid short option name '#{sopt.inspect}'"
1001
+ end
1002
+
1003
+ if sopt
1004
+ raise ArgumentError, "a short option name can't be a number or a dash" if sopt =~ ::OptimistXL::Parser::INVALID_SHORT_ARG_REGEX
1005
+ end
1006
+ return sopt
1007
+ end
1008
+
1009
+ end
1010
+
1011
+ # Flag option. Has no arguments. Can be negated with "no-".
1012
+ class BooleanOption < Option
1013
+ register_alias :flag, :bool, :boolean, :trueclass, :falseclass
1014
+ def initialize
1015
+ super()
1016
+ @default = false
1017
+ end
1018
+ def flag? ; true ; end
1019
+ def parse(_paramlist, neg_given)
1020
+ return(self.name.to_s =~ /^no_/ ? neg_given : !neg_given)
1021
+ end
1022
+ end
1023
+
1024
+ # Floating point number option class.
1025
+ class FloatOption < Option
1026
+ register_alias :float, :double
1027
+ def type_format ; "=<f>" ; end
1028
+ def parse(paramlist, _neg_given)
1029
+ paramlist.map do |pg|
1030
+ pg.map do |param|
1031
+ raise CommandlineError, "option '#{self.name}' needs a floating-point number" unless param.is_a?(Numeric) || param =~ FLOAT_RE
1032
+ param.to_f
1033
+ end
1034
+ end
1035
+ end
1036
+ end
1037
+
1038
+ # Integer number option class.
1039
+ class IntegerOption < Option
1040
+ register_alias :int, :integer, :fixnum
1041
+ def type_format ; "=<i>" ; end
1042
+ def parse(paramlist, _neg_given)
1043
+ paramlist.map do |pg|
1044
+ pg.map do |param|
1045
+ raise CommandlineError, "option '#{self.name}' needs an integer" unless param.is_a?(Numeric) || param =~ /^-?[\d_]+$/
1046
+ param.to_i
1047
+ end
1048
+ end
1049
+ end
1050
+ end
1051
+
1052
+ # Option class for handling IO objects and URLs.
1053
+ # Note that this will return the file-handle, not the file-name
1054
+ # in the case of file-paths given to it.
1055
+ class IOOption < Option
1056
+ register_alias :io
1057
+ def type_format ; "=<filename/uri>" ; end
1058
+ def parse(paramlist, _neg_given)
1059
+ paramlist.map do |pg|
1060
+ pg.map do |param|
1061
+ if param =~ /^(stdin|-)$/i
1062
+ $stdin
1063
+ else
1064
+ require 'open-uri'
1065
+ begin
1066
+ open param
1067
+ rescue SystemCallError => e
1068
+ raise CommandlineError, "file or url for option '#{self.name}' cannot be opened: #{e.message}"
1069
+ end
1070
+ end
1071
+ end
1072
+ end
1073
+ end
1074
+ end
1075
+
1076
+ # Option class for handling Strings.
1077
+ class StringOption < Option
1078
+ register_alias :string
1079
+ def type_format ; "=<s>" ; end
1080
+ def parse(paramlist, _neg_given)
1081
+ paramlist.map { |pg| pg.map(&:to_s) }
1082
+ end
1083
+ end
1084
+
1085
+ # Option for dates. No longer uses Chronic if available.
1086
+ # If chronic style dates are needed, then you may
1087
+ # require 'optimist_xl/chronic'
1088
+
1089
+ class DateOption < Option
1090
+ register_alias :date
1091
+ def type_format ; "=<date>" ; end
1092
+ def parse(paramlist, _neg_given)
1093
+ paramlist.map do |pg|
1094
+ pg.map do |param|
1095
+ next param if param.is_a?(Date)
1096
+ begin
1097
+ Date.parse(param)
1098
+ rescue ArgumentError
1099
+ raise CommandlineError, "option '#{self.name}' needs a date"
1100
+ end
1101
+ end
1102
+ end
1103
+ end
1104
+ end
1105
+
1106
+ ### MULTI_OPT_TYPES :
1107
+ ## The set of values that indicate a multiple-parameter option (i.e., that
1108
+ ## takes multiple space-separated values on the commandline) when passed as
1109
+ ## the +:type+ parameter of #opt.
1110
+
1111
+ # Option class for handling multiple Integers
1112
+ class IntegerArrayOption < IntegerOption
1113
+ register_alias :fixnums, :ints, :integers
1114
+ def type_format ; "=<i+>" ; end
1115
+ def multi_arg? ; true ; end
1116
+ end
1117
+
1118
+ # Option class for handling multiple Floats
1119
+ class FloatArrayOption < FloatOption
1120
+ register_alias :doubles, :floats
1121
+ def type_format ; "=<f+>" ; end
1122
+ def multi_arg? ; true ; end
1123
+ end
1124
+
1125
+ # Option class for handling multiple Strings
1126
+ class StringArrayOption < StringOption
1127
+ register_alias :strings
1128
+ def type_format ; "=<s+>" ; end
1129
+ def multi_arg? ; true ; end
1130
+ end
1131
+
1132
+ # Option class for handling multiple dates
1133
+ class DateArrayOption < DateOption
1134
+ register_alias :dates
1135
+ def type_format ; "=<date+>" ; end
1136
+ def multi_arg? ; true ; end
1137
+ end
1138
+
1139
+ # Option class for handling Files/URLs via 'open'
1140
+ class IOArrayOption < IOOption
1141
+ register_alias :ios
1142
+ def type_format ; "=<filename/uri+>" ; end
1143
+ def multi_arg? ; true ; end
1144
+ end
1145
+
1146
+ ## The easy, syntactic-sugary entry method into OptimistXL. Creates a Parser,
1147
+ ## passes the block to it, then parses +args+ with it, handling any errors or
1148
+ ## requests for help or version information appropriately (and then exiting).
1149
+ ## Modifies +args+ in place. Returns a hash of option values.
1150
+ ##
1151
+ ## The block passed in should contain zero or more calls to +opt+
1152
+ ## (Parser#opt), zero or more calls to +text+ (Parser#text), and
1153
+ ## probably a call to +version+ (Parser#version).
1154
+ ##
1155
+ ## The returned block contains a value for every option specified with
1156
+ ## +opt+. The value will be the value given on the commandline, or the
1157
+ ## default value if the option was not specified on the commandline. For
1158
+ ## every option specified on the commandline, a key "<option
1159
+ ## name>_given" will also be set in the hash.
1160
+ ##
1161
+ ## Example:
1162
+ ##
1163
+ ## require 'optimist'
1164
+ ## opts = OptimistXL::options do
1165
+ ## opt :monkey, "Use monkey mode" # a flag --monkey, defaulting to false
1166
+ ## opt :name, "Monkey name", :type => :string # a string --name <s>, defaulting to nil
1167
+ ## opt :num_limbs, "Number of limbs", :default => 4 # an integer --num-limbs <i>, defaulting to 4
1168
+ ## end
1169
+ ##
1170
+ ## ## if called with no arguments
1171
+ ## p opts # => {:monkey=>false, :name=>nil, :num_limbs=>4, :help=>false}
1172
+ ##
1173
+ ## ## if called with --monkey
1174
+ ## p opts # => {:monkey=>true, :name=>nil, :num_limbs=>4, :help=>false, :monkey_given=>true}
1175
+ ##
1176
+ ## Settings:
1177
+ ## OptimistXL::options and OptimistXL::Parser.new accept +settings+ to control how
1178
+ ## options are interpreted. These settings are given as hash arguments, e.g:
1179
+ ##
1180
+ ## opts = OptimistXL::options(ARGV, :inexact_match => true) do
1181
+ ## opt :foobar, 'messed up'
1182
+ ## opt :forget, 'forget it'
1183
+ ## end
1184
+ ##
1185
+ ## +settings+ include:
1186
+ ## * :inexact_match : Allow minimum unambigous number of characters to match a long option
1187
+ ## * :suggestions : Enables suggestions when unknown arguments are given and DidYouMean is installed. DidYouMean comes standard with Ruby 2.3+
1188
+ ## * :explicit_short : 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.
1189
+
1190
+ ## Because OptimistXL::options uses a default argument for +args+, you must pass that argument when using the settings feature.
1191
+ ##
1192
+ ## See more examples at http://optimist.rubyforge.org.
1193
+ def options(args = ARGV, *a, &b)
1194
+ @last_parser = Parser.new(*a, &b)
1195
+ with_standard_exception_handling(@last_parser) { @last_parser.parse args }
1196
+ end
1197
+
1198
+ ## If OptimistXL::options doesn't do quite what you want, you can create a Parser
1199
+ ## object and call Parser#parse on it. That method will throw CommandlineError,
1200
+ ## HelpNeeded and VersionNeeded exceptions when necessary; if you want to
1201
+ ## have these handled for you in the standard manner (e.g. show the help
1202
+ ## and then exit upon an HelpNeeded exception), call your code from within
1203
+ ## a block passed to this method.
1204
+ ##
1205
+ ## Note that this method will call System#exit after handling an exception!
1206
+ ##
1207
+ ## Usage example:
1208
+ ##
1209
+ ## require 'optimist'
1210
+ ## p = OptimistXL::Parser.new do
1211
+ ## opt :monkey, "Use monkey mode" # a flag --monkey, defaulting to false
1212
+ ## opt :goat, "Use goat mode", :default => true # a flag --goat, defaulting to true
1213
+ ## end
1214
+ ##
1215
+ ## opts = OptimistXL::with_standard_exception_handling p do
1216
+ ## o = p.parse ARGV
1217
+ ## raise OptimistXL::HelpNeeded if ARGV.empty? # show help screen
1218
+ ## o
1219
+ ## end
1220
+ ##
1221
+ ## Requires passing in the parser object.
1222
+
1223
+ def with_standard_exception_handling(parser)
1224
+ yield
1225
+ rescue CommandlineError => e
1226
+ parser.die(e.message, nil, e.error_code)
1227
+ rescue HelpNeeded => e
1228
+ e.parser.educate
1229
+ exit
1230
+ rescue VersionNeeded
1231
+ puts parser.version
1232
+ exit
1233
+ end
1234
+
1235
+ ## Informs the user that their usage of 'arg' was wrong, as detailed by
1236
+ ## 'msg', and dies. Example:
1237
+ ##
1238
+ ## options do
1239
+ ## opt :volume, :default => 0.0
1240
+ ## end
1241
+ ##
1242
+ ## die :volume, "too loud" if opts[:volume] > 10.0
1243
+ ## die :volume, "too soft" if opts[:volume] < 0.1
1244
+ ##
1245
+ ## In the one-argument case, simply print that message, a notice
1246
+ ## about -h, and die. Example:
1247
+ ##
1248
+ ## options do
1249
+ ## opt :whatever # ...
1250
+ ## end
1251
+ ##
1252
+ ## OptimistXL::die "need at least one filename" if ARGV.empty?
1253
+ ##
1254
+ ## An exit code can be provide if needed
1255
+ ##
1256
+ ## OptimistXL::die "need at least one filename", -2 if ARGV.empty?
1257
+ def die(arg, msg = nil, error_code = nil)
1258
+ if @last_parser
1259
+ @last_parser.die arg, msg, error_code
1260
+ else
1261
+ raise ArgumentError, "OptimistXL::die can only be called after OptimistXL::options"
1262
+ end
1263
+ end
1264
+
1265
+ ## Displays the help message and dies. Example:
1266
+ ##
1267
+ ## options do
1268
+ ## opt :volume, :default => 0.0
1269
+ ## banner <<-EOS
1270
+ ## Usage:
1271
+ ## #$0 [options] <name>
1272
+ ## where [options] are:
1273
+ ## EOS
1274
+ ## end
1275
+ ##
1276
+ ## OptimistXL::educate if ARGV.empty?
1277
+ def educate
1278
+ if @last_parser
1279
+ @last_parser.educate
1280
+ exit
1281
+ else
1282
+ raise ArgumentError, "OptimistXL::educate can only be called after OptimistXL::options"
1283
+ end
1284
+ end
1285
+
1286
+ module_function :options, :die, :educate, :with_standard_exception_handling
1287
+ end # module
1288
+
1289
+
1290
+ # @global_parser.stop_on(subcommands)
1291
+ # @global_parser.stop_on_unknown
1292
+ # Trollop::with_standard_exception_handling(@global_parser) do
1293
+ # global_options = @global_parser.parse(args)
1294
+ # cmd = parse_subcommand(args)
1295
+ # cmd_options = parse_subcommand_options(args, cmd)
1296
+ # Result.new(global_options, cmd, cmd_options)
1297
+ # end