methadone 1.0.0.rc2 → 1.0.0.rc3
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/bin/methadone +2 -2
- data/lib/methadone/cucumber.rb +6 -3
- data/lib/methadone/error.rb +3 -2
- data/lib/methadone/exit_now.rb +25 -0
- data/lib/methadone/main.rb +21 -10
- data/lib/methadone/sh.rb +19 -4
- data/lib/methadone/version.rb +1 -1
- data/lib/methadone.rb +1 -0
- data/templates/full/Rakefile.erb +1 -1
- data/templates/full/features/step_definitions/executable_steps.rb.erb +1 -1
- data/test/test_exit_now.rb +37 -0
- data/test/test_main.rb +11 -5
- data/test/test_sh.rb +17 -0
- data/tutorial/.vimrc +6 -0
- data/tutorial/1_intro.md +52 -0
- data/tutorial/2_bootstrap.md +176 -0
- data/tutorial/3_ui.md +349 -0
- data/tutorial/4_happy_path.md +437 -0
- data/tutorial/5_more_features.md +702 -0
- data/tutorial/6_refactor.md +220 -0
- data/tutorial/7_logging_debugging.md +304 -0
- data/tutorial/8_conclusion.md +11 -0
- data/tutorial/code/.rvmrc +1 -0
- data/tutorial/code/fullstop/.gitignore +5 -0
- data/tutorial/code/fullstop/Gemfile +4 -0
- data/tutorial/code/fullstop/LICENSE.txt +202 -0
- data/tutorial/code/fullstop/README.rdoc +23 -0
- data/tutorial/code/fullstop/Rakefile +31 -0
- data/tutorial/code/fullstop/bin/fullstop +43 -0
- data/tutorial/code/fullstop/features/fullstop.feature +40 -0
- data/tutorial/code/fullstop/features/step_definitions/fullstop_steps.rb +64 -0
- data/tutorial/code/fullstop/features/support/env.rb +22 -0
- data/tutorial/code/fullstop/fullstop.gemspec +28 -0
- data/tutorial/code/fullstop/lib/fullstop/repo.rb +38 -0
- data/tutorial/code/fullstop/lib/fullstop/version.rb +3 -0
- data/tutorial/code/fullstop/lib/fullstop.rb +2 -0
- data/tutorial/code/fullstop/test/tc_something.rb +7 -0
- data/tutorial/en.utf-8.add +18 -0
- data/tutorial/toc.md +27 -0
- metadata +49 -20
data/bin/methadone
CHANGED
@@ -48,7 +48,7 @@ main do |app_name|
|
|
48
48
|
" #{gem_variable}.add_development_dependency('rdoc')",
|
49
49
|
" #{gem_variable}.add_development_dependency('aruba')",
|
50
50
|
" #{gem_variable}.add_development_dependency('rake','~> 0.9.2')",
|
51
|
-
" #{gem_variable}.add_dependency('methadone')",
|
51
|
+
" #{gem_variable}.add_dependency('methadone', '~>1.0.0.rc3')",
|
52
52
|
], :before => /^end\s*$/
|
53
53
|
end
|
54
54
|
|
@@ -64,7 +64,7 @@ on("-l LICENSE","--license",licenses,"Specify the license for your project (#{li
|
|
64
64
|
|
65
65
|
use_log_level_option
|
66
66
|
|
67
|
-
arg :app_name, :required
|
67
|
+
arg :app_name, :required, "Name of your app, which is used for the gem name and executable name"
|
68
68
|
|
69
69
|
version Methadone::VERSION
|
70
70
|
|
data/lib/methadone/cucumber.rb
CHANGED
@@ -34,7 +34,9 @@ module Methadone
|
|
34
34
|
#
|
35
35
|
# * Checks that the app's usage banner documents that its arguments are <tt>args</tt>
|
36
36
|
#
|
37
|
-
# Then the banner should document that this app's arguments are
|
37
|
+
# Then the banner should document that this app's arguments are
|
38
|
+
# |foo|which is optional|
|
39
|
+
# |bar|which is required|
|
38
40
|
#
|
39
41
|
# * Do the opposite; check that your app doesn't take any arguments
|
40
42
|
#
|
@@ -42,7 +44,7 @@ module Methadone
|
|
42
44
|
#
|
43
45
|
# * Check for a usage description which occurs after the banner and a blank line
|
44
46
|
#
|
45
|
-
# Then there should be a one
|
47
|
+
# Then there should be a one line summary of what the app does
|
46
48
|
#
|
47
49
|
module Cucumber
|
48
50
|
end
|
@@ -59,7 +61,7 @@ Then /^the following options should be documented:$/ do |options|
|
|
59
61
|
end
|
60
62
|
|
61
63
|
Then /^the option "([^"]*)" should be documented$/ do |option|
|
62
|
-
step %(the output should match
|
64
|
+
step %(the output should match /\\s*#{option}[\\s\\W]+\\w\\w\\w+/)
|
63
65
|
end
|
64
66
|
|
65
67
|
Then /^the banner should be present$/ do
|
@@ -75,6 +77,7 @@ Then /^the banner should document that this app's arguments are:$/ do |table|
|
|
75
77
|
expected_arguments = table.raw.map { |row|
|
76
78
|
option = row[0]
|
77
79
|
option = "[#{option}]" if row[1] == 'optional' || row[1] == 'which is optional'
|
80
|
+
option
|
78
81
|
}.join(' ')
|
79
82
|
step %(the output should contain "#{expected_arguments}")
|
80
83
|
end
|
data/lib/methadone/error.rb
CHANGED
@@ -16,8 +16,9 @@ module Methadone
|
|
16
16
|
# The command that caused the failure
|
17
17
|
attr_reader :command
|
18
18
|
|
19
|
-
def initialize(exit_code,command)
|
20
|
-
|
19
|
+
def initialize(exit_code,command,custom_error_message = nil)
|
20
|
+
error_message = String(custom_error_message).empty? ? "Command '#{command}' exited #{exit_code}" : custom_error_message
|
21
|
+
super(exit_code,error_message)
|
21
22
|
@command = command
|
22
23
|
end
|
23
24
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Methadone
|
2
|
+
module ExitNow
|
3
|
+
def self.included(k)
|
4
|
+
k.extend(self)
|
5
|
+
end
|
6
|
+
# Call this to exit the program immediately
|
7
|
+
# with the given error code and message.
|
8
|
+
#
|
9
|
+
# +exit_code+:: exit status you'd like to exit with
|
10
|
+
# +message+:: message to display to the user explaining the problem
|
11
|
+
#
|
12
|
+
# Also can be used without an exit code like so:
|
13
|
+
#
|
14
|
+
# exit_now!("Oh noes!")
|
15
|
+
#
|
16
|
+
# In this case, it's equivalent to <code>exit_now!(1,"Oh noes!")</code>.
|
17
|
+
def exit_now!(exit_code,message=nil)
|
18
|
+
if exit_code.kind_of?(String) && message.nil?
|
19
|
+
raise Methadone::Error.new(1,exit_code)
|
20
|
+
else
|
21
|
+
raise Methadone::Error.new(exit_code,message)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/methadone/main.rb
CHANGED
@@ -61,6 +61,7 @@ module Methadone
|
|
61
61
|
# *not* do this.
|
62
62
|
#
|
63
63
|
module Main
|
64
|
+
include Methadone::ExitNow
|
64
65
|
def self.included(k)
|
65
66
|
k.extend(self)
|
66
67
|
end
|
@@ -118,7 +119,6 @@ module Methadone
|
|
118
119
|
@env_var = env_var
|
119
120
|
opts.separator ''
|
120
121
|
opts.separator "Default values can be placed in the #{env_var} environment variable"
|
121
|
-
opts.separator ''
|
122
122
|
end
|
123
123
|
|
124
124
|
# Start your command-line app, exiting appropriately when
|
@@ -136,6 +136,7 @@ module Methadone
|
|
136
136
|
#
|
137
137
|
def go!
|
138
138
|
normalize_defaults
|
139
|
+
opts.post_setup
|
139
140
|
if @env_var
|
140
141
|
String(ENV[@env_var]).split(/\s+/).each do |arg|
|
141
142
|
::ARGV.unshift(arg)
|
@@ -156,15 +157,6 @@ module Methadone
|
|
156
157
|
exit 64 # Linux standard for bad command line
|
157
158
|
end
|
158
159
|
|
159
|
-
# Call this to exit the program immediately
|
160
|
-
# with the given error code and message.
|
161
|
-
#
|
162
|
-
# +exit_code+:: exit status you'd like to exit with
|
163
|
-
# +message+:: message to display to the user explaining the problem
|
164
|
-
def exit_now!(exit_code,message=nil)
|
165
|
-
raise Methadone::Error.new(exit_code,message)
|
166
|
-
end
|
167
|
-
|
168
160
|
# Returns an OptionParser that you can use
|
169
161
|
# to declare your command-line interface. Generally, you
|
170
162
|
# won't use this and will use #on directly, but this allows
|
@@ -212,6 +204,7 @@ module Methadone
|
|
212
204
|
# <tt>:one</tt>:: only one of this arg should be supplied (default)
|
213
205
|
# <tt>:many</tt>:: many of this arg may be supplied, but at least one is required
|
214
206
|
# <tt>:any</tt>:: any number, include zero, may be supplied
|
207
|
+
# A string:: if present, this will be documentation for the argument and appear in the help
|
215
208
|
def arg(arg_name,*options)
|
216
209
|
opts.arg(arg_name,*options)
|
217
210
|
end
|
@@ -311,6 +304,7 @@ module Methadone
|
|
311
304
|
@accept_options = false
|
312
305
|
@args = []
|
313
306
|
@arg_options = {}
|
307
|
+
@arg_documentation = {}
|
314
308
|
@description = nil
|
315
309
|
@version = nil
|
316
310
|
set_banner
|
@@ -363,6 +357,9 @@ module Methadone
|
|
363
357
|
options << :one unless options.include?(:any) || options.include?(:many)
|
364
358
|
@args << arg_name
|
365
359
|
@arg_options[arg_name] = options
|
360
|
+
options.select { |_| _.kind_of? ::String }.each do |doc|
|
361
|
+
@arg_documentation[arg_name] = doc + (options.include?(:optional) ? " (optional)" : "")
|
362
|
+
end
|
366
363
|
set_banner
|
367
364
|
end
|
368
365
|
|
@@ -388,6 +385,20 @@ module Methadone
|
|
388
385
|
set_banner
|
389
386
|
end
|
390
387
|
|
388
|
+
# We need some documentation to appear at the end, after all OptionParser setup
|
389
|
+
# has occured, but before we actually start. This method serves that purpose
|
390
|
+
def post_setup
|
391
|
+
unless @arg_documentation.empty?
|
392
|
+
@option_parser.separator ''
|
393
|
+
@option_parser.separator "Arguments:"
|
394
|
+
@option_parser.separator ''
|
395
|
+
@args.each do |arg|
|
396
|
+
@option_parser.separator " #{arg}"
|
397
|
+
@option_parser.separator " #{@arg_documentation[arg]}"
|
398
|
+
end
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
391
402
|
private
|
392
403
|
|
393
404
|
def add_default_value_to_docstring(*args)
|
data/lib/methadone/sh.rb
CHANGED
@@ -57,6 +57,9 @@ module Methadone
|
|
57
57
|
# This is to make it easy for you to shell-out to external commands and have your app be robust and
|
58
58
|
# easy to maintain.
|
59
59
|
module SH
|
60
|
+
def self.included(k)
|
61
|
+
k.extend(self)
|
62
|
+
end
|
60
63
|
# Run a shell command, capturing and logging its output.
|
61
64
|
# If the command completed successfully, it's output is logged at DEBUG.
|
62
65
|
# If not, its output as logged at INFO. In either case, its
|
@@ -87,10 +90,10 @@ module Methadone
|
|
87
90
|
sh_logger.warn("Error output of '#{command}': #{stderr}") unless stderr.strip.length == 0
|
88
91
|
|
89
92
|
if status.exitstatus != 0
|
90
|
-
sh_logger.info("Output of '#{command}': #{stdout}")
|
93
|
+
sh_logger.info("Output of '#{command}': #{stdout}") unless stdout.strip.length == 0
|
91
94
|
sh_logger.warn("Error running '#{command}'")
|
92
95
|
else
|
93
|
-
sh_logger.debug("Output of '#{command}': #{stdout}")
|
96
|
+
sh_logger.debug("Output of '#{command}': #{stdout}") unless stdout.strip.length == 0
|
94
97
|
call_block(block,stdout,stderr) unless block.nil?
|
95
98
|
end
|
96
99
|
|
@@ -103,10 +106,22 @@ module Methadone
|
|
103
106
|
# Run a command, throwing an exception if the command exited nonzero.
|
104
107
|
# Otherwise, behaves exactly like #sh.
|
105
108
|
#
|
109
|
+
# options - options hash, responding to:
|
110
|
+
# <tt>:on_fail</tt>:: a custom error message. This allows you to have your
|
111
|
+
# app exit on shell command failures, but customize the error
|
112
|
+
# message that they see.
|
113
|
+
#
|
106
114
|
# Raises Methadone::FailedCommandError if the command exited nonzero.
|
107
|
-
|
115
|
+
#
|
116
|
+
# Examples:
|
117
|
+
#
|
118
|
+
# sh!("rsync foo bar")
|
119
|
+
# # => if command fails, app exits and user sees: "error: Command 'rsync foo bar' exited 12"
|
120
|
+
# sh!("rsync foo bar", :on_fail => "Couldn't rsync, check log for details")
|
121
|
+
# # => if command fails, app exits and user sees: "error: Couldn't rsync, check log for details
|
122
|
+
def sh!(command,options={},&block)
|
108
123
|
sh(command,&block).tap do |exitstatus|
|
109
|
-
raise Methadone::FailedCommandError.new(exitstatus,command) if exitstatus != 0
|
124
|
+
raise Methadone::FailedCommandError.new(exitstatus,command,options[:on_fail]) if exitstatus != 0
|
110
125
|
end
|
111
126
|
end
|
112
127
|
|
data/lib/methadone/version.rb
CHANGED
data/lib/methadone.rb
CHANGED
data/templates/full/Rakefile.erb
CHANGED
@@ -17,7 +17,7 @@ end
|
|
17
17
|
CUKE_RESULTS = 'results.html'
|
18
18
|
CLEAN << CUKE_RESULTS
|
19
19
|
Cucumber::Rake::Task.new(:features) do |t|
|
20
|
-
t.cucumber_opts = "features --format html -o #{CUKE_RESULTS} --format
|
20
|
+
t.cucumber_opts = "features --format html -o #{CUKE_RESULTS} --format pretty --no-source -x"
|
21
21
|
t.fork = false
|
22
22
|
end
|
23
23
|
|
@@ -1 +1 @@
|
|
1
|
-
# Put your step
|
1
|
+
# Put your step definitions here
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'base_test'
|
2
|
+
require 'methadone'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
class TestExitNow < BaseTest
|
6
|
+
include Methadone
|
7
|
+
include Methadone::ExitNow
|
8
|
+
|
9
|
+
test_that "exit_now raises the proper error" do
|
10
|
+
Given {
|
11
|
+
@exit_code = any_int :min => 1
|
12
|
+
@message = any_string
|
13
|
+
}
|
14
|
+
When {
|
15
|
+
@code = lambda { exit_now!(@exit_code,@message) }
|
16
|
+
}
|
17
|
+
Then {
|
18
|
+
exception = assert_raises(Methadone::Error,&@code)
|
19
|
+
exception.exit_code.should == @exit_code
|
20
|
+
exception.message.should == @message
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
test_that "exit_now without an exit code uses 1 as the exti code" do
|
25
|
+
Given {
|
26
|
+
@message = any_string
|
27
|
+
}
|
28
|
+
When {
|
29
|
+
@code = lambda { exit_now!(@message) }
|
30
|
+
}
|
31
|
+
Then {
|
32
|
+
exception = assert_raises(Methadone::Error,&@code)
|
33
|
+
exception.exit_code.should == 1
|
34
|
+
exception.message.should == @message
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
data/test/test_main.rb
CHANGED
@@ -334,17 +334,23 @@ class TestMain < BaseTest
|
|
334
334
|
|
335
335
|
end
|
336
336
|
|
337
|
-
test_that "I can specify which arguments my app takes and if they are required" do
|
337
|
+
test_that "I can specify which arguments my app takes and if they are required as well as document them" do
|
338
338
|
Given {
|
339
339
|
main {}
|
340
|
+
@db_name_desc = any_string
|
341
|
+
@user_desc = any_string
|
342
|
+
@password_desc = any_string
|
340
343
|
|
341
|
-
arg :db_name
|
342
|
-
arg :user, :required
|
343
|
-
arg :password, :optional
|
344
|
+
arg :db_name, @db_name_desc
|
345
|
+
arg :user, :required, @user_desc
|
346
|
+
arg :password, :optional, @password_desc
|
344
347
|
}
|
345
|
-
|
348
|
+
When run_go_safely
|
346
349
|
Then {
|
347
350
|
opts.banner.should match /db_name user \[password\]$/
|
351
|
+
opts.to_s.should match /#{@db_name_desc}/
|
352
|
+
opts.to_s.should match /#{@user_desc}/
|
353
|
+
opts.to_s.should match /#{@password_desc}/
|
348
354
|
}
|
349
355
|
end
|
350
356
|
|
data/test/test_sh.rb
CHANGED
@@ -170,6 +170,23 @@ class TestSH < Clean::Test::TestCase
|
|
170
170
|
}
|
171
171
|
end
|
172
172
|
|
173
|
+
test_that "sh! runs a command that will fail and includes an error message that appears in the exception" do
|
174
|
+
Given {
|
175
|
+
use_capturing_logger
|
176
|
+
@command = test_command("foo")
|
177
|
+
@custom_error_message = any_sentence
|
178
|
+
}
|
179
|
+
When {
|
180
|
+
@code = lambda { sh! @command, :on_fail => @custom_error_message }
|
181
|
+
}
|
182
|
+
Then {
|
183
|
+
exception = assert_raises(Methadone::FailedCommandError,&@code)
|
184
|
+
exception.command.should == @command
|
185
|
+
exception.message.should == @custom_error_message
|
186
|
+
assert_logger_output_for_failure(@logger,@command,test_command_stdout,test_command_stderr)
|
187
|
+
}
|
188
|
+
end
|
189
|
+
|
173
190
|
class MyTestApp
|
174
191
|
include Methadone::SH
|
175
192
|
def initialize(logger)
|
data/tutorial/.vimrc
ADDED
data/tutorial/1_intro.md
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# Awesome Ruby Command Line Apps with Methadone
|
2
|
+
|
3
|
+
Kick the bash habit, and make all your command-line apps with Ruby.
|
4
|
+
|
5
|
+
In [Build Awesome Command-Line Applications in Ruby][clibook], I lay out how to make an awesome command-line application using
|
6
|
+
Ruby. The book focuses on tools like `OptionParser` to create the app. As I wrote and researched, it became clear that there
|
7
|
+
was a gap between `OptionParser`, which is very powerful, yet verbose, and other command line tools like [trollop][trollop],
|
8
|
+
[main][main], and [thor][thor], which have simple APIs, but aren't very powerful.
|
9
|
+
|
10
|
+
[clibook]: http://www.awesomecommandlineapps.com
|
11
|
+
[main]: http://github.com/ahoward/main
|
12
|
+
[trollop]: http://trollop.rubyforge.org
|
13
|
+
[thor]: http://www.github.com/wycats/thor
|
14
|
+
|
15
|
+
I created Methadone to bridge that gap. Methadone provides all the power of `OptionParser`, but has a simple, clean API.
|
16
|
+
Methadone also includes additional tools and classes to make your command-line apps even better.
|
17
|
+
|
18
|
+
This tutorial will show you how to make a simple command-line app using Methadone that will be easy-to-use, easy-to-maintain, and
|
19
|
+
fully tested.
|
20
|
+
|
21
|
+
## What you'll need
|
22
|
+
|
23
|
+
You'll need an installation of Ruby and the ability to install Ruby gems. I would recommend that you use rvm and a gemset to
|
24
|
+
work through these, but they aren't required. There's a decent [walkthrough][setup] on my book's website of setting this up the
|
25
|
+
way I work. Although Methadone works on most versions of Ruby, I would recommend you use Ruby
|
26
|
+
1.9.3, if you can. If not, try to use an MRI Ruby as those versions (1.8.7, REE, 1.9.2, or 1.9.3) have the highest compatibility
|
27
|
+
with other gems.
|
28
|
+
|
29
|
+
[setup]: http://www.awesomecommandlineapps.com/setup.html
|
30
|
+
|
31
|
+
## How this is organized
|
32
|
+
|
33
|
+
This is a tutorial for making a simple command-line app. Unlike some tutorials and books, we will be working through this using
|
34
|
+
a "test-first" approach. One thing that Methadone tries to enable is using [TDD][tdd] for creating and writing your command-line
|
35
|
+
app. As such, we'll write tests as much as possible to drive our work.
|
36
|
+
|
37
|
+
[tdd]: http://en.wikipedia.org/wiki/Test-driven_development
|
38
|
+
|
39
|
+
## The tutorial app
|
40
|
+
|
41
|
+
The app we'll build is going to manage "dot files". These are the files that live in your home directory and configure your
|
42
|
+
shell, editor, and various other programs. For example, `~/.bashrc` is the file to configure `bash`. Many developers keep these
|
43
|
+
files on [Github][github] so that they can maintain the same files across multiple computers.
|
44
|
+
|
45
|
+
[github]: http://www.github.com
|
46
|
+
|
47
|
+
To set this up on a new computer, you have to checkout the repo, and symlink all the files to your home directory. To update the
|
48
|
+
files you have to update the repo and then check if any new files were added. This is the sort of tedious manual process that is
|
49
|
+
ripe for automation via a command-line app.
|
50
|
+
|
51
|
+
We'll develop a simplified version to demonstrate how to use Methadone.
|
52
|
+
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# Bootstrapping our app
|
2
|
+
|
3
|
+
One thing that's great about writing a webapp with Ruby on Rails is that, with one command, you have a skeleton app, including
|
4
|
+
a fully functional test framework set up. You can start writing tests immediately. There's no common equivalent for a
|
5
|
+
command-line app, which is what Methadone aims to provide.
|
6
|
+
|
7
|
+
Methadone will also bootstrap other aspects of your app, such a `Rakefile`, a gemspec, a shell of an executable, a license, and a
|
8
|
+
README. First, let's install Methadone via RubyGems (note that if your aren't using rvm, you may need to use `sudo` to install
|
9
|
+
gems):
|
10
|
+
|
11
|
+
```sh
|
12
|
+
$ gem install methadone
|
13
|
+
Fetching: methadone-1.0.0.gem (100%)
|
14
|
+
Successfully installed methadone-1.0.0
|
15
|
+
1 gem installed
|
16
|
+
Installing ri documentation for methadone-1.0.0...
|
17
|
+
Installing RDoc documentation for methadone-1.0.0...
|
18
|
+
```
|
19
|
+
|
20
|
+
Methadone comes bundled with a command-linen app that will do the bootstrapping:
|
21
|
+
|
22
|
+
```sh
|
23
|
+
$ methadone --help
|
24
|
+
Usage: methadone [options] app_name
|
25
|
+
|
26
|
+
Kick the bash habit by bootstrapping your Ruby command-line apps
|
27
|
+
|
28
|
+
v1.0.0
|
29
|
+
|
30
|
+
Options:
|
31
|
+
--force Overwrite files if they exist
|
32
|
+
--[no-]readme [Do not ]produce a README file
|
33
|
+
-l, --license LICENSE Specify the license for your project (mit|apache|custom|NONE)
|
34
|
+
--log-level LEVEL Set the logging level (debug|info|warn|error|fatal)
|
35
|
+
(Default: info)
|
36
|
+
--version Show help/version info
|
37
|
+
|
38
|
+
Default values can be placed in the METHODONE_OPTS environment variable
|
39
|
+
```
|
40
|
+
|
41
|
+
The app that we'll be building in this tutorial is be called `fullstop`, which is derived from the [British name][fullstop] for a period, which is the character used as a prefix to our dotfiles, and, is the reason they are called "dot" files in the first place. Based on the command-line syntax for `methadone`, we can create our app right now with one simple command. We'll use the Apache license as well as a README.
|
42
|
+
|
43
|
+
[fullstop]: http://en.wikipedia.org/wiki/Full_stop
|
44
|
+
|
45
|
+
```sh
|
46
|
+
$ methadone --readme --license apache fullstop
|
47
|
+
$ ls fullstop
|
48
|
+
Gemfile README.rdoc bin/
|
49
|
+
fullstop.gemspec test/ LICENSE.txt
|
50
|
+
Rakefile features/ lib/
|
51
|
+
```
|
52
|
+
|
53
|
+
As you can see, we've got a generic gemified project. Before we can start developing, we'll need to install a few gems using Bundler first:
|
54
|
+
|
55
|
+
```sh
|
56
|
+
$ cd fullstop
|
57
|
+
$ bundle install
|
58
|
+
Fetching source index for http://rubygems.org/
|
59
|
+
Installing rake (0.9.2.2)
|
60
|
+
Installing ffi (1.0.11) with native extensions
|
61
|
+
Installing childprocess (0.3.1)
|
62
|
+
Installing builder (3.0.0)
|
63
|
+
Installing diff-lcs (1.1.3)
|
64
|
+
Installing json (1.6.5) with native extensions
|
65
|
+
Installing gherkin (2.7.6) with native extensions
|
66
|
+
Installing term-ansicolor (1.0.7)
|
67
|
+
Installing cucumber (1.1.4)
|
68
|
+
Installing rspec-core (2.8.0)
|
69
|
+
Installing rspec-expectations (2.8.0)
|
70
|
+
Installing rspec-mocks (2.8.0)
|
71
|
+
Installing rspec (2.8.0)
|
72
|
+
Installing aruba (0.4.11)
|
73
|
+
Using bundler (1.0.21)
|
74
|
+
Installing methadone (0.5.1)
|
75
|
+
Using fullstop (0.0.1) from source at /Users/davec/Projects/methadone/tutorial/code/fullstop
|
76
|
+
Installing rdoc (3.12)
|
77
|
+
Your bundle is complete! Use `bundle show [gemname]` to see where a bundled gem is installed.
|
78
|
+
```
|
79
|
+
|
80
|
+
Your versions might not match up, but this should be more or less what you see. The first thing you'll notice is that this seems
|
81
|
+
like a *lot* of gems! Most of them are brought in by our acceptance testing framework, [aruba][aruba], which is a library on top
|
82
|
+
of [cucumber][cucumber] tailor-made for testing command-line apps. Methadone also assumes you'll be unit
|
83
|
+
testing with `Test::Unit`, which is a fine default. In fact, both unit and acceptance tests are set up for you and available
|
84
|
+
via `rake` tasks. Let's see them in action.
|
85
|
+
|
86
|
+
[aruba]: http://www.github.com/cucumber/aruba
|
87
|
+
[cucumber]: http://cukes.info
|
88
|
+
|
89
|
+
```sh
|
90
|
+
$ rake
|
91
|
+
Run options:
|
92
|
+
|
93
|
+
# Running tests:
|
94
|
+
|
95
|
+
.
|
96
|
+
|
97
|
+
Finished tests in 0.000623s, 1605.1364 tests/s, 1605.1364 assertions/s.
|
98
|
+
|
99
|
+
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
|
100
|
+
......
|
101
|
+
|
102
|
+
1 scenario (1 passed)
|
103
|
+
6 steps (6 passed)
|
104
|
+
0m0.136s
|
105
|
+
```
|
106
|
+
|
107
|
+
As you can see, we ran one unit test and one cucumber scenario. These were provided by Methadone as placeholders for your tests.
|
108
|
+
Just like what Ruby on Rails does when you create an app, Methadone has reduced the friction between your need to write software
|
109
|
+
and your ability to do so.
|
110
|
+
|
111
|
+
Methadone also generated a very basic scaffold of the command-line app itself. It's in `bin` and is called `fullstop` (the
|
112
|
+
argument we gave to `methadone`). Let's run it now:
|
113
|
+
|
114
|
+
```sh
|
115
|
+
$ bin/fullstop
|
116
|
+
/Users/davec/.rvm/rubies/ruby-1.9.3-p0/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:36:in `require': cannot load such file -- fullstop (LoadError)
|
117
|
+
from /Users/davec/.rvm/rubies/ruby-1.9.3-p0/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:36:in `require'
|
118
|
+
from bin/fullstop:5:in `<main>'
|
119
|
+
```
|
120
|
+
|
121
|
+
Oops! What happened?
|
122
|
+
|
123
|
+
Methadone is encouraging you to develop your app with best practices, and one such practice is to not have
|
124
|
+
your executables mess with the load path. In many Ruby command-line applications, you'll see code like this at the top of the
|
125
|
+
file:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
$: << File.join(File.dirname(__FILE__),'..','lib')
|
129
|
+
```
|
130
|
+
|
131
|
+
This puts the `lib` directory, that is relative to the `bin` directory (where our executable lives), into Ruby's load path. This will allow *us* to run the app easily, but for your users, it's not necessary if you distribute your app with RubyGems (which you should do) and it's generally not a good idea to modify the load path. In order to run the app directly during development, we'll need to use `bundle exec`, like so (note that we won't be running our app a lot in development, but scripting it using Aruba so we can test its behavior in an automated way):
|
132
|
+
|
133
|
+
```sh
|
134
|
+
$ bundle exec bin/fullstop --help
|
135
|
+
Usage: fullstop [options]
|
136
|
+
|
137
|
+
v0.0.1
|
138
|
+
|
139
|
+
Options:
|
140
|
+
--version Show help/version info
|
141
|
+
--log-level LEVEL Set the logging level (debug|info|warn|error|fatal)
|
142
|
+
(Default: info)
|
143
|
+
```
|
144
|
+
|
145
|
+
|
146
|
+
Not too bad! We've got the makings of a reasonable help system, versioning support, a usage statement and a working executable.
|
147
|
+
Just remember to run the app with `bundle exec` while you're developing. Remember, your users won't have to worry about as long
|
148
|
+
as they installed it with RubyGems.
|
149
|
+
|
150
|
+
Before we move on, let's look at the cucumber scenario that Methadone generated for us. We're going to work "outside in" on our
|
151
|
+
app, so this will be a sneak peek at what we'll be doing next.
|
152
|
+
|
153
|
+
```sh
|
154
|
+
$ cat features/fullstop.feature
|
155
|
+
```
|
156
|
+
```cucumber
|
157
|
+
Feature: My bootstrapped app kinda works
|
158
|
+
In order to get going on coding my awesome app
|
159
|
+
I want to have aruba and cucumber setup
|
160
|
+
So I don't have to do it myself
|
161
|
+
|
162
|
+
Scenario: App just runs
|
163
|
+
When I get help for "fullstop"
|
164
|
+
Then the exit status should be 0
|
165
|
+
And the banner should be present
|
166
|
+
And the banner should document that this app takes options
|
167
|
+
And the following options should be documented:
|
168
|
+
|--version|
|
169
|
+
And the banner should document that this app takes no arguments
|
170
|
+
```
|
171
|
+
|
172
|
+
We probably won't keep this exactly scenario around, but it's a good demonstration of Aruba and Cucumber, and will help to get us
|
173
|
+
going. Since this scenario passes, that means that we already have the cucumber steps defined somewhere. As we'll see, the
|
174
|
+
combination of Aruba and Methadone results in a lot of pre-defined steps that make acceptance testing a snap.
|
175
|
+
|
176
|
+
In the next section, we'll expand this scenario to create the user interface we'll need to get our app going.
|