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.
- data/lib/methadone/cli.rb +2 -0
- data/lib/methadone/cli_logging.rb +3 -0
- data/lib/methadone/cucumber.rb +1 -1
- data/lib/methadone/error.rb +8 -1
- data/lib/methadone/execution_strategy/jvm.rb +2 -0
- data/lib/methadone/execution_strategy/mri.rb +2 -0
- data/lib/methadone/execution_strategy/open_3.rb +2 -0
- data/lib/methadone/execution_strategy/open_4.rb +2 -0
- data/lib/methadone/execution_strategy/rbx_open_4.rb +2 -0
- data/lib/methadone/exit_now.rb +18 -3
- data/lib/methadone/main.rb +34 -6
- data/lib/methadone/process_status.rb +45 -0
- data/lib/methadone/sh.rb +52 -29
- data/lib/methadone/version.rb +1 -1
- data/methadone.gemspec +10 -0
- data/test/test_main.rb +20 -0
- data/test/test_sh.rb +104 -3
- metadata +23 -47
- data/tutorial/.vimrc +0 -6
- data/tutorial/1_intro.md +0 -52
- data/tutorial/2_bootstrap.md +0 -174
- data/tutorial/3_ui.md +0 -336
- data/tutorial/4_happy_path.md +0 -405
- data/tutorial/5_more_features.md +0 -693
- data/tutorial/6_refactor.md +0 -220
- data/tutorial/7_logging_debugging.md +0 -274
- data/tutorial/8_conclusion.md +0 -11
- data/tutorial/code/.rvmrc +0 -1
- data/tutorial/code/fullstop/.gitignore +0 -5
- data/tutorial/code/fullstop/Gemfile +0 -4
- data/tutorial/code/fullstop/LICENSE.txt +0 -202
- data/tutorial/code/fullstop/README.rdoc +0 -23
- data/tutorial/code/fullstop/Rakefile +0 -31
- data/tutorial/code/fullstop/bin/fullstop +0 -43
- data/tutorial/code/fullstop/features/fullstop.feature +0 -40
- data/tutorial/code/fullstop/features/step_definitions/fullstop_steps.rb +0 -64
- data/tutorial/code/fullstop/features/support/env.rb +0 -22
- data/tutorial/code/fullstop/fullstop.gemspec +0 -28
- data/tutorial/code/fullstop/lib/fullstop.rb +0 -2
- data/tutorial/code/fullstop/lib/fullstop/repo.rb +0 -38
- data/tutorial/code/fullstop/lib/fullstop/version.rb +0 -3
- data/tutorial/code/fullstop/test/tc_something.rb +0 -7
- data/tutorial/en.utf-8.add +0 -18
- data/tutorial/toc.md +0 -27
data/lib/methadone/cli.rb
CHANGED
@@ -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)
|
data/lib/methadone/cucumber.rb
CHANGED
@@ -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
|
data/lib/methadone/error.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
module Methadone
|
2
2
|
# Standard exception you can throw to exit with a given
|
3
|
-
# status code.
|
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)
|
data/lib/methadone/exit_now.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
#
|
17
|
+
# === Examples
|
15
18
|
#
|
16
|
-
#
|
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
|
data/lib/methadone/main.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
-
#
|
33
|
-
#
|
34
|
-
#
|
39
|
+
# sh 'cp foo.txt /tmp' do
|
40
|
+
# # Behaves exactly as before, but this block is called after
|
41
|
+
# end
|
35
42
|
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
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
|
-
#
|
41
|
-
#
|
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
|
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
|
-
#
|
70
|
-
#
|
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
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
-
#
|
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
|
data/lib/methadone/version.rb
CHANGED