methadone 1.0.0.rc5 → 1.0.0.rc6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/lib/methadone/cli.rb +2 -0
  2. data/lib/methadone/cli_logging.rb +3 -0
  3. data/lib/methadone/cucumber.rb +1 -1
  4. data/lib/methadone/error.rb +8 -1
  5. data/lib/methadone/execution_strategy/jvm.rb +2 -0
  6. data/lib/methadone/execution_strategy/mri.rb +2 -0
  7. data/lib/methadone/execution_strategy/open_3.rb +2 -0
  8. data/lib/methadone/execution_strategy/open_4.rb +2 -0
  9. data/lib/methadone/execution_strategy/rbx_open_4.rb +2 -0
  10. data/lib/methadone/exit_now.rb +18 -3
  11. data/lib/methadone/main.rb +34 -6
  12. data/lib/methadone/process_status.rb +45 -0
  13. data/lib/methadone/sh.rb +52 -29
  14. data/lib/methadone/version.rb +1 -1
  15. data/methadone.gemspec +10 -0
  16. data/test/test_main.rb +20 -0
  17. data/test/test_sh.rb +104 -3
  18. metadata +23 -47
  19. data/tutorial/.vimrc +0 -6
  20. data/tutorial/1_intro.md +0 -52
  21. data/tutorial/2_bootstrap.md +0 -174
  22. data/tutorial/3_ui.md +0 -336
  23. data/tutorial/4_happy_path.md +0 -405
  24. data/tutorial/5_more_features.md +0 -693
  25. data/tutorial/6_refactor.md +0 -220
  26. data/tutorial/7_logging_debugging.md +0 -274
  27. data/tutorial/8_conclusion.md +0 -11
  28. data/tutorial/code/.rvmrc +0 -1
  29. data/tutorial/code/fullstop/.gitignore +0 -5
  30. data/tutorial/code/fullstop/Gemfile +0 -4
  31. data/tutorial/code/fullstop/LICENSE.txt +0 -202
  32. data/tutorial/code/fullstop/README.rdoc +0 -23
  33. data/tutorial/code/fullstop/Rakefile +0 -31
  34. data/tutorial/code/fullstop/bin/fullstop +0 -43
  35. data/tutorial/code/fullstop/features/fullstop.feature +0 -40
  36. data/tutorial/code/fullstop/features/step_definitions/fullstop_steps.rb +0 -64
  37. data/tutorial/code/fullstop/features/support/env.rb +0 -22
  38. data/tutorial/code/fullstop/fullstop.gemspec +0 -28
  39. data/tutorial/code/fullstop/lib/fullstop.rb +0 -2
  40. data/tutorial/code/fullstop/lib/fullstop/repo.rb +0 -38
  41. data/tutorial/code/fullstop/lib/fullstop/version.rb +0 -3
  42. data/tutorial/code/fullstop/test/tc_something.rb +0 -7
  43. data/tutorial/en.utf-8.add +0 -18
  44. data/tutorial/toc.md +0 -27
data/lib/methadone/cli.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require 'erb'
2
2
 
3
3
  module Methadone
4
+ # <b>Methadone Internal - treat as private</b>
5
+ #
4
6
  # Stuff to implement methadone's CLI app. These
5
7
  # stuff isn't generally for your use and it's not
6
8
  # included when you require 'methadone'
@@ -22,6 +22,9 @@ module Methadone
22
22
  # debug("Done doing it")
23
23
  # end
24
24
  # end
25
+ #
26
+ # Note that every class that mixes this in shares the *same logger instance*, so if you call #change_logger, this
27
+ # will change the logger for all classes that mix this in. This is likely what you want.
25
28
  module CLILogging
26
29
 
27
30
  def self.included(k)
@@ -61,7 +61,7 @@ Then /^the following options should be documented:$/ do |options|
61
61
  end
62
62
 
63
63
  Then /^the option "([^"]*)" should be documented$/ do |option|
64
- step %(the output should match /\\s*#{option}[\\s\\W]+\\w\\w\\w+/)
64
+ step %(the output should match /\\s*#{Regexp.escape(option)}[\\s\\W]+\\w\\w\\w+/)
65
65
  end
66
66
 
67
67
  Then /^the banner should be present$/ do
@@ -1,6 +1,8 @@
1
1
  module Methadone
2
2
  # Standard exception you can throw to exit with a given
3
- # status code. Prefer Methadone::Main#exit_now! over this
3
+ # status code. Generally, you should prefer Methadone::Main#exit_now! over using
4
+ # this directly, however you may wish to create a rich hierarchy of exceptions that extend from
5
+ # this in your app, so this is provided if you wish to do so.
4
6
  class Error < StandardError
5
7
  attr_reader :exit_code
6
8
  # Create an Error with the given status code and message
@@ -16,6 +18,11 @@ module Methadone
16
18
  # The command that caused the failure
17
19
  attr_reader :command
18
20
 
21
+ # exit_code:: exit code of the command that caused this
22
+ # command:: the entire command-line that caused this
23
+ # custom_error_message:: an error message to show the user instead of the boilerplate one. Useful
24
+ # for allowing this exception to bubble up and exit the program, but to give
25
+ # the user something actionable.
19
26
  def initialize(exit_code,command,custom_error_message = nil)
20
27
  error_message = String(custom_error_message).empty? ? "Command '#{command}' exited #{exit_code}" : custom_error_message
21
28
  super(exit_code,error_message)
@@ -1,5 +1,7 @@
1
1
  module Methadone
2
2
  module ExecutionStrategy
3
+ # <b>Methadone Internal - treat as private</b>
4
+ #
3
5
  # Methadone::ExecutionStrategy for the JVM that uses JVM classes to run the command and get its results.
4
6
  class JVM < Base
5
7
  def run_command(command)
@@ -1,5 +1,7 @@
1
1
  module Methadone
2
2
  module ExecutionStrategy
3
+ # <b>Methadone Internal - treat as private</b>
4
+ #
3
5
  # Base strategy for MRI rubies.
4
6
  class MRI < Base
5
7
  def run_command(command)
@@ -1,5 +1,7 @@
1
1
  module Methadone
2
2
  module ExecutionStrategy
3
+ # <b>Methadone Internal - treat as private</b>
4
+ #
3
5
  # Implementation for modern Rubies that uses the built-in Open3 library
4
6
  class Open_3 < MRI
5
7
  def run_command(command)
@@ -1,5 +1,7 @@
1
1
  module Methadone
2
2
  module ExecutionStrategy
3
+ # <b>Methadone Internal - treat as private</b>
4
+ #
3
5
  # ExecutionStrategy for non-modern Rubies that must rely on
4
6
  # Open4 to get access to the standard output AND error.
5
7
  class Open_4 < MRI
@@ -1,5 +1,7 @@
1
1
  module Methadone
2
2
  module ExecutionStrategy
3
+ # <b>Methadone Internal - treat as private</b>
4
+ #
3
5
  # For RBX; it throws a different exception when a command isn't found, so we override that here.
4
6
  class RBXOpen_4 < Open_4
5
7
  def exception_meaning_command_not_found
@@ -1,4 +1,6 @@
1
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.
2
4
  module ExitNow
3
5
  def self.included(k)
4
6
  k.extend(self)
@@ -9,11 +11,17 @@ module Methadone
9
11
  # +exit_code+:: exit status you'd like to exit with
10
12
  # +message+:: message to display to the user explaining the problem
11
13
  #
12
- # Also can be used without an exit code like so:
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.
13
16
  #
14
- # exit_now!("Oh noes!")
17
+ # === Examples
15
18
  #
16
- # In this case, it's equivalent to <code>exit_now!(1,"Oh noes!")</code>.
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)
17
25
  def exit_now!(exit_code,message=nil)
18
26
  if exit_code.kind_of?(String) && message.nil?
19
27
  raise Methadone::Error.new(1,exit_code)
@@ -21,5 +29,12 @@ module Methadone
21
29
  raise Methadone::Error.new(exit_code,message)
22
30
  end
23
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
24
39
  end
25
40
  end
@@ -105,6 +105,10 @@ module Methadone
105
105
  #
106
106
  # The block can accept any parameters, and unparsed arguments
107
107
  # from the command line will be passed.
108
+ #
109
+ # *Note*: #go! will modify +ARGV+ so any unparsed arguments that you do *not* declare as arguments
110
+ # to #main will essentially be unavailable. I consider this a bug, and it should be changed/fixed in
111
+ # a future version.
108
112
  #
109
113
  # To run this method, call #go!
110
114
  def main(&block)
@@ -116,7 +120,7 @@ module Methadone
116
120
  # Configure the auto-handling of StandardError exceptions caught
117
121
  # from calling go!.
118
122
  #
119
- # leak - if true, go! will *not* catch StandardError exceptions, but instead
123
+ # leak:: if true, go! will *not* catch StandardError exceptions, but instead
120
124
  # allow them to bubble up. If false, they will be caught and handled as normal.
121
125
  # This does *not* affect Methadone::Error exceptions; those will NOT leak through.
122
126
  def leak_exceptions(leak)
@@ -187,16 +191,33 @@ module Methadone
187
191
  #
188
192
  # Since, most of the time, this is all you want to do,
189
193
  # this makes it more expedient to do so. The key that is
190
- # is set in #options will be a symbol of the option name, without
191
- # the dashes. Note that if you use multiple option names, a key
194
+ # is set in #options will be a symbol <i>and string</i> of the option name, without
195
+ # the leading dashes. Note that if you use multiple option names, a key
192
196
  # will be generated for each. Further, if you use the negatable form,
193
197
  # only the positive key will be set, e.g. for <tt>--[no-]verbose</tt>,
194
198
  # only <tt>:verbose</tt> will be set (to true or false).
199
+ #
200
+ # As an example, this declaration:
201
+ #
202
+ # opts.on("-f VALUE", "--flag")
203
+ #
204
+ # And this command-line invocation:
205
+ #
206
+ # $ my_app -f foo
207
+ #
208
+ # Will result in all of these forms returning the String "foo":
209
+ # * <tt>options['f']</tt>
210
+ # * <tt>options[:f]</tt>
211
+ # * <tt>options['flag']</tt>
212
+ # * <tt>options[:flag]</tt>
213
+ #
214
+ # Further, any one of those keys can be used to determine the default value for the option.
195
215
  def opts
196
216
  @option_parser
197
217
  end
198
218
 
199
- # Calls <tt>opts.on</tt> with the given arguments
219
+ # Calls the +on+ method of #opts with the given arguments (see RDoc for #opts for the additional
220
+ # help provided).
200
221
  def on(*args,&block)
201
222
  opts.on(*args,&block)
202
223
  end
@@ -231,6 +252,9 @@ module Methadone
231
252
  # parsed from the command line. When you put values in here, if you do so
232
253
  # *before* you've declared your command-line interface via #on, the value
233
254
  # will be used in the docstring to indicate it is the default.
255
+ # You can use either a String or a Symbol and, after #go! is called and
256
+ # the command-line is parsed, the values will be available as both
257
+ # a String and a Symbol.
234
258
  #
235
259
  # Example
236
260
  #
@@ -253,10 +277,10 @@ module Methadone
253
277
  # banner. This also adds --version as an option to your app which,
254
278
  # when used, will act just like --help
255
279
  #
256
- # version - the current version of your app. Should almost always be
280
+ # version:: the current version of your app. Should almost always be
257
281
  # YourApp::VERSION, where the module YourApp should've been generated
258
282
  # by the bootstrap script
259
- # custom_message - if provided, customized the message shown next to --version
283
+ # custom_message:: if provided, customized the message shown next to --version
260
284
  def version(version,custom_message='Show help/version info')
261
285
  opts.version(version)
262
286
  opts.on("--version",custom_message) do
@@ -341,6 +365,8 @@ module Methadone
341
365
  raise ex if ENV['DEBUG']
342
366
  logger.error ex.message unless no_message? ex
343
367
  ex.exit_code
368
+ rescue OptionParser::ParseError
369
+ raise
344
370
  rescue => ex
345
371
  raise ex if ENV['DEBUG']
346
372
  raise ex if @leak_exceptions
@@ -353,6 +379,8 @@ module Methadone
353
379
  end
354
380
  end
355
381
 
382
+ # <b>Methadone Internal - treat as private</b>
383
+ #
356
384
  # A proxy to OptionParser that intercepts #on
357
385
  # so that we can allow a simpler interface
358
386
  class OptionParserProxy < BasicObject
@@ -0,0 +1,45 @@
1
+ module Methadone
2
+ # <b>Methadone Internal - treat as private</b>
3
+ #
4
+ # A wrapper/enhancement of Process::Status that handles coersion and expected
5
+ # nonzero statuses
6
+ class ProcessStatus
7
+
8
+ # The exit status, either directly from a Process::Status or derived from a non-Int value.
9
+ attr_reader :exitstatus
10
+
11
+ # Create the ProcessStatus with the given status.
12
+ #
13
+ # status:: if this responds to #exitstatus, that method is used to extract the exit code. If it's
14
+ # and Int, that is used as the exit code. Otherwise,
15
+ # it's truthiness is used: 0 for truthy, 1 for falsey.
16
+ # expected:: an Int or Array of Int representing the expected exit status, other than zero,
17
+ # that represent "success".
18
+ def initialize(status,expected)
19
+ @exitstatus = derive_exitstatus(status)
20
+ @success = ([0] + Array(expected)).include?(@exitstatus)
21
+ end
22
+
23
+ # True if the exit status was a successul (i.e. expected) one.
24
+ def success?
25
+ @success
26
+ end
27
+
28
+ private
29
+
30
+ def derive_exitstatus(status)
31
+ status = if status.respond_to? :exitstatus
32
+ status.exitstatus
33
+ else
34
+ status
35
+ end
36
+ if status.kind_of? Fixnum
37
+ status
38
+ elsif status
39
+ 0
40
+ else
41
+ 1
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/methadone/sh.rb CHANGED
@@ -2,11 +2,18 @@ if RUBY_PLATFORM == 'java'
2
2
  require 'java'
3
3
  require 'ostruct'
4
4
  elsif RUBY_VERSION =~ /^1.8/
5
+ begin
5
6
  require 'open4'
7
+ rescue LoadError
8
+ STDERR.puts "!! For Ruby #{RUBY_VERSION}, the open4 library must be installed"
9
+ raise
10
+ end
6
11
  else
7
12
  require 'open3'
8
13
  end
9
14
 
15
+ require 'methadone/process_status'
16
+
10
17
  module Methadone
11
18
  # Module with various helper methods for executing external commands.
12
19
  # In most cases, you can use #sh to run commands and have decent logging
@@ -29,23 +36,23 @@ module Methadone
29
36
  # sh! 'cp non_existent_file.txt /nowhere_good'
30
37
  # # => same as above, EXCEPT, raises a Methadone::FailedCommandError
31
38
  #
32
- # sh 'cp foo.txt /tmp' do
33
- # # Behaves exactly as before, but this block is called after
34
- # end
39
+ # sh 'cp foo.txt /tmp' do
40
+ # # Behaves exactly as before, but this block is called after
41
+ # end
35
42
  #
36
- # sh 'cp non_existent_file.txt /nowhere_good' do
37
- # # This block isn't called, since the command failed
38
- # end
43
+ # sh 'cp non_existent_file.txt /nowhere_good' do
44
+ # # This block isn't called, since the command failed
45
+ # end
39
46
  #
40
- # sh 'ls -l /tmp/' do |stdout|
41
- # # stdout contains the output of the command
47
+ # sh 'ls -l /tmp/' do |stdout|
48
+ # # stdout contains the output of the command
49
+ # end
50
+ # sh 'ls -l /tmp/ /non_existent_dir' do |stdout,stderr|
51
+ # # stdout contains the output of the command,
52
+ # # stderr contains the standard error output.
42
53
  # end
43
- # sh 'ls -l /tmp/ /non_existent_dir' do |stdout,stderr|
44
- # # stdout contains the output of the command,
45
- # # stderr contains the standard error output.
46
- # end
47
54
  #
48
- # == Handling remote execution
55
+ # == Handling process execution
49
56
  #
50
57
  # In order to work on as many Rubies as possible, this class defers the actual execution
51
58
  # to an execution strategy. See #set_execution_strategy if you think you'd like to override
@@ -66,10 +73,15 @@ module Methadone
66
73
  # error output is logged at WARN.
67
74
  #
68
75
  # command:: the command to run
69
- # block:: if provided, will be called if the command exited nonzero. The block may take 0, 1, or 2 arguments.
70
- # The arguments provided are the standard output as a string and the standard error as a string,
76
+ # options:: options to control the call. Currently responds to:
77
+ # +:expected+:: an Int or Array of Int representing error codes, <b>in addition to 0</b>, that are
78
+ # expected and therefore constitute success. Useful for commands that don't use
79
+ # exit codes the way you'd like
80
+ # block:: if provided, will be called if the command exited nonzero. The block may take 0, 1, 2, or 3 arguments.
81
+ # The arguments provided are the standard output as a string, standard error as a string, and
82
+ # the exitstatus as an Int.
71
83
  # You should be safe to pass in a lambda instead of a block, as long as your
72
- # lambda doesn't take more than two arguments
84
+ # lambda doesn't take more than three arguments
73
85
  #
74
86
  # Example
75
87
  #
@@ -82,22 +94,23 @@ module Methadone
82
94
  # end
83
95
  #
84
96
  # Returns the exit status of the command. Note that if the command doesn't exist, this returns 127.
85
- def sh(command,&block)
97
+ def sh(command,options={},&block)
86
98
  sh_logger.debug("Executing '#{command}'")
87
99
 
88
100
  stdout,stderr,status = execution_strategy.run_command(command)
101
+ process_status = Methadone::ProcessStatus.new(status,options[:expected])
89
102
 
90
103
  sh_logger.warn("Error output of '#{command}': #{stderr}") unless stderr.strip.length == 0
91
104
 
92
- if status.exitstatus != 0
105
+ if process_status.success?
106
+ sh_logger.debug("Output of '#{command}': #{stdout}") unless stdout.strip.length == 0
107
+ call_block(block,stdout,stderr,process_status.exitstatus) unless block.nil?
108
+ else
93
109
  sh_logger.info("Output of '#{command}': #{stdout}") unless stdout.strip.length == 0
94
110
  sh_logger.warn("Error running '#{command}'")
95
- else
96
- sh_logger.debug("Output of '#{command}': #{stdout}") unless stdout.strip.length == 0
97
- call_block(block,stdout,stderr) unless block.nil?
98
111
  end
99
112
 
100
- status.exitstatus
113
+ process_status.exitstatus
101
114
  rescue exception_meaning_command_not_found => ex
102
115
  sh_logger.error("Error running '#{command}': #{ex.message}")
103
116
  127
@@ -106,7 +119,8 @@ module Methadone
106
119
  # Run a command, throwing an exception if the command exited nonzero.
107
120
  # Otherwise, behaves exactly like #sh.
108
121
  #
109
- # options - options hash, responding to:
122
+ # options:: options hash, responding to:
123
+ # <tt>:expected</tt>:: same as for #sh
110
124
  # <tt>:on_fail</tt>:: a custom error message. This allows you to have your
111
125
  # app exit on shell command failures, but customize the error
112
126
  # message that they see.
@@ -120,8 +134,11 @@ module Methadone
120
134
  # sh!("rsync foo bar", :on_fail => "Couldn't rsync, check log for details")
121
135
  # # => if command fails, app exits and user sees: "error: Couldn't rsync, check log for details
122
136
  def sh!(command,options={},&block)
123
- sh(command,&block).tap do |exitstatus|
124
- raise Methadone::FailedCommandError.new(exitstatus,command,options[:on_fail]) if exitstatus != 0
137
+ sh(command,options,&block).tap do |exitstatus|
138
+ process_status = Methadone::ProcessStatus.new(exitstatus,options[:expected])
139
+ unless process_status.success?
140
+ raise Methadone::FailedCommandError.new(exitstatus,command,options[:on_fail])
141
+ end
125
142
  end
126
143
  end
127
144
 
@@ -138,8 +155,12 @@ module Methadone
138
155
  # Set the strategy to use for executing commands. In general, you don't need to set this
139
156
  # since this module chooses an appropriate implementation based on your Ruby platform:
140
157
  #
141
- # 1.8 Rubies, including 1.8, and REE:: Open4 is used via Methadone::ExecutionStrategy::Open_4
142
- # Rubinius:: Open4 is used, but we handle things a bit differently; see Methadone::ExecutionStrategy::RBXOpen_4
158
+ # 1.8 Rubies, including 1.8, and REE:: Open4 is used via Methadone::ExecutionStrategy::Open_4. <b><tt>open4</tt> will not be
159
+ # installed as a dependency</b>. RubyGems doesn't allow conditional dependencies,
160
+ # so make sure that your app declares it as a dependency if you think you'll be
161
+ # running on 1.8 or REE.
162
+ # Rubinius:: Open4 is used, but we handle things a bit differently; see Methadone::ExecutionStrategy::RBXOpen_4.
163
+ # Same warning on dependencies applies.
143
164
  # JRuby:: Use JVM calls to +Runtime+ via Methadone::ExecutionStrategy::JVM
144
165
  # Windows:: Currently no support for Windows
145
166
  # All others:: we use Open3 from the standard library, via Methadone::ExecutionStrategy::Open_3
@@ -176,15 +197,17 @@ module Methadone
176
197
  end
177
198
 
178
199
  # Safely call our block, even if the user passed in a lambda
179
- def call_block(block,stdout,stderr)
200
+ def call_block(block,stdout,stderr,exitstatus)
180
201
  # blocks that take no arguments have arity -1. Or 0. Ugh.
181
202
  if block.arity > 0
182
203
  case block.arity
183
204
  when 1
184
205
  block.call(stdout)
206
+ when 2
207
+ block.call(stdout,stderr)
185
208
  else
186
209
  # Let it fail for lambdas
187
- block.call(stdout,stderr)
210
+ block.call(stdout,stderr,exitstatus)
188
211
  end
189
212
  else
190
213
  block.call
@@ -1,3 +1,3 @@
1
1
  module Methadone
2
- VERSION = "1.0.0.rc5"
2
+ VERSION = "1.0.0.rc6"
3
3
  end