methadone 1.0.0.rc4 → 1.0.0.rc5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +1 -1
- data/bin/methadone +1 -4
- data/lib/methadone/cli_logging.rb +3 -1
- data/lib/methadone/main.rb +44 -26
- data/lib/methadone/version.rb +1 -1
- data/tutorial/2_bootstrap.md +2 -4
- data/tutorial/3_ui.md +7 -20
- data/tutorial/4_happy_path.md +14 -46
- data/tutorial/5_more_features.md +10 -19
- data/tutorial/7_logging_debugging.md +20 -50
- metadata +20 -20
data/README.rdoc
CHANGED
@@ -70,7 +70,7 @@ Basically, this sets you up with all the boilerplate that you *should* be using
|
|
70
70
|
|
71
71
|
== DSL for your <tt>bin</tt> file
|
72
72
|
|
73
|
-
A canonical <tt>OptionParser</tt> driven app has a few problems with it structurally that
|
73
|
+
A canonical <tt>OptionParser</tt> driven app has a few problems with it structurally that Methadone can solve:
|
74
74
|
|
75
75
|
* Backwards organization - main logic is at the bottom of the file, not the top
|
76
76
|
* Verbose to use +opts.on+ just to set a value in a +Hash+
|
data/bin/methadone
CHANGED
@@ -60,7 +60,7 @@ on("--force","Overwrite files if they exist")
|
|
60
60
|
on("--[no-]readme","[Do not ]produce a README file")
|
61
61
|
|
62
62
|
licenses = %w(mit apache custom NONE)
|
63
|
-
on("-l LICENSE","--license",licenses,"Specify the license for your project
|
63
|
+
on("-l LICENSE","--license",licenses,"Specify the license for your project",'(' + licenses.join('|') + ')')
|
64
64
|
|
65
65
|
use_log_level_option
|
66
66
|
|
@@ -68,8 +68,5 @@ arg :app_name, :required, "Name of your app, which is used for the gem name and
|
|
68
68
|
|
69
69
|
version Methadone::VERSION
|
70
70
|
|
71
|
-
defaults_from_env_var 'METHODONE_OPTS'
|
72
|
-
defaults_from_config_file '.methadone.rc'
|
73
|
-
|
74
71
|
go!
|
75
72
|
|
@@ -84,7 +84,9 @@ module Methadone
|
|
84
84
|
# go!
|
85
85
|
#
|
86
86
|
def use_log_level_option
|
87
|
-
on("--log-level LEVEL",LOG_LEVELS,
|
87
|
+
on("--log-level LEVEL",LOG_LEVELS,'Set the logging level',
|
88
|
+
'(' + LOG_LEVELS.keys.join('|') + ')',
|
89
|
+
'(Default: info)') do |level|
|
88
90
|
@log_level = level
|
89
91
|
logger.level = level
|
90
92
|
end
|
data/lib/methadone/main.rb
CHANGED
@@ -43,7 +43,10 @@ module Methadone
|
|
43
43
|
#
|
44
44
|
# arg :needed
|
45
45
|
# arg :maybe, :optional
|
46
|
-
#
|
46
|
+
#
|
47
|
+
# defaults_from_env_var SOME_VAR
|
48
|
+
# defaults_from_config_file '.my_app.rc'
|
49
|
+
#
|
47
50
|
# go!
|
48
51
|
# end
|
49
52
|
#
|
@@ -53,6 +56,12 @@ module Methadone
|
|
53
56
|
# # => parse error: 'needed' is required
|
54
57
|
# $ our_app foo
|
55
58
|
# # => succeeds; "maybe" in main is nil
|
59
|
+
# $ our_app --flag foo
|
60
|
+
# # => options[:flag] has the value "foo"
|
61
|
+
# $ SOME_VAR='--flag foo' our_app
|
62
|
+
# # => options[:flag] has the value "foo"
|
63
|
+
# $ SOME_VAR='--flag foo' our_app --flag bar
|
64
|
+
# # => options[:flag] has the value "bar"
|
56
65
|
#
|
57
66
|
# Note that we've done all of this inside a class that we called +App+. This isn't strictly
|
58
67
|
# necessary, and you can just +include+ Methadone::Main and Methadone::CLILogging at the root
|
@@ -130,22 +139,6 @@ module Methadone
|
|
130
139
|
@rc_file = File.join(ENV['HOME'],filename)
|
131
140
|
end
|
132
141
|
|
133
|
-
def add_defaults_to_docs
|
134
|
-
if @env_var && @rc_file
|
135
|
-
opts.separator ''
|
136
|
-
opts.separator 'Default values can be placed in:'
|
137
|
-
opts.separator ''
|
138
|
-
opts.separator " #{@env_var} environment variable, as a String of options"
|
139
|
-
opts.separator " #{@rc_file} with contents either a String of options or a YAML-encoded Hash"
|
140
|
-
elsif @env_var
|
141
|
-
opts.separator ''
|
142
|
-
opts.separator "Default values can be placed in the #{@env_var} environment variable"
|
143
|
-
elsif @rc_file
|
144
|
-
opts.separator ''
|
145
|
-
opts.separator "Default values can be placed in #{@rc_file}"
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
142
|
# Start your command-line app, exiting appropriately when
|
150
143
|
# complete.
|
151
144
|
#
|
@@ -158,17 +151,9 @@ module Methadone
|
|
158
151
|
#
|
159
152
|
# If a required argument (see #arg) is not found, this exits with
|
160
153
|
# 64 and a message about that missing argument.
|
161
|
-
#
|
162
154
|
def go!
|
163
|
-
|
164
|
-
set_defaults_from_rc_file
|
165
|
-
normalize_defaults
|
155
|
+
setup_defaults
|
166
156
|
opts.post_setup
|
167
|
-
if @env_var
|
168
|
-
String(ENV[@env_var]).split(/\s+/).each do |arg|
|
169
|
-
::ARGV.unshift(arg)
|
170
|
-
end
|
171
|
-
end
|
172
157
|
opts.parse!
|
173
158
|
opts.check_args!
|
174
159
|
result = call_main
|
@@ -282,6 +267,39 @@ module Methadone
|
|
282
267
|
|
283
268
|
private
|
284
269
|
|
270
|
+
def setup_defaults
|
271
|
+
add_defaults_to_docs
|
272
|
+
set_defaults_from_rc_file
|
273
|
+
normalize_defaults
|
274
|
+
set_defaults_from_env_var
|
275
|
+
end
|
276
|
+
|
277
|
+
def add_defaults_to_docs
|
278
|
+
if @env_var && @rc_file
|
279
|
+
opts.separator ''
|
280
|
+
opts.separator 'Default values can be placed in:'
|
281
|
+
opts.separator ''
|
282
|
+
opts.separator " #{@env_var} environment variable, as a String of options"
|
283
|
+
opts.separator " #{@rc_file} with contents either a String of options "
|
284
|
+
spaces = (0..@rc_file.length).reduce('') { |a,_| a << ' ' }
|
285
|
+
opts.separator " #{spaces}or a YAML-encoded Hash"
|
286
|
+
elsif @env_var
|
287
|
+
opts.separator ''
|
288
|
+
opts.separator "Default values can be placed in the #{@env_var} environment variable"
|
289
|
+
elsif @rc_file
|
290
|
+
opts.separator ''
|
291
|
+
opts.separator "Default values can be placed in #{@rc_file}"
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def set_defaults_from_env_var
|
296
|
+
if @env_var
|
297
|
+
String(ENV[@env_var]).split(/\s+/).each do |arg|
|
298
|
+
::ARGV.unshift(arg)
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
285
303
|
def set_defaults_from_rc_file
|
286
304
|
if @rc_file && File.exists?(@rc_file)
|
287
305
|
File.open(@rc_file) do |file|
|
data/lib/methadone/version.rb
CHANGED
data/tutorial/2_bootstrap.md
CHANGED
@@ -17,7 +17,7 @@ Installing ri documentation for methadone-1.0.0...
|
|
17
17
|
Installing RDoc documentation for methadone-1.0.0...
|
18
18
|
```
|
19
19
|
|
20
|
-
Methadone comes bundled with a command-
|
20
|
+
Methadone comes bundled with a command-line app that will do the bootstrapping:
|
21
21
|
|
22
22
|
```sh
|
23
23
|
$ methadone --help
|
@@ -120,9 +120,7 @@ from bin/fullstop:5:in `<main>'
|
|
120
120
|
|
121
121
|
Oops! What happened?
|
122
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:
|
123
|
+
Methadone is encouraging you to develop your app with best practices, and one such practice is to not have your executables mess with the load path. In many Ruby command-line applications, you'll see code like this at the top of the file:
|
126
124
|
|
127
125
|
```ruby
|
128
126
|
$: << File.join(File.dirname(__FILE__),'..','lib')
|
data/tutorial/3_ui.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# UI
|
1
|
+
# Tutorial: UI
|
2
2
|
|
3
3
|
We're taking an [outside-in][outsidein] approach to our app. This means that we start with the user interface, and work our way
|
4
4
|
down to make that interface a reality. I highly recommend this approach, since it forces you to focus on the user of your app
|
@@ -87,18 +87,11 @@ Tasks: TOP => features
|
|
87
87
|
(See full trace by running task with --trace)
|
88
88
|
```
|
89
89
|
|
90
|
-
We have a failing test! Note that cucumber knows about all of these steps; between Aruba and Metahdone, they are all already
|
91
|
-
defined. We'll see some custom steps later, that are specific to our app, but for now, we haven't had to write any testing code,
|
92
|
-
which is great!
|
90
|
+
We have a failing test! Note that cucumber knows about all of these steps; between Aruba and Metahdone, they are all already defined. We'll see some custom steps later, that are specific to our app, but for now, we haven't had to write any testing code, which is great!
|
93
91
|
|
94
|
-
You'll also notice that some steps are already passing, despite the fact that we've done no coding. Also notice that
|
95
|
-
cucumber didn't complain about unknown steps. Methadone provides almost all of these cucumber steps for us. The rest are
|
96
|
-
provided by Aruba. Since Methadone generated an executable for us when we ran the `methadone` command, it already provides the
|
97
|
-
ability to get help, and exits with the correct exit status.
|
92
|
+
You'll also notice that some steps are already passing, despite the fact that we've done no coding. Also notice that cucumber didn't complain about unknown steps. Methadone provides almost all of these cucumber steps for us. The rest are provided by Aruba. Since Methadone generated an executable for us when we ran the `methadone` command, it already provides the ability to get help, and exits with the correct exit status.
|
98
93
|
|
99
|
-
Let's fix things one step at a time, so we can see exactly what we need to do. The current scenario is failing because our app
|
100
|
-
doesn't have a one line summary. This summary is important so that we can remember what the app does later on (despite how
|
101
|
-
clever our name is, it's likely we'll forget a few months from now and the description will jog our memory).
|
94
|
+
Let's fix things one step at a time, so we can see exactly what we need to do. The current scenario is failing because our app doesn't have a one line summary. This summary is important so that we can remember what the app does later on (despite how clever our name is, it's likely we'll forget a few months from now and the description will jog our memory).
|
102
95
|
|
103
96
|
Let's have a look at our executable. A Methadone app is made up of four parts: the setup where we require necessary libraries, a "main" block containing the primary logic of our code, a block of code that declares the app's UI, and a call to `go!`, which runs our app.
|
104
97
|
|
@@ -230,9 +223,7 @@ We got farther this time. Our step for checking that we have a one-line summary
|
|
230
223
|
|
231
224
|
The call to Methadone's `version` method ensures that the version of our app appears in the online help. The other step, "And the banner should document that this app takes options" passes because we are allowing Methadone to manage the banner. Methadone knows that our app takes options (namely `--version`), and inserts the string `"[options]"` into the usage statement.
|
232
225
|
|
233
|
-
The last step in our scenario is still failing, so let's fix that to finish up our user interface.
|
234
|
-
What Methadone is looking for is for the string `repo_url` (the name of our only,
|
235
|
-
required, argument) to be in the usage string, in other words, Methadone is expecting to see this:
|
226
|
+
The last step in our scenario is still failing, so let's fix that to finish up our user interface. What Methadone is looking for is for the string `repo_url` (the name of our only, required, argument) to be in the usage string, in other words, Methadone is expecting to see this:
|
236
227
|
|
237
228
|
```
|
238
229
|
Usage: fullstop [option] repo_url
|
@@ -244,9 +235,7 @@ Right now, our app's usage string looks like this:
|
|
244
235
|
Usage: fullstop [option]
|
245
236
|
```
|
246
237
|
|
247
|
-
Again, if we were using `OptionParser`, we would need to modify the argument given to `banner` to include this string. Methadone
|
248
|
-
provides a method, `arg` that will do this automatically for us. We'll add it right after the call to `description` in the
|
249
|
-
"declare UI" section of our app:
|
238
|
+
Again, if we were using `OptionParser`, we would need to modify the argument given to `banner` to include this string. Methadone provides a method, `arg` that will do this automatically for us. We'll add it right after the call to `description` in the "declare UI" section of our app:
|
250
239
|
|
251
240
|
```ruby
|
252
241
|
#!/usr/bin/env ruby
|
@@ -340,9 +329,7 @@ $ echo $?
|
|
340
329
|
|
341
330
|
We see an error message, and exited nonzero (64 is a somewhat standard exit code for errors in command-line invocation).
|
342
331
|
|
343
|
-
It's also worth pointing out that Methadone is taking a very light touch. We could completely re-implement `bin/fullstop` using
|
344
|
-
`OptionParser` and still have our scenario pass. As we'll see, few of Methadone's parts really rely on each other, and many can
|
345
|
-
be used piecemeal, if that's what you want.
|
332
|
+
It's also worth pointing out that Methadone is taking a very light touch. We could completely re-implement `bin/fullstop` using `OptionParser` and still have our scenario pass. As we'll see, few of Methadone's parts really rely on each other, and many can be used piecemeal, if that's what you want.
|
346
333
|
|
347
334
|
Now that we have our UI, the next order of business is to actually implement something.
|
348
335
|
|
data/tutorial/4_happy_path.md
CHANGED
@@ -14,19 +14,13 @@ We'll append this to `features/fullstop.feature`:
|
|
14
14
|
And the files in "~/dotfiles" should be symlinked in my home directory
|
15
15
|
```
|
16
16
|
|
17
|
-
Basically, what we're doing is assuming a git repository in `/tmp/dotfiles.git`, which we then expect `fullstop` to clone,
|
18
|
-
followed by symlinking the contents to our home directory. There is, however, a slight problem.
|
17
|
+
Basically, what we're doing is assuming a git repository in `/tmp/dotfiles.git`, which we then expect `fullstop` to clone, followed by symlinking the contents to our home directory. There is, however, a slight problem.
|
19
18
|
|
20
|
-
Suppose we make this scenario pass. This means that *every* time we run this scenario, our dotfiles in our *actual* home
|
21
|
-
directory will be blown away. Yikes! We don't want that; we want our test as isolated as it can be. What we'd like is to work
|
22
|
-
in a home directory that, from the perspective of our cucumber tests, is not our home directory and completely under the conrol
|
23
|
-
of the tests but, from the perspective of the `fullstop` app, is the user's bona-fide home directory.
|
19
|
+
Suppose we make this scenario pass. This means that *every* time we run this scenario, our dotfiles in our *actual* home directory will be blown away. Yikes! We don't want that; we want our test as isolated as it can be. What we'd like is to work in a home directory that, from the perspective of our cucumber tests, is not our home directory and completely under the conrol of the tests but, from the perspective of the `fullstop` app, is the user's bona-fide home directory.
|
24
20
|
|
25
|
-
We can easily fake this by changing the environment variable `$HOME` just for the tests. As long as `bin/fullstop` uses this
|
26
|
-
environment variable to access the user's home directory (which is perfectly valid), everything will be OK.
|
21
|
+
We can easily fake this by changing the environment variable `$HOME` just for the tests. As long as `bin/fullstop` uses this environment variable to access the user's home directory (which is perfectly valid), everything will be OK.
|
27
22
|
|
28
|
-
To do that, we need to modify some of cucumber's plumbing. Methadone won't do this for you, since it's not applicable to every
|
29
|
-
situation or app. Open up `features/support/env.rb`. It should look like this:
|
23
|
+
To do that, we need to modify some of cucumber's plumbing. Methadone won't do this for you, since it's not applicable to every situation or app. Open up `features/support/env.rb`. It should look like this:
|
30
24
|
|
31
25
|
```ruby
|
32
26
|
require 'aruba/cucumber'
|
@@ -47,10 +41,7 @@ After do
|
|
47
41
|
end
|
48
42
|
```
|
49
43
|
|
50
|
-
There's a lot in there already to make our tests work and, fortunately, it makes our job of faking the home directory a bit
|
51
|
-
easier. We need to save the original location in `Before`, and then change it there, setting it back to normal in `After`, just
|
52
|
-
as we have done with the `$RUBYLIB` environment variable (incidentally, this is how Aruba can run our app without using `bundle
|
53
|
-
exec`).
|
44
|
+
There's a lot in there already to make our tests work and, fortunately, it makes our job of faking the home directory a bit easier. We need to save the original location in `Before`, and then change it there, setting it back to normal in `After`, just as we have done with the `$RUBYLIB` environment variable (incidentally, this is how Aruba can run our app without using `bundle exec`).
|
54
45
|
|
55
46
|
```ruby
|
56
47
|
require 'aruba/cucumber'
|
@@ -128,10 +119,7 @@ Then /^the files in "([^"]*)" should be symlinked in my home directory$/ do |arg
|
|
128
119
|
end
|
129
120
|
```
|
130
121
|
|
131
|
-
As you can see there are three steps that cucumber doesn't know how to execute. It provides boilerplate for doing so, so let's
|
132
|
-
do that next. We're going to move a bit faster here, since the specifics of implementing cucumber steps is orthogonal to
|
133
|
-
Methadone, and we don't want to stray too far from our goal of learning Methadone. If you'd like
|
134
|
-
to explore this in more detail, check out the testing chapter of [my book][clibook].
|
122
|
+
As you can see there are three steps that cucumber doesn't know how to execute. It provides boilerplate for doing so, so let's do that next. We're going to move a bit faster here, since the specifics of implementing cucumber steps is orthogonal to Methadone, and we don't want to stray too far from our goal of learning Methadone. If you'd like to explore this in more detail, check out the testing chapter of [my book][clibook].
|
135
123
|
|
136
124
|
[clibook]: http://www.awesomecommandlineapps.com
|
137
125
|
|
@@ -239,31 +227,16 @@ It's a bit hard to understand *why* it's failing, but the error message and line
|
|
239
227
|
File.exist?(dotfiles_dir).should == true
|
240
228
|
```
|
241
229
|
|
242
|
-
Since `dotfiles_dir` is `~/dotfiles` (or, more specifically, `File.join(ENV['HOME'],'dotfiles')`), and it doesn't exist, since we
|
243
|
-
|
244
|
-
should consider writing some custom RSpec matchers for your assertions, since they can allow you to produce better failure
|
245
|
-
messages.
|
246
|
-
|
247
|
-
Now that we have a failing test, we can start writing some code. This is the first bit of actual logic we'll write, and we need
|
248
|
-
to revisit the canonical structure of a Methadone app to know where to put it.
|
249
|
-
|
230
|
+
Since `dotfiles_dir` is `~/dotfiles` (or, more specifically, `File.join(ENV['HOME'],'dotfiles')`), and it doesn't exist, since we haven't written any code that might cause it to exist, the test fails. Although it's outside the scope of this tutorial, you should consider writing some custom RSpec matchers for your assertions, since they can allow you to produce better failure messages.
|
231
|
+
Now that we have a failing test, we can start writing some code. This is the first bit of actual logic we'll write, and we need to revisit the canonical structure of a Methadone app to know where to put it.
|
250
232
|
Recall that the second part of our app is the "main" block, and it's intended to hold the primary logic of your application. Methadone provides the method `main`, which lives in `Methadone::Main`, and takes a block. This block is where you put your logic. Think of it like the `main` method of a C program.
|
251
|
-
|
252
|
-
Now that we know where to put our code, we need to know *what* code we need to add. To make this step pass, we need to clone the
|
253
|
-
repo given to us on the command-line. To do that we need:
|
254
|
-
|
233
|
+
Now that we know where to put our code, we need to know *what* code we need to add. To make this step pass, we need to clone the repo given to us on the command-line. To do that we need:
|
255
234
|
* The ability to execute `git`
|
256
235
|
* The ability to change to the user's home directory
|
257
236
|
* Access to the repo's URL from the command line
|
237
|
+
Although we can use `system` or the backtick operator to call `git`, we're going to use `sh`, which is available by mixing in `Methadone::SH`. We'll go into the advantages of why we might want to do that later in the tutorial, but for now, think of it as saving us a few characters over `system`.
|
258
238
|
|
259
|
-
|
260
|
-
`Methadone::SH`. We'll go into the advantages of why we might want to do that later in the tutorial, but for now, think of it as
|
261
|
-
saving us a few characters over `system`.
|
262
|
-
|
263
|
-
We can change to the user's home directory using the `chdir` method of `Dir`, which is built-in to Ruby. To get the value of the
|
264
|
-
URL the user provided on the command-line, we could certainly take it from `ARGV`, but Methadone allows you `main` block to take
|
265
|
-
arguments, which it will populate with the contents of `ARGV`. All we need to do is change our `main` block to accept `repo_url`
|
266
|
-
as an argument.
|
239
|
+
We can change to the user's home directory using the `chdir` method of `Dir`, which is built-in to Ruby. To get the value of the URL the user provided on the command-line, we could certainly take it from `ARGV`, but Methadone allows you `main` block to take arguments, which it will populate with the contents of `ARGV`. All we need to do is change our `main` block to accept `repo_url` as an argument.
|
267
240
|
|
268
241
|
Here's the code:
|
269
242
|
|
@@ -304,8 +277,7 @@ class App
|
|
304
277
|
end
|
305
278
|
```
|
306
279
|
|
307
|
-
Note that all we're doing here is getting the currently-failing step to pass. We *aren't* implementing the entire app. We want
|
308
|
-
to write only the code we need to, and we go one step at a time. Let's re-run our scenario and see if we get farther:
|
280
|
+
Note that all we're doing here is getting the currently-failing step to pass. We *aren't* implementing the entire app. We want to write only the code we need to, and we go one step at a time. Let's re-run our scenario and see if we get farther:
|
309
281
|
|
310
282
|
```sh
|
311
283
|
rake features
|
@@ -357,10 +329,7 @@ We're now failing at the next step:
|
|
357
329
|
And the files in "~/dotfiles" should be symlinked in my home directory
|
358
330
|
```
|
359
331
|
|
360
|
-
The error, "No such file or directory - .vimrc", is being raised from `File.lstat` (as opposed to an explicit test failure).
|
361
|
-
This is enough to allow us to write some more code. What we need to do know is iterate over the files in the cloned repo and
|
362
|
-
symlink them to the user's home directory. The tools to do this are already available to use via the built-in Ruby library
|
363
|
-
`FileUtils`. We'll require it and implement the symlinking logic:
|
332
|
+
The error, "No such file or directory - .vimrc", is being raised from `File.lstat` (as opposed to an explicit test failure). This is enough to allow us to write some more code. What we need to do know is iterate over the files in the cloned repo and symlink them to the user's home directory. The tools to do this are already available to use via the built-in Ruby library `FileUtils`. We'll require it and implement the symlinking logic:
|
364
333
|
|
365
334
|
```ruby
|
366
335
|
#!/usr/bin/env ruby
|
@@ -433,5 +402,4 @@ Feature: Checkout dotfiles
|
|
433
402
|
0m0.396s
|
434
403
|
```
|
435
404
|
|
436
|
-
Everything passed! Our app now works for the "happy path". As long as the user starts from a clean home directory, `fullstop`
|
437
|
-
will clone their dotfiles, and setup symlinks to them in their home directory. Now that we have the basics of our app running, we'll see how Methadone makes it easy to add new features.
|
405
|
+
Everything passed! Our app now works for the "happy path". As long as the user starts from a clean home directory, `fullstop` will clone their dotfiles, and setup symlinks to them in their home directory. Now that we have the basics of our app running, we'll see how Methadone makes it easy to add new features.
|
data/tutorial/5_more_features.md
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
# Adding Features
|
2
2
|
|
3
3
|
Our command-line app isn't very interesting at this point; it's more of a glorified shell script. Where Ruby and Methadone
|
4
|
-
really shine is when things start getting complex.
|
5
|
-
|
4
|
+
really shine is when things start getting complex.
|
5
|
+
|
6
|
+
There's a lot of features we can add and error cases we can handle, for example:
|
6
7
|
|
7
8
|
* The app will blow up if the git repo is already cloned
|
8
9
|
* The app might blow up if the files are already symlinked
|
@@ -180,7 +181,8 @@ on("--force","Force overwriting of existing files")
|
|
180
181
|
on("-d DIR","--checkout-dir","Set the location of the checkout dir")
|
181
182
|
```
|
182
183
|
|
183
|
-
That's it! 14 lines become 3.
|
184
|
+
That's it! 14 lines become 3.
|
185
|
+
When `main` executes, the following keys in `options` will be available:
|
184
186
|
|
185
187
|
* `"force"` - true if the user specified `--force`
|
186
188
|
* `:force` - the same
|
@@ -375,9 +377,7 @@ Tasks: TOP => features
|
|
375
377
|
(See full trace by running task with --trace)
|
376
378
|
```
|
377
379
|
|
378
|
-
We're failing because the new file we added to our repo after the initial clone can't be found. It's likely that our second
|
379
|
-
clone failed, but we didn't notice, because we aren't checking. If we run our app manually, we can see that errors are flying,
|
380
|
-
but we're ignoring them:
|
380
|
+
We're failing because the new file we added to our repo after the initial clone can't be found. It's likely that our second clone failed, but we didn't notice, because we aren't checking. If we run our app manually, we can see that errors are flying, but we're ignoring them:
|
381
381
|
|
382
382
|
```sh
|
383
383
|
$ HOME=/tmp/fake-home bundle exec bin/fullstop file:///tmp/dotfiles.git
|
@@ -390,10 +390,7 @@ $ echo $?
|
|
390
390
|
70
|
391
391
|
```
|
392
392
|
|
393
|
-
We can see that error output is being produced from `git`, but we're ignoring it. `fullstop` fails later in the process when we
|
394
|
-
ask it to symlink files that already exist. This is actually a bug, so let's take a short detour and fix
|
395
|
-
this problem. When doing TDD, it's important to know how your app is failing, so you can be confident that the code you are
|
396
|
-
about to write fixes the correct failing in the existing app.
|
393
|
+
We can see that error output is being produced from `git`, but we're ignoring it. `fullstop` fails later in the process when we ask it to symlink files that already exist. This is actually a bug, so let's take a short detour and fix this problem. When doing TDD, it's important to know how your app is failing, so you can be confident that the code you are about to write fixes the correct failing in the existing app.
|
397
394
|
|
398
395
|
We'll write a scenario to reveal the bug:
|
399
396
|
|
@@ -465,9 +462,7 @@ Tasks: TOP => features
|
|
465
462
|
(See full trace by running task with --trace)
|
466
463
|
```
|
467
464
|
|
468
|
-
It looks like `fullstop` is writing log messages. It is, and we'll talk about that more later, but right now, we need to focus
|
469
|
-
on the fact that we aren't producing the error message we expect. Let's modify `bin/fullstop` to check that the call to `git`
|
470
|
-
succeeded. `sh` returns the exit status of the command it calls, so we can use that to fix things.
|
465
|
+
It looks like `fullstop` is writing log messages. It is, and we'll talk about that more later, but right now, we need to focus on the fact that we aren't producing the error message we expect. Let's modify `bin/fullstop` to check that the call to `git` succeeded. `sh` returns the exit status of the command it calls, so we can use that to fix things.
|
471
466
|
|
472
467
|
Here's the changes we'll make to `bin/fullstop` to check for this:
|
473
468
|
|
@@ -566,10 +561,7 @@ Feature: Checkout dotfiles
|
|
566
561
|
0m0.789s
|
567
562
|
```
|
568
563
|
|
569
|
-
Note that there is a companion method to `sh`, called `sh!` that will throw an exception if the underlying command it calls
|
570
|
-
fails. In a Methadone app, any unhandled exception will trigger a nonzero exit from the app, and show the user the message of
|
571
|
-
the exception that caused the exit. We can customize the message of the exception thrown from `sh!`, and thus our change
|
572
|
-
to our app could also be implemented like so:
|
564
|
+
Note that there is a companion method to `sh`, called `sh!` that will throw an exception if the underlying command it calls fails. In a Methadone app, any unhandled exception will trigger a nonzero exit from the app, and show the user the message of the exception that caused the exit. We can customize the message of the exception thrown from `sh!`, and thus our change to our app could also be implemented like so:
|
573
565
|
|
574
566
|
```ruby
|
575
567
|
main do |repo_url|
|
@@ -591,8 +583,7 @@ Which method to use is purely stylistic and up to you.
|
|
591
583
|
|
592
584
|
NOW, we can get back to the `--force` flag. We're going to change our scenario a bit, as well. Instead of using "When I run `fullstop --force file:///tmp/dotfiles.git`" we'll use "When I successfully run `fullstop --force file:///tmp/dotfiles.git`", which will fail if the app exits nonzero. This will cause our scenario to fail earlier.
|
593
585
|
|
594
|
-
To fix this, we'll change the code in `bin/fullstop` so that if the user specified `--force`, we'll delete the directory before we
|
595
|
-
clone. We'll also need to delete the files that were symlinked in the home directory as well.
|
586
|
+
To fix this, we'll change the code in `bin/fullstop` so that if the user specified `--force`, we'll delete the directory before we clone. We'll also need to delete the files that were symlinked in the home directory as well.
|
596
587
|
|
597
588
|
```ruby
|
598
589
|
#!/usr/bin/env ruby
|
@@ -1,8 +1,6 @@
|
|
1
1
|
# Logging & Debugging
|
2
2
|
|
3
|
-
By now, you've got the basics of using Methadone, but there's a few things happening under the covers that you should know about,
|
4
|
-
and a few things built-in to the methods and modules we've been using that will be helpful both in developing your app and in
|
5
|
-
examining its behavior in production.
|
3
|
+
By now, you've got the basics of using Methadone, but there's a few things happening under the covers that you should know about, and a few things built-in to the methods and modules we've been using that will be helpful both in developing your app and in examining its behavior in production.
|
6
4
|
|
7
5
|
When trying to figure out what's going on with our apps, be it in development or producton, we often first turn to `puts`
|
8
6
|
statements, like so:
|
@@ -16,9 +14,7 @@ else
|
|
16
14
|
end
|
17
15
|
```
|
18
16
|
|
19
|
-
Because of the way `system` or the backtick operator work, this sort of debugging isn't terribly helpful. It's also hard to turn
|
20
|
-
off: you either delete the lines (possibly adding them back later when things go wrong again), or comment them out, which
|
21
|
-
leads to hard-to-follow code and potentially misleading messages.
|
17
|
+
Because of the way `system` or the backtick operator work, this sort of debugging isn't terribly helpful. It's also hard to turn off: you either delete the lines (possibly adding them back later when things go wrong again), or comment them out, which leads to hard-to-follow code and potentially misleading messages.
|
22
18
|
|
23
19
|
Instead, you should use logging, and Methadone bakes logging right in.
|
24
20
|
|
@@ -40,29 +36,19 @@ else
|
|
40
36
|
end
|
41
37
|
```
|
42
38
|
|
43
|
-
At runtime, you can change the log level, meaning you can hide the `debug` statement without changing your code. You may have
|
44
|
-
noticed in our tutorial app, `fullstop`, that the flag `--log-level` was shown as an option. The method `use_log_level_option`
|
45
|
-
enables this flag. This means that you don't have to do *anything additional* to get full control over your logging.
|
39
|
+
At runtime, you can change the log level, meaning you can hide the `debug` statement without changing your code. You may have noticed in our tutorial app, `fullstop`, that the flag `--log-level` was shown as an option. The method `use_log_level_option` enables this flag. This means that you don't have to do *anything additional* to get full control over your logging.
|
46
40
|
|
47
|
-
Methadone goes beyond this, however, and makes heavy use of the logger in the `Methadone::SH` module. This module assumes that
|
48
|
-
`Methadone::CLILogging` is mixed in (or, more specifically, assumes a method `logger` which returns a `Logger`), and all
|
49
|
-
interaction with external commands via `sh` is logged in a useful and appropriate manner.
|
41
|
+
Methadone goes beyond this, however, and makes heavy use of the logger in the `Methadone::SH` module. This module assumes that `Methadone::CLILogging` is mixed in (or, more specifically, assumes a method `logger` which returns a `Logger`), and all interaction with external commands via `sh` is logged in a useful and appropriate manner.
|
50
42
|
|
51
43
|
By default, `sh` will log the full command it executes at debug level. It will also capture the standard output and standard error of the commands you run and examine the exit code.
|
52
44
|
|
53
|
-
Any output to the standard error device is logged as a warning; error output from commands you call is important and should be
|
54
|
-
examined.
|
45
|
+
Any output to the standard error device is logged as a warning; error output from commands you call is important and should be examined.
|
55
46
|
|
56
47
|
If the exit code of the command is zero, the standard output is logged at debug level, otherwise it will be logged at info level.
|
57
48
|
|
58
|
-
What this means is that you can dial up logging to debug level in production to see everything your app is doing, but can
|
59
|
-
generally keep the log level higher, to reduce log noise. This is a powerful tool for debugging your apps, and it doesn't
|
60
|
-
require any code changes.
|
49
|
+
What this means is that you can dial up logging to debug level in production to see everything your app is doing, but can generally keep the log level higher, to reduce log noise. This is a powerful tool for debugging your apps, and it doesn't require any code changes.
|
61
50
|
|
62
|
-
Let's enhance `bin/fullstop` to log more things, and examine what's going on. First, we'll add an info message to our executable
|
63
|
-
that indicates that everything worked. Generally, you don't want to add noisy messages like this (see [my book][clibook] for a
|
64
|
-
deeper discussion as to why), however for demonstration purposes, it should be OK. Here's just the `main` block with our
|
65
|
-
additional logging:
|
51
|
+
Let's enhance `bin/fullstop` to log more things, and examine what's going on. First, we'll add an info message to our executable that indicates that everything worked. Generally, you don't want to add noisy messages like this (see [my book][clibook] for a deeper discussion as to why), however for demonstration purposes, it should be OK. Here's just the `main` block with our additional logging:
|
66
52
|
|
67
53
|
```ruby
|
68
54
|
main do |repo_url|
|
@@ -78,8 +64,7 @@ main do |repo_url|
|
|
78
64
|
end
|
79
65
|
```
|
80
66
|
|
81
|
-
We'll also add some debug logging to `Repo`. This can be useful since we're doing some filename manipulation with regular
|
82
|
-
expressions and it might help to see what's going on if we encouter an odd bug:
|
67
|
+
We'll also add some debug logging to `Repo`. This can be useful since we're doing some filename manipulation with regular expressions and it might help to see what's going on if we encouter an odd bug:
|
83
68
|
|
84
69
|
```ruby
|
85
70
|
module Fullstop
|
@@ -129,8 +114,7 @@ $ HOME=/tmp/fake-home bundle exec bin/fullstop file:///tmp/dotfiles.git
|
|
129
114
|
Dotfiles symlinked
|
130
115
|
```
|
131
116
|
|
132
|
-
As we can see, things went normally and we saw just our info message. By default, the Methadone logger is set at info level.
|
133
|
-
Let's try it again at debug level:
|
117
|
+
As we can see, things went normally and we saw just our info message. By default, the Methadone logger is set at info level. Let's try it again at debug level:
|
134
118
|
|
135
119
|
```sh
|
136
120
|
$ rm -rf /tmp/fake-home ; mkdir /tmp/fake-home/
|
@@ -160,9 +144,7 @@ D, [2012-02-13T21:11:05.950866 #49986] DEBUG -- : Yielding .inputrc
|
|
160
144
|
D, [2012-02-13T21:11:05.950968 #49986] DEBUG -- : Yielding .vimrc
|
161
145
|
I, [2012-02-13T21:11:05.951086 #49986] INFO -- : Dotfiles symlinked
|
162
146
|
```
|
163
|
-
The format has changed. Methadone reasons that if you are showing output to a terminal TTY, the user will not need or want to
|
164
|
-
see the logging level of each message nor the timestamp. However, if the user has redirected the output to a file, this
|
165
|
-
information becomes much more useful.
|
147
|
+
The format has changed. Methadone reasons that if you are showing output to a terminal TTY, the user will not need or want to see the logging level of each message nor the timestamp. However, if the user has redirected the output to a file, this information becomes much more useful.
|
166
148
|
|
167
149
|
Now, let's run the app again, but without "resetting" our fake home directory in `/tmp/fake-home`.
|
168
150
|
|
@@ -196,18 +178,13 @@ messages to potentially many places. How does this work?
|
|
196
178
|
|
197
179
|
## Methadone's Special Logger
|
198
180
|
|
199
|
-
The logger used by default in `Methadone::CLILogging` is a `Methadone::CLILogger`. This
|
200
|
-
is a special logger designed for command-line apps. By default, any message logged at warn or higher will go to the standard
|
201
|
-
error stream. Messages logged at info and debug will go to the standard output stream. This allows you to fluently communicate
|
202
|
-
things to the user and have them go to the appropriate place.
|
181
|
+
The logger used by default in `Methadone::CLILogging` is a `Methadone::CLILogger`. This is a special logger designed for command-line apps. By default, any message logged at warn or higher will go to the standard error stream. Messages logged at info and debug will go to the standard output stream. This allows you to fluently communicate things to the user and have them go to the appropriate place.
|
203
182
|
|
204
|
-
Further, when your app is run at a terminal, these messages are unformatted. When your apps output is redirected somewhere, the
|
205
|
-
messages are formatted with date and time stamps, as you'd expect in a log.
|
183
|
+
Further, when your app is run at a terminal, these messages are unformatted. When your apps output is redirected somewhere, the messages are formatted with date and time stamps, as you'd expect in a log.
|
206
184
|
|
207
|
-
Note that if you want a normal Ruby logger (or want to use the Rails logger in a Rails environment), you can still get the
|
208
|
-
|
209
|
-
|
210
|
-
Methadone's fancy logger.
|
185
|
+
Note that if you want a normal Ruby logger (or want to use the Rails logger in a Rails environment), you can still get the benefits of `Methadone::CLILogging` without being required to use the `Methadone::CLILogger`. I've used this to great affect to use the thread-safe [Log4r][log4r] logger in a JRuby app.
|
186
|
+
|
187
|
+
Let's change `bin/fullstop` to use a plain Ruby Logger instead of Methadone's fancy logger.
|
211
188
|
|
212
189
|
We just need to change one line in `bin/fullstop`, to call `change_logger` inside our `main` block:
|
213
190
|
|
@@ -226,8 +203,7 @@ main do |repo_url|
|
|
226
203
|
end
|
227
204
|
```
|
228
205
|
|
229
|
-
All other files stay as they are. Now, let's re-run our app, first cleaning up the fake home directory, and then immediately
|
230
|
-
running the app again to see errors.
|
206
|
+
All other files stay as they are. Now, let's re-run our app, first cleaning up the fake home directory, and then immediately running the app again to see errors.
|
231
207
|
|
232
208
|
```sh
|
233
209
|
$ rm -rf /tmp/fake-home ; mkdir /tmp/fake-home/
|
@@ -259,19 +235,13 @@ output.
|
|
259
235
|
|
260
236
|
## Exceptions
|
261
237
|
|
262
|
-
We've already seen the use of `exit_now!` to abort our app and show the user an error message. `exit_now!` is implemented to
|
263
|
-
raise a `Methadone::Error`, but we could've just as easily raised a `StandardError` or `RuntimeError` ourselves. The result
|
264
|
-
would be the same: Methadone would show the user just the error message and exit nonzero.
|
238
|
+
We've already seen the use of `exit_now!` to abort our app and show the user an error message. `exit_now!` is implemented to raise a `Methadone::Error`, but we could've just as easily raised a `StandardError` or `RuntimeError` ourselves. The result would be the same: Methadone would show the user just the error message and exit nonzero.
|
265
239
|
|
266
|
-
Methadone traps all exceptions, so that users never see a backtrace. Generally, this is what you want, because it allows you to
|
267
|
-
write your code without complex exit logic and you don't need to worry about a bad user experience by letting stack traces leak
|
268
|
-
through to the output. In fact, the method `go!` that we've seen at the bottom of our executables handles this.
|
240
|
+
Methadone traps all exceptions, so that users never see a backtrace. Generally, this is what you want, because it allows you to write your code without complex exit logic and you don't need to worry about a bad user experience by letting stack traces leak through to the output. In fact, the method `go!` that we've seen at the bottom of our executables handles this.
|
269
241
|
|
270
|
-
There are times, however, when you want to see these traces. When writing and debugging your app, the exception backtraces are
|
271
|
-
crucial for identifying where things went wrong.
|
242
|
+
There are times, however, when you want to see these traces. When writing and debugging your app, the exception backtraces are crucial for identifying where things went wrong.
|
272
243
|
|
273
|
-
All Methadone apps look for the environment variable `DEBUG` and, if it's set to "true", will show the stack trace on errors
|
274
|
-
instead of hiding it. Let's see it work with `bin/fullstop`. We've restored it back to use a `Methadone::CLILogger`, and we can now see how `DEBUG` affects the output:
|
244
|
+
All Methadone apps look for the environment variable `DEBUG` and, if it's set to "true", will show the stack trace on errors instead of hiding it. Let's see it work with `bin/fullstop`. We've restored it back to use a `Methadone::CLILogger`, and we can now see how `DEBUG` affects the output:
|
275
245
|
|
276
246
|
```sh
|
277
247
|
$ HOME=/tmp/fake-home bundle exec bin/fullstop --log-level=debug file:///tmp/dotfiles.git
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: methadone
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.0.
|
4
|
+
version: 1.0.0.rc5
|
5
5
|
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-02-
|
12
|
+
date: 2012-02-28 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
16
|
-
requirement: &
|
16
|
+
requirement: &70123273030540 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70123273030540
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rspec-expectations
|
27
|
-
requirement: &
|
27
|
+
requirement: &70123273030040 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ~>
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '2.6'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70123273030040
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rake
|
38
|
-
requirement: &
|
38
|
+
requirement: &70123273029560 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70123273029560
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: rdoc
|
49
|
-
requirement: &
|
49
|
+
requirement: &70123273028900 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ~>
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: '3.9'
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70123273028900
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: cucumber
|
60
|
-
requirement: &
|
60
|
+
requirement: &70123273028320 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ~>
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: 1.1.1
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70123273028320
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: aruba
|
71
|
-
requirement: &
|
71
|
+
requirement: &70123273027800 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ! '>='
|
@@ -76,10 +76,10 @@ dependencies:
|
|
76
76
|
version: '0'
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *70123273027800
|
80
80
|
- !ruby/object:Gem::Dependency
|
81
81
|
name: simplecov
|
82
|
-
requirement: &
|
82
|
+
requirement: &70123273027220 !ruby/object:Gem::Requirement
|
83
83
|
none: false
|
84
84
|
requirements:
|
85
85
|
- - ~>
|
@@ -87,10 +87,10 @@ dependencies:
|
|
87
87
|
version: '0.5'
|
88
88
|
type: :development
|
89
89
|
prerelease: false
|
90
|
-
version_requirements: *
|
90
|
+
version_requirements: *70123273027220
|
91
91
|
- !ruby/object:Gem::Dependency
|
92
92
|
name: clean_test
|
93
|
-
requirement: &
|
93
|
+
requirement: &70123273026680 !ruby/object:Gem::Requirement
|
94
94
|
none: false
|
95
95
|
requirements:
|
96
96
|
- - ~>
|
@@ -98,10 +98,10 @@ dependencies:
|
|
98
98
|
version: '0.10'
|
99
99
|
type: :development
|
100
100
|
prerelease: false
|
101
|
-
version_requirements: *
|
101
|
+
version_requirements: *70123273026680
|
102
102
|
- !ruby/object:Gem::Dependency
|
103
103
|
name: mocha
|
104
|
-
requirement: &
|
104
|
+
requirement: &70123273026220 !ruby/object:Gem::Requirement
|
105
105
|
none: false
|
106
106
|
requirements:
|
107
107
|
- - ! '>='
|
@@ -109,7 +109,7 @@ dependencies:
|
|
109
109
|
version: '0'
|
110
110
|
type: :development
|
111
111
|
prerelease: false
|
112
|
-
version_requirements: *
|
112
|
+
version_requirements: *70123273026220
|
113
113
|
description: Methadone provides a lot of small but useful features for developing
|
114
114
|
a command-line app, including an opinionated bootstrapping process, some helpful
|
115
115
|
cucumber steps, and some classes to bridge logging and output into a simple, unified,
|