methadone-rehab 1.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +11 -0
  6. data/CHANGES.md +66 -0
  7. data/Gemfile +6 -0
  8. data/LICENSE.txt +201 -0
  9. data/README.rdoc +179 -0
  10. data/Rakefile +98 -0
  11. data/TODO.md +3 -0
  12. data/bin/methadone +157 -0
  13. data/features/bootstrap.feature +169 -0
  14. data/features/license.feature +43 -0
  15. data/features/multilevel_commands.feature +125 -0
  16. data/features/readme.feature +26 -0
  17. data/features/rspec_support.feature +27 -0
  18. data/features/step_definitions/bootstrap_steps.rb +47 -0
  19. data/features/step_definitions/license_steps.rb +30 -0
  20. data/features/step_definitions/readme_steps.rb +26 -0
  21. data/features/step_definitions/version_steps.rb +4 -0
  22. data/features/support/env.rb +26 -0
  23. data/features/version.feature +17 -0
  24. data/lib/methadone.rb +15 -0
  25. data/lib/methadone/argv_parser.rb +50 -0
  26. data/lib/methadone/cli.rb +124 -0
  27. data/lib/methadone/cli_logger.rb +133 -0
  28. data/lib/methadone/cli_logging.rb +138 -0
  29. data/lib/methadone/cucumber.rb +174 -0
  30. data/lib/methadone/error.rb +32 -0
  31. data/lib/methadone/execution_strategy/base.rb +34 -0
  32. data/lib/methadone/execution_strategy/jvm.rb +37 -0
  33. data/lib/methadone/execution_strategy/mri.rb +16 -0
  34. data/lib/methadone/execution_strategy/open_3.rb +16 -0
  35. data/lib/methadone/execution_strategy/open_4.rb +22 -0
  36. data/lib/methadone/execution_strategy/rbx_open_4.rb +12 -0
  37. data/lib/methadone/exit_now.rb +40 -0
  38. data/lib/methadone/main.rb +1039 -0
  39. data/lib/methadone/process_status.rb +45 -0
  40. data/lib/methadone/sh.rb +223 -0
  41. data/lib/methadone/version.rb +3 -0
  42. data/methadone-rehab.gemspec +32 -0
  43. data/templates/full/.gitignore.erb +4 -0
  44. data/templates/full/README.rdoc.erb +25 -0
  45. data/templates/full/Rakefile.erb +74 -0
  46. data/templates/full/_license_head.txt.erb +2 -0
  47. data/templates/full/apache_LICENSE.txt.erb +203 -0
  48. data/templates/full/bin/executable.erb +47 -0
  49. data/templates/full/custom_LICENSE.txt.erb +0 -0
  50. data/templates/full/features/executable.feature.erb +13 -0
  51. data/templates/full/features/step_definitions/executable_steps.rb.erb +1 -0
  52. data/templates/full/features/support/env.rb.erb +16 -0
  53. data/templates/full/gplv2_LICENSE.txt.erb +14 -0
  54. data/templates/full/gplv3_LICENSE.txt.erb +15 -0
  55. data/templates/full/mit_LICENSE.txt.erb +7 -0
  56. data/templates/multicommand/bin/executable.erb +52 -0
  57. data/templates/multicommand/lib/command.rb.erb +40 -0
  58. data/templates/multicommand/lib/commands.rb.erb +7 -0
  59. data/templates/rspec/spec/something_spec.rb.erb +5 -0
  60. data/templates/test_unit/test/tc_something.rb.erb +7 -0
  61. data/test/base_test.rb +20 -0
  62. data/test/command_for_tests.sh +7 -0
  63. data/test/execution_strategy/test_base.rb +24 -0
  64. data/test/execution_strategy/test_jvm.rb +77 -0
  65. data/test/execution_strategy/test_mri.rb +32 -0
  66. data/test/execution_strategy/test_open_3.rb +70 -0
  67. data/test/execution_strategy/test_open_4.rb +86 -0
  68. data/test/execution_strategy/test_rbx_open_4.rb +25 -0
  69. data/test/test_cli_logger.rb +219 -0
  70. data/test/test_cli_logging.rb +243 -0
  71. data/test/test_exit_now.rb +37 -0
  72. data/test/test_main.rb +1213 -0
  73. data/test/test_multi.rb +405 -0
  74. data/test/test_sh.rb +404 -0
  75. metadata +321 -0
@@ -0,0 +1,34 @@
1
+ module Methadone
2
+ module ExecutionStrategy
3
+ # Base for any ExecutionStrategy implementation. Currently, this is nothing more than an interface
4
+ # specification.
5
+ class Base
6
+ # Executes the command and returns the results back. This
7
+ # should do no logging or other logic other than to execute the
8
+ # command and return the required results. If command is an
9
+ # array, use exec directly bypassing any tokenization, shell or
10
+ # otherwise; otherwise use the normal shell interpretation of
11
+ # the command string.
12
+ #
13
+ # command:: the command-line to run, as an Array or a String
14
+ #
15
+ # Returns an array of size 3:
16
+ # <tt>[0]</tt>:: The standard output of the command as a String, never nil
17
+ # <tt>[1]</tt>:: The standard error output of the command as a String, never nil
18
+ # <tt>[2]</tt>:: A Process::Status-like objects that responds to <tt>exitstatus</tt> which returns
19
+ # the exit code of the command (e.g. 0 for success).
20
+ def run_command(command)
21
+ subclass_must_implement!
22
+ end
23
+
24
+ # Returns the class that, if caught by calling #run_command, represents the underlying command
25
+ # not existing. For example, in MRI Ruby, if you try to execute a non-existent command,
26
+ # you get a Errno::ENOENT.
27
+ def exception_meaning_command_not_found
28
+ subclass_must_implement!
29
+ end
30
+ protected
31
+ def subclass_must_implement!; raise "subclass must implement"; end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,37 @@
1
+ module Methadone
2
+ module ExecutionStrategy
3
+ # <b>Methadone Internal - treat as private</b>
4
+ #
5
+ # Methadone::ExecutionStrategy for the JVM that uses JVM classes to run the command and get its results.
6
+ class JVM < Base
7
+ def run_command(command)
8
+ process = case command
9
+ when String then
10
+ java.lang.Runtime.get_runtime.exec(command)
11
+ else
12
+ java.lang.Runtime.get_runtime.exec(*command)
13
+ end
14
+ process.get_output_stream.close
15
+ stdout = input_stream_to_string(process.get_input_stream)
16
+ stderr = input_stream_to_string(process.get_error_stream)
17
+ exitstatus = process.wait_for
18
+ [stdout.chomp,stderr.chomp,OpenStruct.new(:exitstatus => exitstatus)]
19
+ end
20
+
21
+ def exception_meaning_command_not_found
22
+ NativeException
23
+ end
24
+
25
+ private
26
+ def input_stream_to_string(is)
27
+ ''.tap do |string|
28
+ ch = is.read
29
+ while ch != -1
30
+ string << ch
31
+ ch = is.read
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,16 @@
1
+ module Methadone
2
+ module ExecutionStrategy
3
+ # <b>Methadone Internal - treat as private</b>
4
+ #
5
+ # Base strategy for MRI rubies.
6
+ class MRI < Base
7
+ def run_command(command)
8
+ raise "subclass must implement"
9
+ end
10
+
11
+ def exception_meaning_command_not_found
12
+ Errno::ENOENT
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module Methadone
2
+ module ExecutionStrategy
3
+ # <b>Methadone Internal - treat as private</b>
4
+ #
5
+ # Implementation for modern Rubies that uses the built-in Open3 library
6
+ class Open_3 < MRI
7
+ def run_command(command)
8
+ stdout,stderr,status = case command
9
+ when String then Open3.capture3(command)
10
+ else Open3.capture3(*command)
11
+ end
12
+ [stdout.chomp,stderr.chomp,status]
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ module Methadone
2
+ module ExecutionStrategy
3
+ # <b>Methadone Internal - treat as private</b>
4
+ #
5
+ # ExecutionStrategy for non-modern Rubies that must rely on
6
+ # Open4 to get access to the standard output AND error.
7
+ class Open_4 < MRI
8
+ def run_command(command)
9
+ pid, stdin_io, stdout_io, stderr_io =
10
+ case command
11
+ when String then Open4::popen4(command)
12
+ else Open4::popen4(*command)
13
+ end
14
+ stdin_io.close
15
+ stdout = stdout_io.read
16
+ stderr = stderr_io.read
17
+ _ , status = Process::waitpid2(pid)
18
+ [stdout.chomp,stderr.chomp,status]
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,12 @@
1
+ module Methadone
2
+ module ExecutionStrategy
3
+ # <b>Methadone Internal - treat as private</b>
4
+ #
5
+ # For RBX; it throws a different exception when a command isn't found, so we override that here.
6
+ class RBXOpen_4 < Open_4
7
+ def exception_meaning_command_not_found
8
+ [Errno::EINVAL] + Array(super)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,40 @@
1
+ module Methadone
2
+ # Provides #exit_now! and #help_now!. You might mix this into your business logic classes if they will
3
+ # need to exit the program with a human-readable error message.
4
+ module ExitNow
5
+ def self.included(k)
6
+ k.extend(self)
7
+ end
8
+ # Call this to exit the program immediately
9
+ # with the given error code and message.
10
+ #
11
+ # +exit_code+:: exit status you'd like to exit with
12
+ # +message+:: message to display to the user explaining the problem
13
+ #
14
+ # If +exit_code+ is a String and +message+ is omitted, +exit_code+ will be used as the message
15
+ # and the actual exit code will be 1.
16
+ #
17
+ # === Examples
18
+ #
19
+ # exit_now!(4,"Oh noes!")
20
+ # # => exit app with status 4 and show the user "Oh noes!" on stderr
21
+ # exit_now!("Oh noes!")
22
+ # # => exit app with status 1 and show the user "Oh noes!" on stderr
23
+ # exit_now!(4)
24
+ # # => exit app with status 4 and dont' give the user a message (how rude of you)
25
+ def exit_now!(exit_code,message=nil)
26
+ if exit_code.kind_of?(String) && message.nil?
27
+ raise Methadone::Error.new(1,exit_code)
28
+ else
29
+ raise Methadone::Error.new(exit_code,message)
30
+ end
31
+ end
32
+
33
+ # Exit the program as if the user made an error invoking your app, providing
34
+ # them the message as well as printing the help. This is useful if
35
+ # you have complex UI validation that can't be done by OptionParser.
36
+ def help_now!(message)
37
+ raise OptionParser::ParseError.new(message)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,1039 @@
1
+ require 'optparse'
2
+ require 'yaml'
3
+ begin
4
+ Module.const_get('BasicObject')
5
+ # We are 1.9.x
6
+ rescue NameError
7
+ BasicObject = Object
8
+ end
9
+
10
+ module Methadone
11
+ # Include this module to gain access to the "canonical command-line app structure"
12
+ # DSL. This is a *very* lightweight layer on top of what you might
13
+ # normally write that gives you just a bit of help to keep your code structured
14
+ # in a sensible way. You can use as much or as little as you want, though
15
+ # you must at least use #main to get any benefits.
16
+ #
17
+ # Further, you must provide access to a logger via a method named
18
+ # #logger. If you include Methadone::CLILogging, this will be done for you
19
+ #
20
+ # You also get a more expedient interface to OptionParser as well
21
+ # as checking for required arguments to your app. For example, if
22
+ # we want our app to accept a negatable switch named "switch", a flag
23
+ # named "flag", and two arguments "needed" (which is required)
24
+ # and "maybe" which is optional, we can do the following:
25
+ #
26
+ # #!/usr/bin/env ruby
27
+ #
28
+ # require 'methadone'
29
+ #
30
+ # class App
31
+ # include Methadone::Main
32
+ # include Methadone::CLILogging
33
+ #
34
+ # main do |needed, maybe|
35
+ # options[:switch] => true or false, based on command line
36
+ # options[:flag] => value of flag passed on command line
37
+ # end
38
+ #
39
+ # # Proxy to an OptionParser instance's on method
40
+ # on("--[no]-switch")
41
+ # on("--flag VALUE")
42
+ #
43
+ # arg :needed
44
+ # arg :maybe, :optional
45
+ #
46
+ # defaults_from_env_var SOME_VAR
47
+ # defaults_from_config_file '.my_app.rc'
48
+ #
49
+ # go!
50
+ # end
51
+ #
52
+ # Our app then acts as follows:
53
+ #
54
+ # $ our_app
55
+ # # => parse error: 'needed' is required
56
+ # $ our_app foo
57
+ # # => succeeds; "maybe" in main is nil
58
+ # $ our_app --flag foo
59
+ # # => options[:flag] has the value "foo"
60
+ # $ SOME_VAR='--flag foo' our_app
61
+ # # => options[:flag] has the value "foo"
62
+ # $ SOME_VAR='--flag foo' our_app --flag bar
63
+ # # => options[:flag] has the value "bar"
64
+ #
65
+ # Note that we've done all of this inside a class that we called +App+. This isn't strictly
66
+ # necessary, and you can just +include+ Methadone::Main and Methadone::CLILogging at the root
67
+ # of your +bin+ file if you like. This is somewhat unsafe, because +self+ inside the +bin+
68
+ # file is Object, and any methods you create (or cause to be created via +include+) will be
69
+ # present on *every* object. This can cause odd problems, so it's recommended that you
70
+ # *not* do this.
71
+ #
72
+ # Subcommands
73
+ # -----------
74
+ #
75
+ # In order to promote modularity and maintainability, complex command line
76
+ # applications should be broken up into subcommands. Subcommands are just
77
+ # like regular Methadone applications, except you don't put a go! call in it.
78
+ # It will be run in by the base methadone app class. Likewise, subcommands
79
+ # can have subcommands of their own.
80
+ #
81
+ # In order to tell a Methadone app class that it has subcommands, use the
82
+ # command method, which takes a hash with the command name as a key and the
83
+ # command class as the value. Multiple subcommands can be specified in a
84
+ # single call, or as separate calls.
85
+ #
86
+ # #!/usr/bin/env ruby
87
+ #
88
+ # require 'methadone'
89
+ #
90
+ # class MySubcommand
91
+ # include Methadone::Main
92
+ # include Methadone::CLILogging
93
+ #
94
+ # on '-f','--foo BAR', 'Some option'
95
+ # arg 'something', :required, "Description","defaults: value"
96
+ #
97
+ # main do |something|
98
+ # # stuff
99
+ # end
100
+ # end
101
+ #
102
+ # class App
103
+ # include Methadone::Main
104
+ # include Methadone::CLILogging
105
+ #
106
+ # command "do" => MySubcommand
107
+ #
108
+ # go!
109
+ # end
110
+ #
111
+ # Apps that have subcommands (currently) don't support arguments and don't
112
+ # need to supply a main, as it doesn't get called. This may change in a
113
+ # future version of Methadone. Options to the app can modify the +options+
114
+ # contents will impactful to the subcommand as it receives those option
115
+ # values as the base for its options.
116
+ #
117
+ module Main
118
+ include Methadone::ExitNow
119
+ include Methadone::ARGVParser
120
+
121
+ def self.included(k)
122
+ k.extend(self)
123
+ end
124
+
125
+ # Declare the main method for your app.
126
+ # This allows you to specify the general logic of your
127
+ # app at the top of your bin file, but can rely on any methods
128
+ # or other code that you define later.
129
+ #
130
+ # For example, suppose you want to process a set of files, but
131
+ # wish to determine that list from another method to keep your
132
+ # code clean.
133
+ #
134
+ # #!/usr/bin/env ruby -w
135
+ #
136
+ # require 'methadone'
137
+ #
138
+ # include Methadone::Main
139
+ #
140
+ # main do
141
+ # files_to_process.each do |file|
142
+ # # process file
143
+ # end
144
+ # end
145
+ #
146
+ # def files_to_process
147
+ # # return list of files
148
+ # end
149
+ #
150
+ # go!
151
+ #
152
+ # The block can accept any parameters, and unparsed arguments
153
+ # from the command line will be passed.
154
+ #
155
+ # *Note*: #go! will modify +ARGV+ to remove any known options and
156
+ # arguments. If there are any values left over, they will remain available
157
+ # in +ARGV+. This behaviour is different from 1.x versions of Methadone,
158
+ # which emptied +ARGV+ completely
159
+ #
160
+ # To run this method, call #go!
161
+ def main(&block)
162
+ @main_block = block
163
+ end
164
+
165
+ # Configure the auto-handling of StandardError exceptions caught
166
+ # from calling go!.
167
+ #
168
+ # leak:: if true, go! will *not* catch StandardError exceptions, but
169
+ # instead allow them to bubble up. If false, they will be caught
170
+ # and handled as normal. This does *not* affect Methadone::Error
171
+ # exceptions; those will NOT leak through.
172
+ #
173
+ # leak_exceptions only needs to be set once; since it is stored as a
174
+ # class variable, all classes that include this module will handle
175
+ # exceptions the same way.
176
+ def leak_exceptions(leak)
177
+ @@leak_exceptions = leak
178
+ end
179
+
180
+ # Print the usage help if the command is run without any options or
181
+ # arguments.
182
+ def help_if_bare
183
+ @default_help = true
184
+ end
185
+
186
+ # Set the name of the environment variable where users can place default
187
+ # options for your app. Omit this to disable the feature.
188
+ def defaults_from_env_var(env_var)
189
+ @env_var = env_var
190
+ end
191
+
192
+ # Set the path to the file where defaults can be configured.
193
+ #
194
+ # The format of this file can be either a simple string of options, like what goes
195
+ # in the environment variable (see #defaults_from_env_var), or YAML, in which case
196
+ # it should be a hash where keys are the option names, and values their defaults.
197
+ #
198
+ # Relative paths will be expanded relative to the user's home directory.
199
+ #
200
+ # filename:: path to the file. If relative, will look in user's HOME directory.
201
+ # If absolute, this is the absolute path to where the file should be.
202
+ def defaults_from_config_file(filename,options={})
203
+ @rc_file = File.expand_path(filename, ENV['HOME'])
204
+ end
205
+
206
+ # Start your command-line app, exiting appropriately when
207
+ # complete.
208
+ #
209
+ # This *will* exit your program when it completes. If your
210
+ # #main block evaluates to an integer, that value will be sent
211
+ # to Kernel#exit, otherwise, this will exit with 0
212
+ #
213
+ # If the command-line options couldn't be parsed, this
214
+ # will exit with 64 and whatever message OptionParser provided.
215
+ #
216
+ # If a required argument (see #arg) is not found, this exits with
217
+ # 64 and a message about that missing argument.
218
+ #
219
+ def go!(parent=nil)
220
+
221
+ if @default_help and ARGV.empty?
222
+ puts opts.to_s
223
+ exit 64 # sysexits.h exit code EX_USAGE
224
+ end
225
+
226
+ # Get stuff from parent, if there
227
+ set_parent(parent)
228
+
229
+ setup_defaults
230
+ opts.post_setup
231
+
232
+ if opts.commands.empty?
233
+ opts.parse!
234
+ opts.check_args!
235
+ opts.check_option_usage!
236
+ result = call_main
237
+ else
238
+ opts.parse_to_command! # Leaves unknown args and options in once it encounters a non-option.
239
+ opts.check_option_usage!
240
+ if opts.selected_command
241
+ result = call_provider
242
+ else
243
+ logger.error "You must specify a command"
244
+ puts ""
245
+ puts opts.help
246
+ exit 64
247
+ end
248
+ end
249
+
250
+ if result.kind_of? Fixnum
251
+ exit result
252
+ else
253
+ exit 0
254
+ end
255
+ rescue OptionParser::ParseError => ex
256
+ logger.error ex.message
257
+ puts
258
+ puts opts.help
259
+ exit 64 # Linux standard for bad command line
260
+ end
261
+
262
+ # Returns an OptionParser that you can use
263
+ # to declare your command-line interface. Generally, you
264
+ # won't use this and will use #on directly, but this allows
265
+ # you to have complete control of option parsing.
266
+ #
267
+ # The object returned has
268
+ # an additional feature that implements typical use of OptionParser.
269
+ #
270
+ # opts.on("--flag VALUE")
271
+ #
272
+ # Does this under the covers:
273
+ #
274
+ # opts.on("--flag VALUE") do |value|
275
+ # options[:flag] = value
276
+ # end
277
+ #
278
+ # Since, most of the time, this is all you want to do, this makes it more
279
+ # expedient to do so. The key that is is set in #options will be a symbol
280
+ # <i>and string</i> of the option name, without the leading dashes. Note
281
+ # that if you use multiple option names, a key will be generated for each.
282
+ # Further, if you use the negatable form, only the positive key will be set,
283
+ # e.g. for <tt>--[no-]verbose</tt>, only <tt>:verbose</tt> will be set (to
284
+ # true or false).
285
+ #
286
+ # As an example, this declaration:
287
+ #
288
+ # opts.on("-f VALUE", "--flag")
289
+ #
290
+ # And this command-line invocation:
291
+ #
292
+ # $ my_app -f foo
293
+ #
294
+ # Will result in all of these forms returning the String "foo":
295
+ # * <tt>options['f']</tt>
296
+ # * <tt>options[:f]</tt>
297
+ # * <tt>options['flag']</tt>
298
+ # * <tt>options[:flag]</tt>
299
+ #
300
+ # Further, any one of those keys can be used to determine the default value for the option.
301
+ #
302
+ # Playing well with others
303
+ # ------------------------
304
+ #
305
+ # Sometimes you need the user to specify groups of options, or sometimes
306
+ # one option cannot be used in conjunction with another option. While
307
+ # OptionParser does not natively support this, options defined with
308
+ # Methadone's +on+ method does so by using the following hash arguments:
309
+ #
310
+ # :excludes => <optID>
311
+ # :requires => <optID>
312
+ #
313
+ # The optID can be any of the keys that an option would create in the
314
+ # options hash. You can even specify multiple options by using an array of
315
+ # optIDs:
316
+ #
317
+ # :excludes => [:f, "another-option"]
318
+ #
319
+ # If you specify both an option and another option that excludes that
320
+ # option, an error is logged. Only one side of an exclusion needs to be
321
+ # specified.
322
+ #
323
+ # If you use an option, but do not use an option it requires, an error will
324
+ # be logged. Order of the options do not matter.
325
+ #
326
+ def opts
327
+ @option_parser ||= OptionParserProxy.new(OptionParser.new,options)
328
+ end
329
+
330
+ # Calls the +on+ method of #opts with the given arguments (see RDoc for #opts for the additional
331
+ # help provided).
332
+ def on(*args,&block)
333
+ opts.on(*args,&block)
334
+ end
335
+
336
+ # Calls the +command+ method of #opts with the given arguments (see RDoc
337
+ # for #opts for the additional help provided). Commands are special args
338
+ # that take their own options and other arguments.
339
+ def command(*args)
340
+ opts.command(*args)
341
+ end
342
+
343
+ # Sets the name of an arguments your app accepts.
344
+ # +arg_name+:: name of the argument to appear in documentation
345
+ # This will be converted into a String and used to create
346
+ # the banner (unless you have overridden the banner)
347
+ # +options+:: list (not Hash) of options:
348
+ # <tt>:required</tt>:: this arg is required (this is the default)
349
+ # <tt>:optional</tt>:: this arg is optional
350
+ # <tt>:one</tt>:: only one of this arg should be supplied (default)
351
+ # <tt>:many</tt>:: many of this arg may be supplied, but at least one is required
352
+ # <tt>:any</tt>:: any number, include zero, may be supplied
353
+ # A string:: if present, this will be documentation for the
354
+ # argument and appear in the help. Multiple strings will be
355
+ # listed on multiple lines
356
+ # A Regexp:: Argument values must match the regexp, or an error will be raised.
357
+ # An Array:: Argument values must be found in the array, or an error will be raised.
358
+ #
359
+ # As of version 2.0, best effort is made to ensure values are assigned to
360
+ # your arguments as needed. :required and :many options will take one
361
+ # value if possible, and the first greedy argument (:many or :any) will
362
+ # consume any unallocated count of values remaining in ARGV. Value
363
+ # assignment still goes left to right, but allocation counts are determined
364
+ # by needs of each argument. Filtering rules do not play a part in
365
+ # determining if a value can be allocated to an argument.
366
+ #
367
+ # Greedy arguments that do not receive any values will hold an empty
368
+ # array, while non-greedy arguments that do not receive a value will be
369
+ # nil.
370
+ def arg(arg_name,*options)
371
+ opts.arg(arg_name,*options)
372
+ end
373
+
374
+ # Set the description of your app for inclusion in the help output.
375
+ # +desc+:: a short, one-line description of your app
376
+ def description(desc=nil)
377
+ opts.description(desc)
378
+ end
379
+
380
+ # Returns a Hash that you can use to store or retrieve options
381
+ # parsed from the command line. When you put values in here, if you do so
382
+ # *before* you've declared your command-line interface via #on, the value
383
+ # will be used in the docstring to indicate it is the default.
384
+ # You can use either a String or a Symbol and, after #go! is called and
385
+ # the command-line is parsed, the values will be available as both
386
+ # a String and a Symbol.
387
+ #
388
+ # Example
389
+ #
390
+ # main do
391
+ # puts options[:foo] # put the value of --foo that the user provided
392
+ # end
393
+ #
394
+ # options[:foo] = "bar" # set "bar" as the default value for --foo, which
395
+ # # will cause us to include "(default: bar)" in the
396
+ # # docstring
397
+ #
398
+ # on("--foo FOO","Sets the foo")
399
+ # go!
400
+ #
401
+ def options
402
+ @options ||= {}
403
+ end
404
+
405
+ def global_options
406
+ (@parent.nil? ? {} : @parent.global_options).merge(
407
+ opts.global_options
408
+ )
409
+ end
410
+
411
+ # Set the version of your app so it appears in the
412
+ # banner. This also adds --version as an option to your app which,
413
+ # when used, will act just like --help (see version_options to control this)
414
+ #
415
+ # version:: the current version of your app. Should almost always be
416
+ # YourApp::VERSION, where the module YourApp should've been generated
417
+ # by the bootstrap script
418
+ # version_options:: controls how the version option behaves. If this is a string,
419
+ # then the string will be used as documentation for the --version flag.
420
+ # If a Hash, more configuration is available:
421
+ # custom_docs:: the string to document the --version flag if you don't like the default
422
+ # compact:: if true, --version will just show the app name and version - no help
423
+ # format:: if provided, this can give limited control over the format of the compact
424
+ # version string. It should be a printf-style string and will be given
425
+ # two options: the first is the CLI app name, and the second is the version string
426
+ def version(version,version_options={})
427
+ opts.version(version)
428
+ if version_options.kind_of?(Symbol)
429
+ case version_options
430
+ when :terse
431
+ version_options = {
432
+ :custom_docs => "Show version",
433
+ :format => '%0.0s%s',
434
+ :compact => true
435
+ }
436
+ when :basic
437
+ version_options = {
438
+ :custom_docs => "Show version info",
439
+ :compact => true
440
+ }
441
+ else
442
+ version_options = version_options.to_s
443
+ end
444
+ end
445
+
446
+ if version_options.kind_of?(String)
447
+ version_options = { :custom_docs => version_options }
448
+ end
449
+ version_options[:custom_docs] ||= "Show help/version info"
450
+ version_options[:format] ||= "%s version %s"
451
+ opts.on("--version",version_options[:custom_docs]) do
452
+ if version_options[:compact]
453
+ puts version_options[:format] % [::File.basename($0),version]
454
+ else
455
+ puts opts.to_s
456
+ end
457
+ exit 0
458
+ end
459
+ end
460
+
461
+ private
462
+
463
+ # Reset internal state - mostly useful for tests
464
+ def reset!
465
+ @options = nil
466
+ @option_parser = nil
467
+ end
468
+
469
+ def setup_defaults
470
+ add_defaults_to_docs
471
+ set_defaults_from_rc_file
472
+ normalize_defaults
473
+ set_defaults_from_env_var
474
+ end
475
+
476
+ def set_parent(parent)
477
+ @parent = parent
478
+ if parent
479
+ @options.merge!(parent.global_options)
480
+ opts.extend_help_from_parent(parent.opts)
481
+ end
482
+ end
483
+
484
+ def add_defaults_to_docs
485
+
486
+ # Remove any pre-existing separator text
487
+ opts.top.list.reject! {|v| v.is_a? String}
488
+
489
+ if @env_var && @rc_file
490
+ opts.separator ''
491
+ opts.separator 'Default values can be placed in:'
492
+ opts.separator ''
493
+ opts.separator " #{@env_var} environment variable, as a String of options"
494
+ opts.separator " #{@rc_file} with contents either a String of options "
495
+ spaces = (0..@rc_file.length).reduce('') { |a,_| a << ' ' }
496
+ opts.separator " #{spaces}or a YAML-encoded Hash"
497
+ elsif @env_var
498
+ opts.separator ''
499
+ opts.separator "Default values can be placed in the #{@env_var} environment variable"
500
+ elsif @rc_file
501
+ opts.separator ''
502
+ opts.separator "Default values can be placed in #{@rc_file}"
503
+ end
504
+ end
505
+
506
+ def set_defaults_from_env_var
507
+ if @env_var
508
+ parse_string_for_argv(ENV[@env_var]).each do |arg|
509
+ ::ARGV.unshift(arg)
510
+ end
511
+ end
512
+ end
513
+
514
+ def set_defaults_from_rc_file
515
+ # TODO: Specify defaults for each command
516
+ if @rc_file && File.exists?(@rc_file)
517
+ File.open(@rc_file) do |file|
518
+ parsed = YAML::load(file)
519
+ if parsed.kind_of? String
520
+ parse_string_for_argv(parsed).each do |arg|
521
+ ::ARGV.unshift(arg)
522
+ end
523
+ elsif parsed.kind_of? Hash
524
+ parsed.each do |option,value|
525
+ options[option] = value
526
+ end
527
+ else
528
+ raise OptionParser::ParseError,
529
+ "rc file #{@rc_file} is not parseable, should be a string or YAML-encoded Hash"
530
+ end
531
+ end
532
+ end
533
+ end
534
+
535
+
536
+ # Normalized all defaults to both string and symbol forms, so
537
+ # the user can access them via either means just as they would for
538
+ # non-defaulted options
539
+ def normalize_defaults
540
+ new_options = {}
541
+ options.each do |key,value|
542
+ unless value.nil?
543
+ new_options[key.to_s] = value
544
+ new_options[key.to_sym] = value
545
+ end
546
+ end
547
+ options.merge!(new_options)
548
+ end
549
+
550
+ # Handle calling main and trapping any exceptions thrown
551
+ def call_main
552
+ # Backwards compatibility ensured by adding ::ARGV
553
+ # TBD: rework spec so that unspecified args need to be retrieved from ARGV directly and not just passed into main
554
+ @main_block.call(*(opts.args_for_main))
555
+ rescue Methadone::Error => ex
556
+ raise ex if ENV['DEBUG']
557
+ logger.error ex.message unless no_message? ex
558
+ ex.exit_code
559
+ rescue OptionParser::ParseError
560
+ raise
561
+ rescue => ex
562
+ raise ex if ENV['DEBUG']
563
+ raise ex if @@leak_exceptions
564
+ logger.error ex.message unless no_message? ex
565
+ 70 # Linux sysexit code for internal software error
566
+ end
567
+
568
+ def no_message?(exception)
569
+ exception.message.nil? || exception.message.strip.empty?
570
+ end
571
+
572
+ def call_provider
573
+ command = opts.selected_command
574
+ opts.commands[command].send(:go!,self)
575
+ end
576
+ end
577
+
578
+ # <b>Methadone Internal - treat as private</b>
579
+ #
580
+ # A proxy to OptionParser that intercepts #on
581
+ # so that we can allow a simpler interface
582
+ class OptionParserProxy < Object
583
+ # Create the proxy
584
+ #
585
+ # +option_parser+:: An OptionParser instance
586
+ # +options+:: a hash that will store the options
587
+ # set via automatic setting. The caller should
588
+ # retain a reference to this
589
+ def initialize(option_parser,options)
590
+ @option_parser = option_parser
591
+ @options = options
592
+ @option_defs ||= {:local => [],:global => []}
593
+ @option_sigs = {}
594
+ @options_used = []
595
+ @usage_rules = {}
596
+ @commands = {}
597
+ @selected_command = nil
598
+ @user_specified_banner = false
599
+ @accept_options = false
600
+ @args = []
601
+ @arg_options = {}
602
+ @arg_filters = {}
603
+ @arg_documentation = {}
604
+ @args_by_name = {}
605
+ @description = nil
606
+ @version = nil
607
+ @banner_stale = true
608
+ document_help
609
+ end
610
+
611
+ def parent_opts=(parent_opts)
612
+ @parent_opts = parent_opts
613
+ end
614
+
615
+ def parent_opts
616
+ @parent_opts || nil
617
+ end
618
+
619
+ def global_options
620
+ global_option_defs = @option_defs.fetch(:global, nil)
621
+ return {} if global_option_defs.nil?
622
+
623
+ keys = global_option_defs.map {|opt_def|
624
+ [opt_def.long, opt_def.short].
625
+ flatten.
626
+ map {|flag| flag.sub(/^--?(\[no-\])?/,'')}.
627
+ map {|flag| [flag,flag.to_sym]}
628
+ }.flatten
629
+ global_hash = @options.select {|k,v| keys.include? k}
630
+ global_hash.is_a?(Array) ? Hash[global_hash] : global_hash # Stupid 1.8.7 => 1.9.3 API change of Hash#select
631
+ end
632
+
633
+ def check_args!
634
+ arg_allocation_map = @args.map {|arg_name| @arg_options[arg_name].include?(:required) ? 1 : 0}
635
+
636
+ arg_count = ::ARGV.length - arg_allocation_map.reduce(0,&:+)
637
+ if arg_count > 0
638
+ @args.each.with_index do |arg_name,i|
639
+ if (@arg_options[arg_name] & [:many,:any]).length > 0
640
+ arg_allocation_map[i] += arg_count
641
+ break
642
+ elsif @arg_options[arg_name].include? :optional
643
+ arg_allocation_map[i] += 1
644
+ arg_count -= 1
645
+ break if arg_count == 0
646
+ end
647
+ end
648
+ end
649
+
650
+ @args.zip(arg_allocation_map).each do |arg_name,arg_count|
651
+ if not (@arg_options[arg_name] & [:many,:any]).empty?
652
+ arg_value = ::ARGV.shift(arg_count)
653
+ else
654
+ arg_value = (arg_count == 1) ? ::ARGV.shift : nil
655
+ end
656
+
657
+ if @arg_options[arg_name].include? :required and arg_value.nil?
658
+ message = "'#{arg_name.to_s}' is required"
659
+ raise ::OptionParser::ParseError,message
660
+ elsif @arg_options[arg_name].include?(:many) and arg_value.empty?
661
+ message = "at least one '#{arg_name.to_s}' is required"
662
+ raise ::OptionParser::ParseError,message
663
+ end
664
+
665
+ unless arg_value.nil? or arg_value.empty? or @arg_filters[arg_name].empty?
666
+ match = false
667
+ msg = ''
668
+ @arg_filters[arg_name].each do |filter|
669
+ if not (@arg_options[arg_name] & [:many,:any]).empty?
670
+ if filter.respond_to? :include?
671
+ invalid_values = (filter | arg_value) - filter
672
+ elsif filter.is_a? ::Regexp
673
+ invalid_values = arg_value - arg_value.grep(filter)
674
+ end
675
+ if invalid_values.empty?
676
+ match = true
677
+ break
678
+ end
679
+ msg = "The following value(s) were invalid: '#{invalid_values.join(' ')}'"
680
+ else
681
+ if filter.respond_to? :include?
682
+ if filter.include? arg_value
683
+ match = true
684
+ break
685
+ end
686
+ elsif filter.is_a?(::Regexp)
687
+ if arg_value =~ filter
688
+ match = true
689
+ break
690
+ end
691
+ end
692
+ msg = "'#{arg_value}' is invalid"
693
+ end
694
+ end
695
+
696
+ raise ::OptionParser::ParseError, "#{arg_name}: #{msg}" unless match
697
+
698
+ end
699
+ @args_by_name[arg_name] = arg_value
700
+ end
701
+ end
702
+
703
+ def args_for_main
704
+ @args.map {|name| @args_by_name[name]}
705
+ end
706
+
707
+ # If invoked as with OptionParser, behaves the exact same way.
708
+ # If invoked without a block, however, the options hash given
709
+ # to the constructor will be used to store
710
+ # the parsed command-line value. See #opts in the Main module
711
+ # for how that works.
712
+ # Returns reference to the option for exclusive and mutual
713
+ def on(*args,&block)
714
+
715
+ # Group together any of the hash arguments
716
+ (hashes, args) = args.partition {|a| a.respond_to?(:keys)}
717
+ on_opts = hashes.reduce({}) {|h1,h2| h1.merge(h2)}
718
+
719
+ scope = args.delete(:global) || :local
720
+ args = add_default_value_to_docstring(*args)
721
+ sig = option_signature(args)
722
+ opt_names = option_names(*args)
723
+
724
+ opt_names.each do |name|
725
+ @option_sigs[name] = sig
726
+ end
727
+
728
+ block ||= Proc.new do |value|
729
+ opt_names.each do |name|
730
+ @options[name] = value
731
+ end
732
+ end
733
+ wrapper = Proc.new do |value|
734
+ register_usage opt_names
735
+ block.call(value)
736
+ end
737
+
738
+ opt = @option_parser.define(*args,&wrapper)
739
+ @option_defs[scope] << opt
740
+
741
+ set_usage_rules_for(opt_names,on_opts)
742
+
743
+ @accept_options = true
744
+ @banner_stale = true
745
+ end
746
+
747
+ def set_usage_rules_for(names,rules_source)
748
+ rule_keys = [:excludes, :requires]
749
+ rules = Hash[rule_keys.zip(rules_source.values_at(*rule_keys))].reject{|k,v| v.nil?}
750
+ return if rules.empty?
751
+
752
+ names.each do |name|
753
+ @usage_rules[name] = rules
754
+ end
755
+ end
756
+
757
+ def register_usage(opt_names)
758
+ opt_names.each do |name|
759
+ @options_used << name
760
+ end
761
+ end
762
+
763
+ def check_option_usage!
764
+ requirers = @options_used.select {|name| @usage_rules.fetch(name,{}).key?(:requires)}
765
+ requirers.each do |name|
766
+ required = [@usage_rules[name][:requires]].flatten
767
+ violation = required - @options_used
768
+ unless violation.empty?
769
+ raise OptionParser::OptionConflict.new("Missing option #{@option_sigs[violation.first]} required by option #{@option_sigs[name]}")
770
+ end
771
+ end
772
+ excluders = @options_used.select {|name| @usage_rules.fetch(name,{}).key?(:excludes)}
773
+ excluders.each do |name|
774
+ excluded = [@usage_rules[name][:excludes]].flatten
775
+ violation = (excluded & @options_used)
776
+ unless violation.empty?
777
+ raise OptionParser::OptionConflict, "#{@option_sigs[name]} cannot be used if already using #{@option_sigs[violation.first]}"
778
+ end
779
+ end
780
+ end
781
+
782
+ # Specify an acceptable command that will be hanlded by the given command provider
783
+ def command(provider_hash={})
784
+ provider_hash.each do |name,cls|
785
+ raise InvalidProvider.new("Provider for #{name} must respond to go!") unless cls.respond_to? :go!
786
+ commands[name.to_s] = cls
787
+ end
788
+ @banner_stale = true
789
+ end
790
+
791
+ # Proxies to underlying OptionParser
792
+ def banner=(new_banner)
793
+ @option_parser.banner=new_banner
794
+ @user_specified_banner = true
795
+ end
796
+
797
+
798
+ # Sets the banner to include these arg names
799
+ def arg(arg_name,*options)
800
+ options << :optional if options.include?(:any) && !options.include?(:optional)
801
+ options << :required unless options.include? :optional
802
+ options << :one unless options.include?(:any) || options.include?(:many)
803
+ @args << arg_name
804
+ @arg_options[arg_name] = options
805
+ @arg_documentation[arg_name]= options.select(&STRINGS_ONLY)
806
+ @arg_filters[arg_name] = options.select {|o| o.is_a?(Array) or o.is_a?(Range) or o.is_a?(::Regexp)}
807
+ @banner_stale = true
808
+ end
809
+
810
+ def description(desc)
811
+
812
+ @description = desc if desc
813
+ @banner_stale = true
814
+ @description
815
+ end
816
+
817
+ # Defers all calls save #on to
818
+ # the underlying OptionParser instance
819
+ def method_missing(sym,*args,&block)
820
+ @option_parser.send(sym,*args,&block)
821
+ end
822
+
823
+ def banner
824
+ set_banner if @banner_stale
825
+ @option_parser.banner
826
+ end
827
+
828
+ def help
829
+ set_banner if @banner_stale
830
+ @option_parser.to_s
831
+ end
832
+
833
+ # Since we extend Object on 1.8.x, to_s is defined and thus not proxied by method_missing
834
+ def to_s #::nodoc::
835
+ help
836
+ end
837
+
838
+
839
+ # Acess the command provider list
840
+ def commands
841
+ @commands
842
+ end
843
+
844
+ def parse_to_command!
845
+ @option_parser.order!
846
+ if command_names.include? ::ARGV[0]
847
+ @selected_command = ::ARGV.shift
848
+ end
849
+ end
850
+
851
+ # The selected command
852
+ def selected_command
853
+ @selected_command
854
+ end
855
+
856
+ # Sets the version for the banner
857
+ def version(version)
858
+ @version = version
859
+ @banner_stale = true
860
+ end
861
+
862
+ # List the command names
863
+ def command_names
864
+ @command_names ||= commands.keys.map {|k| k.to_s}
865
+ end
866
+
867
+ # We need some documentation to appear at the end, after all OptionParser setup
868
+ # has occured, but before we actually start. This method serves that purpose
869
+ def post_setup
870
+ if parent_opts and not (global_opts = parent_opts.global_options_help).empty?
871
+ @option_parser.separator ''
872
+ global_opts.split("\n").each {|line| @option_parser.separator line}
873
+ end
874
+
875
+ if @commands.empty? and ! @arg_documentation.empty?
876
+ @option_parser.separator ''
877
+ @option_parser.separator "Arguments:"
878
+ @args.each do |arg|
879
+ option_tag = @arg_options[arg].include?(:optional) ? ' (optional)' : ''
880
+ @option_parser.separator " #{arg}#{option_tag}"
881
+ @arg_documentation[arg].each do |doc|
882
+ @option_parser.separator " #{doc}"
883
+ end
884
+ end
885
+ end
886
+
887
+ unless @commands.empty?
888
+ padding = @commands.keys.map {|name| name.to_s.length}.max + 1
889
+ @option_parser.separator ''
890
+ @option_parser.separator "Commands:"
891
+ @commands.each do |name,provider|
892
+ @option_parser.separator " #{ "%-#{padding}s" % (name.to_s+':')} #{provider.description}"
893
+ end
894
+ end
895
+ @option_parser.separator ''
896
+ end
897
+
898
+ def extend_help_from_parent(parent_opts)
899
+ self.parent_opts = parent_opts
900
+ @banner_stale = true
901
+ end
902
+
903
+ protected
904
+
905
+ def base_usage_line
906
+ line = parent_opts.nil? ? "\nUsage:" : parent_opts.base_usage_line
907
+ cmd = parent_opts && parent_opts.selected_command
908
+ line += ' ' + (cmd || ::File.basename($0)).to_s
909
+ if selected_command && accept_global_options?
910
+ if parent_opts
911
+ line += " [options for #{cmd}]"
912
+ else
913
+ line += " [global options]"
914
+ end
915
+ end
916
+ line
917
+ end
918
+
919
+ def global_options_help
920
+ msg = []
921
+ global_option_defs = @option_defs.fetch(:global,[])
922
+ unless global_option_defs.empty?
923
+ cmd = parent_opts && parent_opts.selected_command
924
+ opt_lines = [cmd.nil? ? "Global options:\n" : "Options for #{cmd}:\n"]
925
+ width = @option_parser.summary_width
926
+ indent = @option_parser.summary_indent
927
+ global_option_defs.each do |opt|
928
+ opt.summarize({},{},width,width - 1,indent) do |line|
929
+ opt_lines << (line.index($/, -1) ? line : line + $/)
930
+ end
931
+ end
932
+ msg << opt_lines.join('')
933
+ end
934
+ msg << parent_opts.global_options_help if parent_opts
935
+ msg.join ("\n")
936
+ end
937
+
938
+ def accept_global_options?
939
+ ! @option_defs.fetch(:global,[]).empty?
940
+ end
941
+
942
+ private
943
+
944
+ # Because there is always an option for -h, if there are subcommands, they
945
+ # need to show the option holder and Options prefix to differentiate
946
+ # between the command option an previous options.
947
+ def accept_options?
948
+ @accept_options
949
+ end
950
+
951
+ def document_help
952
+ @option_parser.on("-h","--help","Show command line help") do
953
+ puts self.to_s
954
+ exit 0
955
+ end
956
+ @banner_stale = true
957
+ end
958
+
959
+ def add_default_value_to_docstring(*args)
960
+ default_value = nil
961
+ option_names_from(args).each do |option|
962
+ option = option.sub(/\A\[no-\]/,'')
963
+ default_value = (@options[option.to_s] || @options[option.to_sym]) if default_value.nil?
964
+ end
965
+ if default_value.nil?
966
+ args
967
+ else
968
+ args + ["(default: #{default_value})"]
969
+ end
970
+ end
971
+
972
+ def option_names_from(args)
973
+ args.select(&STRINGS_ONLY).select { |_|
974
+ _ =~ /^\-/
975
+ }.map { |_|
976
+ _.gsub(/^\-+/,'').gsub(/\s.*$/,'')
977
+ }
978
+ end
979
+
980
+ def option_signature(args)
981
+ args.select(&STRINGS_ONLY).select {|s| s =~ /\A-/}.join('|')
982
+ end
983
+
984
+
985
+ def set_banner
986
+ return if @user_specified_banner
987
+ return unless @banner_stale
988
+
989
+ new_banner = base_usage_line
990
+ new_banner += " [options]" if (@commands.empty? or parent_opts.nil?) and accept_options?
991
+ new_banner += " command [command options and args...]" unless @commands.empty?
992
+
993
+ if @commands.empty? and !@args.empty?
994
+ new_banner += " "
995
+ new_banner += @args.map { |arg|
996
+ if @arg_options[arg].include? :any
997
+ "[#{arg.to_s}...]"
998
+ elsif @arg_options[arg].include? :optional
999
+ "[#{arg.to_s}]"
1000
+ elsif @arg_options[arg].include? :many
1001
+ "#{arg.to_s}..."
1002
+ else
1003
+ arg.to_s
1004
+ end
1005
+ }.join(' ')
1006
+ end
1007
+
1008
+ new_banner += "\n\n#{@description}" if @description
1009
+ new_banner += "\n\nv#{@version}" if @version
1010
+
1011
+ new_banner += "\n\nOptions:"
1012
+ @option_parser.banner=new_banner
1013
+ @banner_stale = false
1014
+ end
1015
+
1016
+ def option_names(*opts_on_args,&block)
1017
+ opts_on_args.select(&STRINGS_ONLY).map { |arg|
1018
+ if arg =~ /^--\[no-\]([^-\s][^\s]*)/
1019
+ $1.to_sym
1020
+ elsif arg =~ /^--([^-\s][^\s]*)/
1021
+ $1.to_sym
1022
+ elsif arg =~ /^-([^-\s][^\s]*)/
1023
+ $1.to_sym
1024
+ else
1025
+ nil
1026
+ end
1027
+ }.reject(&:nil?).map {|name| [name,name.to_s]}.flatten
1028
+ end
1029
+
1030
+ STRINGS_ONLY = lambda { |o| o.kind_of?(::String) }
1031
+
1032
+ end
1033
+
1034
+ InvalidProvider = Class.new(TypeError)
1035
+
1036
+ OptionParser::OptionConflict = Class.new(OptionParser::ParseError)
1037
+ OptionParser::MissingRequiredOption = Class.new(OptionParser::ParseError)
1038
+
1039
+ end