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