methadone 1.0.0.rc5 → 1.0.0.rc6

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