convoy 1.0.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.
Files changed (109) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.irbrc +3 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +8 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE +22 -0
  9. data/README.md +705 -0
  10. data/Rakefile +1 -0
  11. data/convoy.gemspec +24 -0
  12. data/examples/.my_apprc +24 -0
  13. data/examples/basic +10 -0
  14. data/examples/basic_config_file +16 -0
  15. data/examples/basic_conflicts +17 -0
  16. data/examples/basic_depends_on +25 -0
  17. data/examples/basic_flags +15 -0
  18. data/examples/basic_options +14 -0
  19. data/examples/basic_options_multi +15 -0
  20. data/examples/basic_require_arguments +17 -0
  21. data/examples/basic_texts +21 -0
  22. data/examples/basic_validations +21 -0
  23. data/examples/basic_with_everything +30 -0
  24. data/examples/commands/example_command.rb +13 -0
  25. data/examples/suite_complex +65 -0
  26. data/examples/suite_simple +19 -0
  27. data/examples/suite_with_sub_commands +94 -0
  28. data/lib/convoy.rb +83 -0
  29. data/lib/convoy/action_command/base.rb +85 -0
  30. data/lib/convoy/action_command/escort_utility_command.rb +53 -0
  31. data/lib/convoy/app.rb +127 -0
  32. data/lib/convoy/arguments.rb +20 -0
  33. data/lib/convoy/auto_options.rb +71 -0
  34. data/lib/convoy/error/error.rb +33 -0
  35. data/lib/convoy/formatter/command.rb +87 -0
  36. data/lib/convoy/formatter/commands.rb +37 -0
  37. data/lib/convoy/formatter/cursor_position.rb +29 -0
  38. data/lib/convoy/formatter/default_help_formatter.rb +117 -0
  39. data/lib/convoy/formatter/global_command.rb +17 -0
  40. data/lib/convoy/formatter/option.rb +152 -0
  41. data/lib/convoy/formatter/options.rb +28 -0
  42. data/lib/convoy/formatter/shell_command_executor.rb +49 -0
  43. data/lib/convoy/formatter/stream_output_formatter.rb +88 -0
  44. data/lib/convoy/formatter/string_grid.rb +108 -0
  45. data/lib/convoy/formatter/string_splitter.rb +50 -0
  46. data/lib/convoy/formatter/terminal.rb +30 -0
  47. data/lib/convoy/global_pre_parser.rb +43 -0
  48. data/lib/convoy/logger.rb +75 -0
  49. data/lib/convoy/option_dependency_validator.rb +82 -0
  50. data/lib/convoy/option_parser.rb +155 -0
  51. data/lib/convoy/setup/configuration/generator.rb +75 -0
  52. data/lib/convoy/setup/configuration/instance.rb +34 -0
  53. data/lib/convoy/setup/configuration/loader.rb +43 -0
  54. data/lib/convoy/setup/configuration/locator/base.rb +19 -0
  55. data/lib/convoy/setup/configuration/locator/chaining.rb +29 -0
  56. data/lib/convoy/setup/configuration/locator/descending_to_home.rb +23 -0
  57. data/lib/convoy/setup/configuration/locator/executing_script_directory.rb +15 -0
  58. data/lib/convoy/setup/configuration/locator/specified_directory.rb +21 -0
  59. data/lib/convoy/setup/configuration/merge_tool.rb +38 -0
  60. data/lib/convoy/setup/configuration/reader.rb +36 -0
  61. data/lib/convoy/setup/configuration/writer.rb +46 -0
  62. data/lib/convoy/setup/dsl/action.rb +17 -0
  63. data/lib/convoy/setup/dsl/command.rb +67 -0
  64. data/lib/convoy/setup/dsl/config_file.rb +13 -0
  65. data/lib/convoy/setup/dsl/global.rb +29 -0
  66. data/lib/convoy/setup/dsl/options.rb +81 -0
  67. data/lib/convoy/setup_accessor.rb +206 -0
  68. data/lib/convoy/trollop.rb +861 -0
  69. data/lib/convoy/utils.rb +21 -0
  70. data/lib/convoy/validator.rb +45 -0
  71. data/spec/integration/basic_config_file_spec.rb +126 -0
  72. data/spec/integration/basic_conflicts_spec.rb +47 -0
  73. data/spec/integration/basic_depends_on_spec.rb +275 -0
  74. data/spec/integration/basic_options_spec.rb +41 -0
  75. data/spec/integration/basic_options_with_multi_spec.rb +30 -0
  76. data/spec/integration/basic_spec.rb +38 -0
  77. data/spec/integration/basic_validations_spec.rb +77 -0
  78. data/spec/integration/basic_with_arguments_spec.rb +35 -0
  79. data/spec/integration/basic_with_text_fields_spec.rb +21 -0
  80. data/spec/integration/suite_simple_spec.rb +45 -0
  81. data/spec/integration/suite_sub_command_spec.rb +51 -0
  82. data/spec/lib/convoy/action_command/base_spec.rb +200 -0
  83. data/spec/lib/convoy/formatter/command_spec.rb +238 -0
  84. data/spec/lib/convoy/formatter/global_command_spec.rb +50 -0
  85. data/spec/lib/convoy/formatter/option_spec.rb +300 -0
  86. data/spec/lib/convoy/formatter/shell_command_executor_spec.rb +59 -0
  87. data/spec/lib/convoy/formatter/stream_output_formatter_spec.rb +214 -0
  88. data/spec/lib/convoy/formatter/string_grid_spec.rb +59 -0
  89. data/spec/lib/convoy/formatter/string_splitter_spec.rb +50 -0
  90. data/spec/lib/convoy/formatter/terminal_spec.rb +19 -0
  91. data/spec/lib/convoy/setup/configuration/generator_spec.rb +101 -0
  92. data/spec/lib/convoy/setup/configuration/loader_spec.rb +79 -0
  93. data/spec/lib/convoy/setup/configuration/locator/chaining_spec.rb +81 -0
  94. data/spec/lib/convoy/setup/configuration/locator/descending_to_home_spec.rb +57 -0
  95. data/spec/lib/convoy/setup/configuration/locator/executing_script_directory_spec.rb +29 -0
  96. data/spec/lib/convoy/setup/configuration/locator/specified_directory_spec.rb +33 -0
  97. data/spec/lib/convoy/setup/configuration/merge_tool_spec.rb +41 -0
  98. data/spec/lib/convoy/setup/configuration/reader_spec.rb +41 -0
  99. data/spec/lib/convoy/setup/configuration/writer_spec.rb +75 -0
  100. data/spec/lib/convoy/setup_accessor_spec.rb +226 -0
  101. data/spec/lib/convoy/utils_spec.rb +30 -0
  102. data/spec/spec_helper.rb +29 -0
  103. data/spec/support/integration_helpers.rb +2 -0
  104. data/spec/support/matchers/execute_action_for_command_matcher.rb +21 -0
  105. data/spec/support/matchers/execute_action_with_arguments_matcher.rb +25 -0
  106. data/spec/support/matchers/execute_action_with_options_matcher.rb +29 -0
  107. data/spec/support/matchers/exit_with_code_matcher.rb +29 -0
  108. data/spec/support/shared_contexts/integration_setup.rb +34 -0
  109. metadata +292 -0
@@ -0,0 +1,861 @@
1
+ ## lib/trollop.rb -- trollop command-line processing library
2
+ ## Author:: William Morgan (mailto: wmorgan-trollop@masanjin.net)
3
+ ## Copyright:: Copyright 2007 William Morgan
4
+ ## License:: the same terms as ruby itself
5
+
6
+ require 'date'
7
+
8
+ module Trollop
9
+
10
+ VERSION = "2.0"
11
+
12
+ ## Thrown by Parser in the event of a commandline error. Not needed if
13
+ ## you're using the Trollop::options entry.
14
+ class CommandlineError < StandardError;
15
+ end
16
+
17
+ ## Thrown by Parser if the user passes in '-h' or '--help'. Handled
18
+ ## automatically by Trollop#options.
19
+ class HelpNeeded < StandardError;
20
+ end
21
+
22
+ ## Thrown by Parser if the user passes in '-h' or '--version'. Handled
23
+ ## automatically by Trollop#options.
24
+ class VersionNeeded < StandardError;
25
+ end
26
+
27
+ ## Regex for floating point numbers
28
+ FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))([eE][-+]?[\d]+)?$/
29
+
30
+ ## Regex for parameters
31
+ PARAM_RE = /^-(-|\.$|[^\d\.])/
32
+
33
+ ## The commandline parser. In typical usage, the methods in this class
34
+ ## will be handled internally by Trollop::options. In this case, only the
35
+ ## #opt, #banner and #version, #depends, and #conflicts methods will
36
+ ## typically be called.
37
+ ##
38
+ ## If you want to instantiate this class yourself (for more complicated
39
+ ## argument-parsing logic), call #parse to actually produce the output hash,
40
+ ## and consider calling it from within
41
+ ## Trollop::with_standard_exception_handling.
42
+ class Parser
43
+
44
+ ## The set of values that indicate a flag option when passed as the
45
+ ## +:type+ parameter of #opt.
46
+ FLAG_TYPES = [:flag, :bool, :boolean]
47
+
48
+ ## The set of values that indicate a single-parameter (normal) option when
49
+ ## passed as the +:type+ parameter of #opt.
50
+ ##
51
+ ## A value of +io+ corresponds to a readable IO resource, including
52
+ ## a filename, URI, or the strings 'stdin' or '-'.
53
+ SINGLE_ARG_TYPES = [:int, :integer, :string, :double, :float, :io, :date]
54
+
55
+ ## The set of values that indicate a multiple-parameter option (i.e., that
56
+ ## takes multiple space-separated values on the commandline) when passed as
57
+ ## the +:type+ parameter of #opt.
58
+ MULTI_ARG_TYPES = [:ints, :integers, :strings, :doubles, :floats, :ios, :dates]
59
+
60
+ ## The complete set of legal values for the +:type+ parameter of #opt.
61
+ TYPES = FLAG_TYPES + SINGLE_ARG_TYPES + MULTI_ARG_TYPES
62
+
63
+ INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc:
64
+
65
+ ## The values from the commandline that were not interpreted by #parse.
66
+ attr_reader :leftovers
67
+
68
+ ## The complete configuration hashes for each option. (Mainly useful
69
+ ## for testing.)
70
+ attr_reader :specs
71
+
72
+ attr_reader :order
73
+
74
+ ## Initializes the parser, and instance-evaluates any block given.
75
+ def initialize *a, &b
76
+ @version = nil
77
+ @leftovers = []
78
+ @specs = {}
79
+ @long = {}
80
+ @short = {}
81
+ @order = []
82
+ @constraints = []
83
+ @stop_words = []
84
+ @stop_on_unknown = false
85
+ @help_formatter = nil
86
+
87
+ #instance_eval(&b) if b # can't take arguments
88
+ cloaker(&b).bind(self).call(*a) if b
89
+ end
90
+
91
+ ## Define an option. +name+ is the option name, a unique identifier
92
+ ## for the option that you will use internally, which should be a
93
+ ## symbol or a string. +desc+ is a string description which will be
94
+ ## displayed in help messages.
95
+ ##
96
+ ## Takes the following optional arguments:
97
+ ##
98
+ ## [+: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.
99
+ ## [+:short+] Specify the short form of the argument, i.e. the form with one dash. If unspecified, will be automatically derived from +name+.
100
+ ## [+: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.
101
+ ## [+:default+] Set the default value for an argument. Without a default value, the hash returned by #parse (and thus Trollop::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+.
102
+ ## [+:required+] If set to +true+, the argument must be provided on the commandline.
103
+ ## [+: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.)
104
+ ##
105
+ ## Note that there are two types of argument multiplicity: an argument
106
+ ## can take multiple values, e.g. "--arg 1 2 3". An argument can also
107
+ ## be allowed to occur multiple times, e.g. "--arg 1 --arg 2".
108
+ ##
109
+ ## Arguments that take multiple values should have a +:type+ parameter
110
+ ## drawn from +MULTI_ARG_TYPES+ (e.g. +:strings+), or a +:default:+
111
+ ## value of an array of the correct type (e.g. [String]). The
112
+ ## value of this argument will be an array of the parameters on the
113
+ ## commandline.
114
+ ##
115
+ ## Arguments that can occur multiple times should be marked with
116
+ ## +:multi+ => +true+. The value of this argument will also be an array.
117
+ ## In contrast with regular non-multi options, if not specified on
118
+ ## the commandline, the default value will be [], not nil.
119
+ ##
120
+ ## These two attributes can be combined (e.g. +:type+ => +:strings+,
121
+ ## +:multi+ => +true+), in which case the value of the argument will be
122
+ ## an array of arrays.
123
+ ##
124
+ ## There's one ambiguous case to be aware of: when +:multi+: is true and a
125
+ ## +:default+ is set to an array (of something), it's ambiguous whether this
126
+ ## is a multi-value argument as well as a multi-occurrence argument.
127
+ ## In thise case, Trollop assumes that it's not a multi-value argument.
128
+ ## If you want a multi-value, multi-occurrence argument with a default
129
+ ## value, you must specify +:type+ as well.
130
+
131
+ def opt name, desc="", opts={}
132
+ raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name
133
+
134
+ ## fill in :type
135
+ opts[:type] = # normalize
136
+ case opts[:type]
137
+ when :boolean, :bool;
138
+ :flag
139
+ when :integer;
140
+ :int
141
+ when :integers;
142
+ :ints
143
+ when :double;
144
+ :float
145
+ when :doubles;
146
+ :floats
147
+ when Class
148
+ case opts[:type].name
149
+ when 'TrueClass', 'FalseClass';
150
+ :flag
151
+ when 'String';
152
+ :string
153
+ when 'Integer';
154
+ :int
155
+ when 'Float';
156
+ :float
157
+ when 'IO';
158
+ :io
159
+ when 'Date';
160
+ :date
161
+ else
162
+ raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'"
163
+ end
164
+ when nil;
165
+ nil
166
+ else
167
+ raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type])
168
+ opts[:type]
169
+ end
170
+
171
+ ## for options with :multi => true, an array default doesn't imply
172
+ ## a multi-valued argument. for that you have to specify a :type
173
+ ## as well. (this is how we disambiguate an ambiguous situation;
174
+ ## see the docs for Parser#opt for details.)
175
+ disambiguated_default = if opts[:multi] && opts[:default].is_a?(Array) && !opts[:type]
176
+ opts[:default].first
177
+ else
178
+ opts[:default]
179
+ end
180
+
181
+ type_from_default =
182
+ case disambiguated_default
183
+ when Integer;
184
+ :int
185
+ when Numeric;
186
+ :float
187
+ when TrueClass, FalseClass;
188
+ :flag
189
+ when String;
190
+ :string
191
+ when IO;
192
+ :io
193
+ when Date;
194
+ :date
195
+ when Array
196
+ if opts[:default].empty?
197
+ raise ArgumentError, "multiple argument type cannot be deduced from an empty array for '#{opts[:default][0].class.name}'"
198
+ end
199
+ case opts[:default][0] # the first element determines the types
200
+ when Integer;
201
+ :ints
202
+ when Numeric;
203
+ :floats
204
+ when String;
205
+ :strings
206
+ when IO;
207
+ :ios
208
+ when Date;
209
+ :dates
210
+ else
211
+ raise ArgumentError, "unsupported multiple argument type '#{opts[:default][0].class.name}'"
212
+ end
213
+ when nil;
214
+ nil
215
+ else
216
+ raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'"
217
+ end
218
+
219
+ raise ArgumentError, ":type specification and default type don't match (default type is #{type_from_default})" if opts[:type] && type_from_default && opts[:type] != type_from_default
220
+
221
+ opts[:type] = opts[:type] || type_from_default || :flag
222
+
223
+ ## fill in :long
224
+ opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-")
225
+ opts[:long] = case opts[:long]
226
+ when /^--([^-].*)$/;
227
+ $1
228
+ when /^[^-]/;
229
+ opts[:long]
230
+ else
231
+ ; raise ArgumentError, "invalid long option name #{opts[:long].inspect}"
232
+ end
233
+ raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]]
234
+
235
+ ## fill in :short
236
+ opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none
237
+ opts[:short] = case opts[:short]
238
+ when /^-(.)$/;
239
+ $1
240
+ when nil, :none, /^.$/;
241
+ opts[:short]
242
+ else
243
+ raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'"
244
+ end
245
+
246
+ if opts[:short]
247
+ raise ArgumentError, "short option name #{opts[:short].inspect} is already taken; please specify a (different) :short" if @short[opts[:short]]
248
+ raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ INVALID_SHORT_ARG_REGEX
249
+ end
250
+
251
+ ## fill in :default for flags
252
+ opts[:default] = false if opts[:type] == :flag && opts[:default].nil?
253
+
254
+ ## autobox :default for :multi (multi-occurrence) arguments
255
+ opts[:default] = [opts[:default]] if opts[:default] && opts[:multi] && !opts[:default].is_a?(Array)
256
+
257
+ ## fill in :multi
258
+ opts[:multi] ||= false
259
+
260
+ opts[:desc] ||= desc
261
+ @long[opts[:long]] = name
262
+ @short[opts[:short]] = name if opts[:short] && opts[:short] != :none
263
+ @specs[name] = opts
264
+ @order << [:opt, name]
265
+ end
266
+
267
+ ## Sets the version string. If set, the user can request the version
268
+ ## on the commandline. Should probably be of the form "<program name>
269
+ ## <version number>".
270
+ def version s=nil;
271
+ @version = s if s; @version
272
+ end
273
+
274
+ ## Adds text to the help display. Can be interspersed with calls to
275
+ ## #opt to build a multi-section help page.
276
+ def banner s;
277
+ @order << [:text, s]
278
+ end
279
+
280
+ alias :text :banner
281
+
282
+ def help_formatter(formatter)
283
+ @help_formatter = formatter
284
+ end
285
+
286
+ def current_help_formatter
287
+ @help_formatter
288
+ end
289
+
290
+ ## Marks two (or more!) options as requiring each other. Only handles
291
+ ## undirected (i.e., mutual) dependencies. Directed dependencies are
292
+ ## better modeled with Trollop::die.
293
+ def depends *syms
294
+ syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
295
+ @constraints << [:depends, syms]
296
+ end
297
+
298
+ ## Marks two (or more!) options as conflicting.
299
+ def conflicts *syms
300
+ syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
301
+ @constraints << [:conflicts, syms]
302
+ end
303
+
304
+ ## Defines a set of words which cause parsing to terminate when
305
+ ## encountered, such that any options to the left of the word are
306
+ ## parsed as usual, and options to the right of the word are left
307
+ ## intact.
308
+ ##
309
+ ## A typical use case would be for subcommand support, where these
310
+ ## would be set to the list of subcommands. A subsequent Trollop
311
+ ## invocation would then be used to parse subcommand options, after
312
+ ## shifting the subcommand off of ARGV.
313
+ def stop_on *words
314
+ @stop_words = [*words].flatten
315
+ end
316
+
317
+ ## Similar to #stop_on, but stops on any unknown word when encountered
318
+ ## (unless it is a parameter for an argument). This is useful for
319
+ ## cases where you don't know the set of subcommands ahead of time,
320
+ ## i.e., without first parsing the global options.
321
+ def stop_on_unknown
322
+ @stop_on_unknown = true
323
+ end
324
+
325
+ ## Parses the commandline. Typically called by Trollop::options,
326
+ ## but you can call it directly if you need more control.
327
+ ##
328
+ ## throws CommandlineError, HelpNeeded, and VersionNeeded exceptions.
329
+ def parse cmdline=ARGV
330
+ vals = {}
331
+ required = {}
332
+
333
+ opt :version, 'Prints version and exits' if @version unless @specs[:version] || @long['version']
334
+ opt :help, "\x1B[38;5;222mShows help message for current command\x1B[0m" unless @specs[:help] || @long['help']
335
+
336
+ @specs.each do |sym, opts|
337
+ required[sym] = true if opts[:required]
338
+ vals[sym] = opts[:default]
339
+ vals[sym] = [] if opts[:multi] && !opts[:default] # multi arguments default to [], not nil
340
+ end
341
+
342
+ resolve_default_short_options!
343
+
344
+ ## resolve symbols
345
+ given_args = {}
346
+ @leftovers = each_arg cmdline do |arg, params|
347
+ ## handle --no- forms
348
+ arg, negative_given = if arg =~ /^--no-([^-]\S*)$/
349
+ ["--#{$1}", true]
350
+ else
351
+ [arg, false]
352
+ end
353
+
354
+ sym = case arg
355
+ when /^-([^-])$/;
356
+ @short[$1]
357
+ when /^--([^-]\S*)$/;
358
+ @long[$1] || @long["no-#{$1}"]
359
+ else
360
+ # raise CommandlineError, "invalid argument syntax: '#{arg}'"
361
+ puts "\n \x1B[48;5;196m ERROR \x1B[0m \xe2\x80\x94 Invalid argument syntax: \x1B[38;5;123m#{arg}\x1B[0m\n\n"
362
+ exit
363
+ end
364
+
365
+ sym = nil if arg =~ /--no-/ # explicitly invalidate --no-no- arguments
366
+
367
+ # raise CommandlineError, "unknown argument '#{arg}'" unless sym
368
+ unless sym
369
+ puts "\n \x1B[48;5;196m ERROR \x1B[0m \xe2\x80\x94 Unknown flag: \x1B[38;5;123m#{arg}\x1B[0m\n\n"
370
+ exit
371
+ end
372
+
373
+ if given_args.include?(sym) && !@specs[sym][:multi]
374
+ # raise CommandlineError, "option '#{arg}' specified multiple times"
375
+ puts "\n \x1B[48;5;196m ERROR \x1B[0m \xe2\x80\x94 Flag specified multiple times: \x1B[38;5;123m#{arg}\x1B[0m\n\n"
376
+ exit
377
+ end
378
+
379
+ given_args[sym] ||= {}
380
+ given_args[sym][:arg] = arg
381
+ given_args[sym][:negative_given] = negative_given
382
+ given_args[sym][:params] ||= []
383
+
384
+ # The block returns the number of parameters taken.
385
+ num_params_taken = 0
386
+
387
+ unless params.nil?
388
+ if SINGLE_ARG_TYPES.include?(@specs[sym][:type])
389
+ given_args[sym][:params] << params[0, 1] # take the first parameter
390
+ num_params_taken = 1
391
+ elsif MULTI_ARG_TYPES.include?(@specs[sym][:type])
392
+ given_args[sym][:params] << params # take all the parameters
393
+ num_params_taken = params.size
394
+ end
395
+ end
396
+
397
+ num_params_taken
398
+ end
399
+
400
+ ## check for version and help args
401
+ raise VersionNeeded if given_args.include? :version
402
+ raise HelpNeeded if given_args.include? :help
403
+
404
+ ## check constraint satisfaction
405
+ @constraints.each do |type, syms|
406
+ constraint_sym = syms.find { |sym| given_args[sym] }
407
+ next unless constraint_sym
408
+
409
+ case type
410
+ when :depends
411
+ syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} requires --#{@specs[sym][:long]}" unless given_args.include? sym }
412
+ when :conflicts
413
+ syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} conflicts with --#{@specs[sym][:long]}" if given_args.include?(sym) && (sym != constraint_sym) }
414
+ end
415
+ end
416
+
417
+ required.each do |sym, val|
418
+ raise CommandlineError, "option --#{@specs[sym][:long]} must be specified" unless given_args.include? sym
419
+ end
420
+
421
+ ## parse parameters
422
+ given_args.each do |sym, given_data|
423
+ arg, params, negative_given = given_data.values_at :arg, :params, :negative_given
424
+
425
+ opts = @specs[sym]
426
+ raise CommandlineError, "option '#{arg}' needs a parameter" if params.empty? && opts[:type] != :flag
427
+
428
+ vals["#{sym}_given".intern] = true # mark argument as specified on the commandline
429
+
430
+ case opts[:type]
431
+ when :flag
432
+ vals[sym] = (sym.to_s =~ /^no_/ ? negative_given : !negative_given)
433
+ when :int, :ints
434
+ vals[sym] = params.map { |pg| pg.map { |p| parse_integer_parameter p, arg } }
435
+ when :float, :floats
436
+ vals[sym] = params.map { |pg| pg.map { |p| parse_float_parameter p, arg } }
437
+ when :string, :strings
438
+ vals[sym] = params.map { |pg| pg.map { |p| p.to_s } }
439
+ when :io, :ios
440
+ vals[sym] = params.map { |pg| pg.map { |p| parse_io_parameter p, arg } }
441
+ when :date, :dates
442
+ vals[sym] = params.map { |pg| pg.map { |p| parse_date_parameter p, arg } }
443
+ end
444
+
445
+ if SINGLE_ARG_TYPES.include?(opts[:type])
446
+ unless opts[:multi] # single parameter
447
+ vals[sym] = vals[sym][0][0]
448
+ else # multiple options, each with a single parameter
449
+ vals[sym] = vals[sym].map { |p| p[0] }
450
+ end
451
+ elsif MULTI_ARG_TYPES.include?(opts[:type]) && !opts[:multi]
452
+ vals[sym] = vals[sym][0] # single option, with multiple parameters
453
+ end
454
+ # else: multiple options, with multiple parameters
455
+ end
456
+
457
+ ## modify input in place with only those
458
+ ## arguments we didn't process
459
+ cmdline.clear
460
+ @leftovers.each { |l| cmdline << l }
461
+
462
+ ## allow openstruct-style accessors
463
+ class << vals
464
+ def method_missing(m, *args)
465
+ self[m] || self[m.to_s]
466
+ end
467
+ end
468
+ vals
469
+ end
470
+
471
+ def parse_date_parameter param, arg #:nodoc:
472
+ begin
473
+ begin
474
+ time = Chronic.parse(param)
475
+ rescue NameError
476
+ # chronic is not available
477
+ end
478
+ time ? Date.new(time.year, time.month, time.day) : Date.parse(param)
479
+ rescue ArgumentError
480
+ raise CommandlineError, "option '#{arg}' needs a date"
481
+ end
482
+ end
483
+
484
+ ## Print the help message to +stream+.
485
+ def educate stream=$stdout
486
+ width # hack: calculate it now; otherwise we have to be careful not to
487
+ # call this unless the cursor's at the beginning of a line.
488
+ left = {}
489
+ @specs.each do |name, spec|
490
+ left[name] = "--#{spec[:long]}" +
491
+ (spec[:type] == :flag && spec[:default] ? ", --no-#{spec[:long]}" : "") +
492
+ (spec[:short] && spec[:short] != :none ? ", -#{spec[:short]}" : "") +
493
+ case spec[:type]
494
+ when :flag;
495
+ ""
496
+ when :int;
497
+ " <i>"
498
+ when :ints;
499
+ " <i+>"
500
+ when :string;
501
+ " <s>"
502
+ when :strings;
503
+ " <s+>"
504
+ when :float;
505
+ " <f>"
506
+ when :floats;
507
+ " <f+>"
508
+ when :io;
509
+ " <filename/uri>"
510
+ when :ios;
511
+ " <filename/uri+>"
512
+ when :date;
513
+ " <date>"
514
+ when :dates;
515
+ " <date+>"
516
+ end
517
+ end
518
+
519
+ leftcol_width = left.values.map { |s| s.length }.max || 0
520
+ rightcol_start = leftcol_width + 6 # spaces
521
+
522
+ unless @order.size > 0 && @order.first.first == :text
523
+ stream.puts "#@version\n" if @version
524
+ stream.puts "Options:"
525
+ end
526
+
527
+ @order.each do |what, opt|
528
+ if what == :text
529
+ stream.puts wrap(opt)
530
+ next
531
+ end
532
+
533
+ spec = @specs[opt]
534
+ stream.printf " %#{leftcol_width}s: ", left[opt]
535
+ desc = spec[:desc] + begin
536
+ default_s = case spec[:default]
537
+ when $stdout;
538
+ "<stdout>"
539
+ when $stdin;
540
+ "<stdin>"
541
+ when $stderr;
542
+ "<stderr>"
543
+ when Array
544
+ spec[:default].join(", ")
545
+ else
546
+ spec[:default].to_s
547
+ end
548
+
549
+ if spec[:default]
550
+ if spec[:desc] =~ /\.$/
551
+ " (Default: #{default_s})"
552
+ else
553
+ " (default: #{default_s})"
554
+ end
555
+ else
556
+ ""
557
+ end
558
+ end
559
+ stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
560
+ end
561
+ end
562
+
563
+ def width #:nodoc:
564
+ @width ||= if $stdout.tty?
565
+ begin
566
+ require 'curses'
567
+ Curses::init_screen
568
+ x = Curses::cols
569
+ Curses::close_screen
570
+ x
571
+ rescue Exception
572
+ 80
573
+ end
574
+ else
575
+ 80
576
+ end
577
+ end
578
+
579
+ def wrap str, opts={} # :nodoc:
580
+ if str == ""
581
+ [""]
582
+ else
583
+ str.split("\n").map { |s| wrap_line s, opts }.flatten
584
+ end
585
+ end
586
+
587
+ ## The per-parser version of Trollop::die (see that for documentation).
588
+ def die arg, msg
589
+ if msg
590
+ $stderr.puts "Error: argument --#{@specs[arg][:long]} #{msg}."
591
+ else
592
+ $stderr.puts "Error: #{arg}."
593
+ end
594
+ $stderr.puts "Try --help for help."
595
+ exit(1)
596
+ end
597
+
598
+ private
599
+
600
+ ## yield successive arg, parameter pairs
601
+ def each_arg args
602
+ remains = []
603
+ i = 0
604
+
605
+ until i >= args.length
606
+ if @stop_words.member? args[i]
607
+ remains += args[i .. -1]
608
+ return remains
609
+ end
610
+ case args[i]
611
+ when /^--$/ # arg terminator
612
+ remains += args[(i + 1) .. -1]
613
+ return remains
614
+ when /^--(\S+?)=(.*)$/ # long argument with equals
615
+ yield "--#{$1}", [$2]
616
+ i += 1
617
+ when /^--(\S+)$/ # long argument
618
+ params = collect_argument_parameters(args, i + 1)
619
+ unless params.empty?
620
+ num_params_taken = yield args[i], params
621
+ unless num_params_taken
622
+ if @stop_on_unknown
623
+ remains += args[i + 1 .. -1]
624
+ return remains
625
+ else
626
+ remains += params
627
+ end
628
+ end
629
+ i += 1 + num_params_taken
630
+ else # long argument no parameter
631
+ yield args[i], nil
632
+ i += 1
633
+ end
634
+ when /^-(\S+)$/ # one or more short arguments
635
+ shortargs = $1.split(//)
636
+ shortargs.each_with_index do |a, j|
637
+ if j == (shortargs.length - 1)
638
+ params = collect_argument_parameters(args, i + 1)
639
+ unless params.empty?
640
+ num_params_taken = yield "-#{a}", params
641
+ unless num_params_taken
642
+ if @stop_on_unknown
643
+ remains += args[i + 1 .. -1]
644
+ return remains
645
+ else
646
+ remains += params
647
+ end
648
+ end
649
+ i += 1 + num_params_taken
650
+ else # argument no parameter
651
+ yield "-#{a}", nil
652
+ i += 1
653
+ end
654
+ else
655
+ yield "-#{a}", nil
656
+ end
657
+ end
658
+ else
659
+ if @stop_on_unknown
660
+ remains += args[i .. -1]
661
+ return remains
662
+ else
663
+ remains << args[i]
664
+ i += 1
665
+ end
666
+ end
667
+ end
668
+
669
+ remains
670
+ end
671
+
672
+ def parse_integer_parameter param, arg
673
+ raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^\d+$/
674
+ param.to_i
675
+ end
676
+
677
+ def parse_float_parameter param, arg
678
+ raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE
679
+ param.to_f
680
+ end
681
+
682
+ def parse_io_parameter param, arg
683
+ case param
684
+ when /^(stdin|-)$/i;
685
+ $stdin
686
+ else
687
+ require 'open-uri'
688
+ begin
689
+ open param
690
+ rescue SystemCallError => e
691
+ raise CommandlineError, "file or url for option '#{arg}' cannot be opened: #{e.message}"
692
+ end
693
+ end
694
+ end
695
+
696
+ def collect_argument_parameters args, start_at
697
+ params = []
698
+ pos = start_at
699
+ while args[pos] && args[pos] !~ PARAM_RE && !@stop_words.member?(args[pos]) do
700
+ params << args[pos]
701
+ pos += 1
702
+ end
703
+ params
704
+ end
705
+
706
+ def resolve_default_short_options!
707
+ @order.each do |type, name|
708
+ next unless type == :opt
709
+ opts = @specs[name]
710
+ next if opts[:short]
711
+
712
+ c = opts[:long].split(//).find { |d| d !~ INVALID_SHORT_ARG_REGEX && !@short.member?(d) }
713
+ if c # found a character to use
714
+ opts[:short] = c
715
+ @short[c] = name
716
+ end
717
+ end
718
+ end
719
+
720
+ def wrap_line str, opts={}
721
+ prefix = opts[:prefix] || 0
722
+ width = opts[:width] || (self.width - 1)
723
+ start = 0
724
+ ret = []
725
+ until start > str.length
726
+ nextt =
727
+ if start + width >= str.length
728
+ str.length
729
+ else
730
+ x = str.rindex(/\s/, start + width)
731
+ x = str.index(/\s/, start) if x && x < start
732
+ x || str.length
733
+ end
734
+ ret << (ret.empty? ? "" : " " * prefix) + str[start ... nextt]
735
+ start = nextt + 1
736
+ end
737
+ ret
738
+ end
739
+
740
+ ## instance_eval but with ability to handle block arguments
741
+ ## thanks to _why: http://redhanded.hobix.com/inspect/aBlockCostume.html
742
+ def cloaker &b
743
+ (
744
+ class << self;
745
+ self;
746
+ end).class_eval do
747
+ define_method :cloaker_, &b
748
+ meth = instance_method :cloaker_
749
+ remove_method :cloaker_
750
+ meth
751
+ end
752
+ end
753
+ end
754
+
755
+ ## The easy, syntactic-sugary entry method into Trollop. Creates a Parser,
756
+ ## passes the block to it, then parses +args+ with it, handling any errors or
757
+ ## requests for help or version information appropriately (and then exiting).
758
+ ## Modifies +args+ in place. Returns a hash of option values.
759
+ ##
760
+ ## The block passed in should contain zero or more calls to +opt+
761
+ ## (Parser#opt), zero or more calls to +text+ (Parser#text), and
762
+ ## probably a call to +version+ (Parser#version).
763
+ ##
764
+ ## The returned block contains a value for every option specified with
765
+ ## +opt+. The value will be the value given on the commandline, or the
766
+ ## default value if the option was not specified on the commandline. For
767
+ ## every option specified on the commandline, a key "<option
768
+ ## name>_given" will also be set in the hash.
769
+ ##
770
+ ## Example:
771
+ ##
772
+ ## require 'trollop'
773
+ ## opts = Trollop::options do
774
+ ## opt :monkey, "Use monkey mode" # a flag --monkey, defaulting to false
775
+ ## opt :name, "Monkey name", :type => :string # a string --name <s>, defaulting to nil
776
+ ## opt :num_limbs, "Number of limbs", :default => 4 # an integer --num-limbs <i>, defaulting to 4
777
+ ## end
778
+ ##
779
+ ## ## if called with no arguments
780
+ ## p opts # => {:monkey=>false, :name=>nil, :num_limbs=>4, :help=>false}
781
+ ##
782
+ ## ## if called with --monkey
783
+ ## p opts # => {:monkey=>true, :name=>nil, :num_limbs=>4, :help=>false, :monkey_given=>true}
784
+ ##
785
+ ## See more examples at http://trollop.rubyforge.org.
786
+ def options args=ARGV, *a, &b
787
+ @last_parser = Parser.new(*a, &b)
788
+ with_standard_exception_handling(@last_parser) { @last_parser.parse args }
789
+ end
790
+
791
+ ## If Trollop::options doesn't do quite what you want, you can create a Parser
792
+ ## object and call Parser#parse on it. That method will throw CommandlineError,
793
+ ## HelpNeeded and VersionNeeded exceptions when necessary; if you want to
794
+ ## have these handled for you in the standard manner (e.g. show the help
795
+ ## and then exit upon an HelpNeeded exception), call your code from within
796
+ ## a block passed to this method.
797
+ ##
798
+ ## Note that this method will call System#exit after handling an exception!
799
+ ##
800
+ ## Usage example:
801
+ ##
802
+ ## require 'trollop'
803
+ ## p = Trollop::Parser.new do
804
+ ## opt :monkey, "Use monkey mode" # a flag --monkey, defaulting to false
805
+ ## opt :goat, "Use goat mode", :default => true # a flag --goat, defaulting to true
806
+ ## end
807
+ ##
808
+ ## opts = Trollop::with_standard_exception_handling p do
809
+ ## o = p.parse ARGV
810
+ ## raise Trollop::HelpNeeded if ARGV.empty? # show help screen
811
+ ## o
812
+ ## end
813
+ ##
814
+ ## Requires passing in the parser object.
815
+
816
+ def with_standard_exception_handling(parser)
817
+ begin
818
+ yield
819
+ rescue CommandlineError => e
820
+ raise Convoy::UserError.new(e.message, e)
821
+ #$stderr.puts "Error: #{e.message}."
822
+ #$stderr.puts "Try --help for help."
823
+ #exit(1)
824
+ rescue HelpNeeded
825
+ parser.current_help_formatter ? parser.current_help_formatter.print(parser) : parser.educate
826
+ exit
827
+ rescue VersionNeeded
828
+ puts parser.version
829
+ exit
830
+ end
831
+ end
832
+
833
+ ## Informs the user that their usage of 'arg' was wrong, as detailed by
834
+ ## 'msg', and dies. Example:
835
+ ##
836
+ ## options do
837
+ ## opt :volume, :default => 0.0
838
+ ## end
839
+ ##
840
+ ## die :volume, "too loud" if opts[:volume] > 10.0
841
+ ## die :volume, "too soft" if opts[:volume] < 0.1
842
+ ##
843
+ ## In the one-argument case, simply print that message, a notice
844
+ ## about -h, and die. Example:
845
+ ##
846
+ ## options do
847
+ ## opt :whatever # ...
848
+ ## end
849
+ ##
850
+ ## Trollop::die "need at least one filename" if ARGV.empty?
851
+ def die arg, msg=nil
852
+ if @last_parser
853
+ @last_parser.die arg, msg
854
+ else
855
+ raise ArgumentError, "Trollop::die can only be called after Trollop::options"
856
+ end
857
+ end
858
+
859
+ module_function :options, :die, :with_standard_exception_handling
860
+
861
+ end # module