methadone-rehab 1.9.2

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 (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