methadone 1.0.0.rc2 → 1.0.0.rc3
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,220 @@
|
|
1
|
+
# Refactoring
|
2
|
+
|
3
|
+
Refactoring is an important step in TDD, and a Methadone-powered app works just as well with the code all jumbled inside our
|
4
|
+
executable as it would with things nicely organized in classes. Since we'll distribute our app with RubyGems, it will all work
|
5
|
+
out at runtime. This means that there's no additional complexity to organizing our code into classes that live inside the `lib`
|
6
|
+
directory.
|
7
|
+
|
8
|
+
Currently, our `main` block looks like this:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
main do |repo_url|
|
12
|
+
|
13
|
+
Dir.chdir options['checkout-dir'] do
|
14
|
+
basedir = repo_url.split(/\//)[-1].gsub(/\.git$/,'')
|
15
|
+
if options[:force] && Dir.exists?(basedir)
|
16
|
+
warn "deleting #{basedir} before cloning"
|
17
|
+
FileUtils.rm_rf basedir
|
18
|
+
end
|
19
|
+
if sh("git clone #{repo_url}") == 0
|
20
|
+
Dir.entries(basedir).each do |file|
|
21
|
+
next if file == '.' || file == '..' || file == '.git'
|
22
|
+
source_file = File.join(basedir,file)
|
23
|
+
FileUtils.rm(file) if File.exists?(file) && options[:force]
|
24
|
+
FileUtils.ln_s source_file,'.'
|
25
|
+
end
|
26
|
+
else
|
27
|
+
exit_now!("checkout dir already exists, use --force to overwrite")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
Let's use method extraction to clean this up before we worry about classes. This exercise will help us identify classes we can
|
34
|
+
create later.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
main do |repo_url|
|
38
|
+
Dir.chdir options['checkout-dir'] do
|
39
|
+
repo_dir = clone_repo(repo_url,options[:force])
|
40
|
+
files_in(repo_dir) do |file|
|
41
|
+
link_file(repo_dir,file,options[:force])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.link_file(repo_dir,file,overwrite)
|
47
|
+
source_file = File.join(repo_dir,file)
|
48
|
+
FileUtils.rm(file) if File.exists?(file) && overwrite
|
49
|
+
FileUtils.ln_s source_file,'.'
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.files_in(repo_dir)
|
53
|
+
Dir.entries(repo_dir).each do |file|
|
54
|
+
next if file == '.' || file == '..' || file == '.git'
|
55
|
+
yield file
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.clone_repo(repo_url,force)
|
60
|
+
repo_dir = repo_url.split(/\//)[-1].gsub(/\.git$/,'')
|
61
|
+
if force && Dir.exists?(repo_dir)
|
62
|
+
warn "deleting #{repo_dir} before cloning"
|
63
|
+
FileUtils.rm_rf repo_dir
|
64
|
+
end
|
65
|
+
unless sh("git clone #{repo_url}") == 0
|
66
|
+
exit_now!("checkout dir already exists, use --force to overwrite")
|
67
|
+
end
|
68
|
+
repo_dir
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
Our `main` block is now a lot clearer, and, although we have more code, each routine is much more concise and cohesive. Let's
|
73
|
+
run our features to make sure nothing's broken.
|
74
|
+
|
75
|
+
```sh
|
76
|
+
$ rake features
|
77
|
+
Feature: Checkout dotfiles
|
78
|
+
In order to get my dotfiles onto a new computer
|
79
|
+
I want a one-command way to keep them up to date
|
80
|
+
So I don't have to do it myself
|
81
|
+
|
82
|
+
Scenario: Basic UI
|
83
|
+
When I get help for "fullstop"
|
84
|
+
Then the exit status should be 0
|
85
|
+
And the banner should be present
|
86
|
+
And there should be a one line summary of what the app does
|
87
|
+
And the banner should include the version
|
88
|
+
And the banner should document that this app takes options
|
89
|
+
And the banner should document that this app's arguments are:
|
90
|
+
| repo_url | which is required |
|
91
|
+
And the following options should be documented:
|
92
|
+
| --force |
|
93
|
+
| --checkout-dir |
|
94
|
+
| -d |
|
95
|
+
|
96
|
+
Scenario: Happy Path
|
97
|
+
Given a git repo with some dotfiles at "/tmp/dotfiles.git"
|
98
|
+
When I successfully run `fullstop file:///tmp/dotfiles.git`
|
99
|
+
Then the dotfiles should be checked out in the directory "~/dotfiles"
|
100
|
+
And the files in "~/dotfiles" should be symlinked in my home directory
|
101
|
+
|
102
|
+
Scenario: Fail if directory is cloned
|
103
|
+
Given a git repo with some dotfiles at "/tmp/dotfiles.git"
|
104
|
+
And I have my dotfiles cloned and symlinked to "~/dotfiles"
|
105
|
+
And there's a new file in the git repo
|
106
|
+
When I run `fullstop file:///tmp/dotfiles.git`
|
107
|
+
Then the exit status should not be 0
|
108
|
+
And the stderr should contain "checkout dir already exists, use --force to overwrite"
|
109
|
+
|
110
|
+
Scenario: Force overwrite
|
111
|
+
Given a git repo with some dotfiles at "/tmp/dotfiles.git"
|
112
|
+
And I have my dotfiles cloned and symlinked to "~/dotfiles"
|
113
|
+
And there's a new file in the git repo
|
114
|
+
When I successfully run `fullstop --force file:///tmp/dotfiles.git`
|
115
|
+
Then the dotfiles in "~/dotfiles" should be re-cloned
|
116
|
+
And the files in "~/dotfiles" should be symlinked in my home directory
|
117
|
+
|
118
|
+
4 scenarios (4 passed)
|
119
|
+
24 steps (24 passed)
|
120
|
+
0m1.277s
|
121
|
+
```
|
122
|
+
|
123
|
+
Everything's still working, so our refactor was good. We'd like to move a lot of the code out of our executable. This will let
|
124
|
+
us unit test it better, and generally make things a bit easier to organize and understand (as always, [my book][clibook] contains more in-depth discussion of why this is and how to do it). The objects of our app are "Repositories" and "Files". Ruby already has a `File` class, so let's start with "Repository". We'll make one in `lib` that can be cloned and whose files can be listed.
|
125
|
+
|
126
|
+
We'll create a class named `Repo` in `lib/fullstop/repo.rb` that has a factory method, `clone_from`, that will clone and create a
|
127
|
+
`Repo` instance that has a method `repo_dir` exposing the dir where the repo was cloned, and `files` which iterates over each
|
128
|
+
file in the repo, skipping '.' and '..' as before:
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
module Fullstop
|
132
|
+
class Repo
|
133
|
+
|
134
|
+
include Methadone::CLILogging
|
135
|
+
include Methadone::SH
|
136
|
+
include Methadone::Main
|
137
|
+
|
138
|
+
def self.clone_from(repo_url,force=false)
|
139
|
+
repo_dir = repo_url.split(/\//)[-1].gsub(/\.git$/,'')
|
140
|
+
if force && Dir.exists?(repo_dir)
|
141
|
+
warn "deleting #{repo_dir} before cloning"
|
142
|
+
FileUtils.rm_rf repo_dir
|
143
|
+
end
|
144
|
+
unless sh("git clone #{repo_url}") == 0
|
145
|
+
exit_now!("checkout dir already exists, use --force to overwrite")
|
146
|
+
end
|
147
|
+
Repo.new(repo_dir)
|
148
|
+
end
|
149
|
+
|
150
|
+
attr_reader :repo_dir
|
151
|
+
def initialize(repo_dir)
|
152
|
+
@repo_dir = repo_dir
|
153
|
+
end
|
154
|
+
|
155
|
+
def files
|
156
|
+
Dir.entries(@repo_dir).each do |file|
|
157
|
+
next if file == '.' || file == '..' || file == '.git'
|
158
|
+
yield file
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
We'll explain why we included the Methadone modules a bit later. Now, our `bin/fullstop` executable now looks like so:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
#!/usr/bin/env ruby
|
169
|
+
|
170
|
+
require 'optparse'
|
171
|
+
require 'methadone'
|
172
|
+
require 'fullstop'
|
173
|
+
require 'fileutils'
|
174
|
+
|
175
|
+
class App
|
176
|
+
include Methadone::ExitNow
|
177
|
+
include Methadone::CLILogging
|
178
|
+
include Methadone::SH
|
179
|
+
include Fullstop
|
180
|
+
|
181
|
+
main do |repo_url|
|
182
|
+
Dir.chdir options['checkout-dir'] do
|
183
|
+
repo = Repo.clone_from(repo_url,options[:force])
|
184
|
+
repo.files do |file|
|
185
|
+
link_file(repo,file,options[:force])
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def self.link_file(repo,file,overwrite)
|
191
|
+
source_file = File.join(repo.repo_dir,file)
|
192
|
+
FileUtils.rm(file) if File.exists?(file) && overwrite
|
193
|
+
FileUtils.ln_s source_file,'.'
|
194
|
+
end
|
195
|
+
|
196
|
+
version Fullstop::VERSION
|
197
|
+
|
198
|
+
description 'Manages dotfiles from a git repo'
|
199
|
+
|
200
|
+
options['checkout-dir'] = ENV['HOME']
|
201
|
+
on("--force","Overwrite files if they exist")
|
202
|
+
on("-d DIR","--checkout-dir","Where to clone the repo")
|
203
|
+
|
204
|
+
arg :repo_url, "URL to the git repository containing your dotfiles"
|
205
|
+
|
206
|
+
use_log_level_option
|
207
|
+
|
208
|
+
go!
|
209
|
+
end
|
210
|
+
```
|
211
|
+
|
212
|
+
It's now a lot shorter, easier to understand and we have our code in classes, where they can be tested in a fast-running unit
|
213
|
+
test (we'll leave those tests as an exercise to the reader).
|
214
|
+
|
215
|
+
The point of all this is that *none of this matters to Methadone*. When you distribute your app, the code will be available, and
|
216
|
+
thus you can organize it however you'd like.
|
217
|
+
|
218
|
+
You've noticed that we've been punting on a few things that we've seen, most recently, the module `Methadone::CLILogging`. At
|
219
|
+
this point, you know enough to effectively use Methadone to make awesome command-line apps. In the next section, we'll take a
|
220
|
+
closer look at how logging and debugging work with a Methadone app, which will clear up a few details that we've glossed over.
|
@@ -0,0 +1,304 @@
|
|
1
|
+
# Logging & Debugging
|
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.
|
6
|
+
|
7
|
+
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
|
+
statements, like so:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
if sh "rsync /apps/my_app/ backup-srv:/apps/my_app" == 0
|
12
|
+
puts "rsync successful"
|
13
|
+
# do other things
|
14
|
+
else
|
15
|
+
puts "Something, went wrong: #{$!}"
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
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.
|
22
|
+
|
23
|
+
Instead, you should use logging, and Methadone bakes logging right in.
|
24
|
+
|
25
|
+
## Logging
|
26
|
+
|
27
|
+
We've seen the module `Methadone::CLILogging` before. This module can be mixed into any class and does two things:
|
28
|
+
|
29
|
+
* Provides a shared instance of a logger, available via the method `logger`
|
30
|
+
* Provides the convienience methods `debug`, `info`, `warn`, `error`, and `fatal`, which proxy to the underlying logger.
|
31
|
+
|
32
|
+
In a Methadone app, most output should be done using the logger. The above code would look like so:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
if sh "rsync /apps/my_app/ backup-srv:/apps/my_app" == 0
|
36
|
+
debug "rsync successful"
|
37
|
+
# do other things
|
38
|
+
else
|
39
|
+
warn "Something, went wrong: #{$!}"
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
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.
|
46
|
+
|
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.
|
50
|
+
|
51
|
+
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
|
+
|
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.
|
55
|
+
|
56
|
+
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
|
+
|
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.
|
61
|
+
|
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:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
main do |repo_url|
|
69
|
+
Dir.chdir options['checkout-dir'] do
|
70
|
+
repo = Repo.clone_from(repo_url,options[:force])
|
71
|
+
repo.files do |file|
|
72
|
+
link_file(repo,file,options[:force])
|
73
|
+
end
|
74
|
+
end
|
75
|
+
# vvv
|
76
|
+
info "Dotfiles symlinked"
|
77
|
+
# ^^^
|
78
|
+
end
|
79
|
+
```
|
80
|
+
|
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:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
module Fullstop
|
86
|
+
class Repo
|
87
|
+
|
88
|
+
include Methadone::CLILogging
|
89
|
+
include Methadone::SH
|
90
|
+
include Methadone::ExitNow
|
91
|
+
|
92
|
+
def self.clone_from(repo_url,force=false)
|
93
|
+
repo_dir = repo_url.split(/\//)[-1].gsub(/\.git$/,'')
|
94
|
+
# vvv
|
95
|
+
debug "Cloning #{repo_url} into #{repo_dir}"
|
96
|
+
# ^^^
|
97
|
+
if force && Dir.exists?(repo_dir)
|
98
|
+
warn "deleting #{repo_dir} before cloning"
|
99
|
+
FileUtils.rm_rf repo_dir
|
100
|
+
end
|
101
|
+
if sh("git clone #{repo_url}") == 0
|
102
|
+
exit_now!(1,"checkout dir already exists, use --force to overwrite")
|
103
|
+
end
|
104
|
+
Repo.new(repo_dir)
|
105
|
+
end
|
106
|
+
|
107
|
+
attr_reader :repo_dir
|
108
|
+
def initialize(repo_dir)
|
109
|
+
@repo_dir = repo_dir
|
110
|
+
end
|
111
|
+
|
112
|
+
def files
|
113
|
+
Dir.entries(@repo_dir).each do |file|
|
114
|
+
next if file == '.' || file == '..' || file == '.git'
|
115
|
+
# vvv
|
116
|
+
debug "Yielding #{file}"
|
117
|
+
# ^^^
|
118
|
+
yield file
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
```
|
124
|
+
|
125
|
+
Let's run our app on the command-line:
|
126
|
+
|
127
|
+
```sh
|
128
|
+
$ HOME=/tmp/fake-home bundle exec bin/fullstop file:///tmp/dotfiles.git
|
129
|
+
Dotfiles symlinked
|
130
|
+
```
|
131
|
+
|
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:
|
134
|
+
|
135
|
+
```sh
|
136
|
+
$ rm -rf /tmp/fake-home ; mkdir /tmp/fake-home/
|
137
|
+
$ HOME=/tmp/fake-home bundle exec bin/fullstop --log-level=debug file:///tmp/dotfiles.git
|
138
|
+
Cloning file:///tmp/dotfiles.git into dotfiles
|
139
|
+
Executing 'git clone file:///tmp/dotfiles.git'
|
140
|
+
Output of 'git clone file:///tmp/dotfiles.git': Cloning into dotfiles...
|
141
|
+
Yielding .bashrc
|
142
|
+
Yielding .exrc
|
143
|
+
Yielding .inputrc
|
144
|
+
Yielding .vimrc
|
145
|
+
Dotfiles symlinked
|
146
|
+
```
|
147
|
+
|
148
|
+
As you can see, we see all the debug messages. Now, let's redirect that to a log file and see what it looks like.
|
149
|
+
|
150
|
+
```sh
|
151
|
+
$ rm -rf /tmp/fake-home ; mkdir /tmp/fake-home/
|
152
|
+
$ HOME=/tmp/fake-home bundle exec bin/fullstop --log-level=debug file:///tmp/dotfiles.git > fullstop.log
|
153
|
+
$ cat fullstop.log
|
154
|
+
D, [2012-02-13T21:11:05.924220 #49986] DEBUG -- : Cloning file:///tmp/dotfiles.git into dotfiles
|
155
|
+
D, [2012-02-13T21:11:05.928311 #49986] DEBUG -- : Executing 'git clone file:///tmp/dotfiles.git'
|
156
|
+
D, [2012-02-13T21:11:05.950333 #49986] DEBUG -- : Output of 'git clone file:///tmp/dotfiles.git': Cloning into dotfiles...
|
157
|
+
D, [2012-02-13T21:11:05.950566 #49986] DEBUG -- : Yielding .bashrc
|
158
|
+
D, [2012-02-13T21:11:05.950752 #49986] DEBUG -- : Yielding .exrc
|
159
|
+
D, [2012-02-13T21:11:05.950866 #49986] DEBUG -- : Yielding .inputrc
|
160
|
+
D, [2012-02-13T21:11:05.950968 #49986] DEBUG -- : Yielding .vimrc
|
161
|
+
I, [2012-02-13T21:11:05.951086 #49986] INFO -- : Dotfiles symlinked
|
162
|
+
```
|
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.
|
166
|
+
|
167
|
+
Now, let's run the app again, but without "resetting" our fake home directory in `/tmp/fake-home`.
|
168
|
+
|
169
|
+
```sh
|
170
|
+
$ HOME=/tmp/fake-home bundle exec bin/fullstop file:///tmp/dotfiles.git
|
171
|
+
Error output of 'git clone file:///tmp/dotfiles.git': fatal: destination path 'dotfiles' already exists and is not an empty directory.
|
172
|
+
Output of 'git clone file:///tmp/dotfiles.git':
|
173
|
+
Error running 'git clone file:///tmp/dotfiles.git'
|
174
|
+
checkout dir already exists, use --force to overwrite
|
175
|
+
```
|
176
|
+
|
177
|
+
We get several error messages. Let's redirect the standard output to a file and try again.
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
$ HOME=/tmp/fake-home bundle exec bin/fullstop file:///tmp/dotfiles.git > fullstop.log
|
181
|
+
Error output of 'git clone file:///tmp/dotfiles.git': fatal: destination path 'dotfiles' already exists and is not an empty directory.
|
182
|
+
Error running 'git clone file:///tmp/dotfiles.git'
|
183
|
+
checkout dir already exists, use --force to overwrite
|
184
|
+
$ cat fullstop.log
|
185
|
+
W, [2012-02-13T21:13:29.819867 #50061] WARN -- : Error output of 'git clone file:///tmp/dotfiles.git': fatal: destination path 'dotfiles' already exists and is not an empty directory.
|
186
|
+
I, [2012-02-13T21:13:29.820076 #50061] INFO -- : Output of 'git clone file:///tmp/dotfiles.git':
|
187
|
+
W, [2012-02-13T21:13:29.820120 #50061] WARN -- : Error running 'git clone file:///tmp/dotfiles.git'
|
188
|
+
E, [2012-02-13T21:13:29.820251 #50061] ERROR -- : checkout dir already exists, use --force to overwrite
|
189
|
+
```
|
190
|
+
|
191
|
+
As you can see, our terminal (which is only showing the standard error output) shows us only the warning and log messages, and
|
192
|
+
ourlog file has *all* the messages. This gives you a *lot* of flexibility.
|
193
|
+
|
194
|
+
The normal Ruby `Logger` doesn't have quite these smarts; it produces messages onto one `IO` device. Methadone is sending
|
195
|
+
messages to potentially many places. How does this work?
|
196
|
+
|
197
|
+
## Methadone's Special Logger
|
198
|
+
|
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.
|
203
|
+
|
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.
|
206
|
+
|
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
|
+
benefits of `Methadone::CLILogging` without being required to use the `Methadone::CLILogger`. I've used this to great affect to
|
209
|
+
use the thread-safe [Log4r][log4r] logger in a JRuby app. Let's change `bin/fullstop` to use a plain Ruby Logger instead of
|
210
|
+
Methadone's fancy logger.
|
211
|
+
|
212
|
+
We just need to change one line in `bin/fullstop`, to call `change_logger` inside our `main` block:
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
main do |repo_url|
|
216
|
+
# vvv
|
217
|
+
change_logger(Logger.new(STDERR))
|
218
|
+
# ^^^
|
219
|
+
Dir.chdir options['checkout-dir'] do
|
220
|
+
repo = Repo.clone_from(repo_url,options[:force])
|
221
|
+
repo.files do |file|
|
222
|
+
link_file(repo,file,options[:force])
|
223
|
+
end
|
224
|
+
end
|
225
|
+
info "Dotfiles symlinked"
|
226
|
+
end
|
227
|
+
```
|
228
|
+
|
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.
|
231
|
+
|
232
|
+
```sh
|
233
|
+
$ rm -rf /tmp/fake-home ; mkdir /tmp/fake-home/
|
234
|
+
$ HOME=/tmp/fake-home bundle exec bin/fullstop --log-level=debug file:///tmp/dotfiles.git > fullstop.log
|
235
|
+
D, [2012-02-13T21:18:11.492004 #50317] DEBUG -- : Cloning file:///tmp/dotfiles.git into dotfiles
|
236
|
+
D, [2012-02-13T21:18:11.492125 #50317] DEBUG -- : Executing 'git clone file:///tmp/dotfiles.git'
|
237
|
+
D, [2012-02-13T21:18:11.513846 #50317] DEBUG -- : Output of 'git clone file:///tmp/dotfiles.git': Cloning into dotfiles...
|
238
|
+
D, [2012-02-13T21:18:11.514113 #50317] DEBUG -- : Yielding .bashrc
|
239
|
+
D, [2012-02-13T21:18:11.514339 #50317] DEBUG -- : Yielding .exrc
|
240
|
+
D, [2012-02-13T21:18:11.514516 #50317] DEBUG -- : Yielding .inputrc
|
241
|
+
D, [2012-02-13T21:18:11.514719 #50317] DEBUG -- : Yielding .vimrc
|
242
|
+
I, [2012-02-13T21:18:11.514899 #50317] INFO -- : Dotfiles symlinked
|
243
|
+
HOME=/tmp/fake-home bundle exec bin/fullstop --log-level=debug file:///tmp/dotfiles.git > fullstop.log
|
244
|
+
D, [2012-02-13T21:18:17.181995 #50348] DEBUG -- : Cloning file:///tmp/dotfiles.git into dotfiles
|
245
|
+
D, [2012-02-13T21:18:17.182112 #50348] DEBUG -- : Executing 'git clone file:///tmp/dotfiles.git'
|
246
|
+
W, [2012-02-13T21:18:17.186447 #50348] WARN -- : Error output of 'git clone file:///tmp/dotfiles.git': fatal: destination path 'dotfiles' already exists and is not an empty directory.
|
247
|
+
I, [2012-02-13T21:18:17.186579 #50348] INFO -- : Output of 'git clone file:///tmp/dotfiles.git':
|
248
|
+
W, [2012-02-13T21:18:17.186621 #50348] WARN -- : Error running 'git clone file:///tmp/dotfiles.git'
|
249
|
+
E, [2012-02-13T21:18:17.186798 #50348] ERROR -- : checkout dir already exists, use --force to overwrite
|
250
|
+
```
|
251
|
+
|
252
|
+
As you can see, our output is the default for a Ruby `Logger`, and there's no special formatting. You'll also notice that, even
|
253
|
+
though we redirected standard out to a log, we still saw all the messages. Since our `Logger` was configured to use the standard
|
254
|
+
error stream, our terminal gets all the messages.
|
255
|
+
|
256
|
+
Note that all of our code, include code in `lib/fullstop/repo.rb` uses this logger via the convienience methods provided by
|
257
|
+
`Methadone::CLILogging`. This is a great way to avoid global variables, and can provide central control over your logging and
|
258
|
+
output.
|
259
|
+
|
260
|
+
## Exceptions
|
261
|
+
|
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.
|
265
|
+
|
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.
|
269
|
+
|
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.
|
272
|
+
|
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:
|
275
|
+
|
276
|
+
```sh
|
277
|
+
$ HOME=/tmp/fake-home bundle exec bin/fullstop --log-level=debug file:///tmp/dotfiles.git
|
278
|
+
Cloning file:///tmp/dotfiles.git into dotfiles
|
279
|
+
Executing 'git clone file:///tmp/dotfiles.git'
|
280
|
+
Error output of 'git clone file:///tmp/dotfiles.git': fatal: destination path 'dotfiles' already exists and is not an empty directory.
|
281
|
+
Output of 'git clone file:///tmp/dotfiles.git':
|
282
|
+
Error running 'git clone file:///tmp/dotfiles.git'
|
283
|
+
checkout dir already exists, use --force to overwrite
|
284
|
+
$ DEBUG=true HOME=/tmp/fake-home bundle exec bin/fullstop --log-level=debug file:///tmp/dotfiles.git
|
285
|
+
Cloning file:///tmp/dotfiles.git into dotfiles
|
286
|
+
Executing 'git clone file:///tmp/dotfiles.git'
|
287
|
+
Error output of 'git clone file:///tmp/dotfiles.git': fatal: destination path 'dotfiles' already exists and is not an empty directory.
|
288
|
+
Output of 'git clone file:///tmp/dotfiles.git':
|
289
|
+
Error running 'git clone file:///tmp/dotfiles.git'
|
290
|
+
/Users/davec/.rvm/gems/ruby-1.9.3-p0@methadone-tutorial/gems/methadone-1.0.0.rc2/lib/methadone/exit_now.rb:21:in `exit_now!': checkout dir already exists, use --force to overwrite (Methadone::Error)
|
291
|
+
from /Users/davec/Projects/methadone/tutorial/code/fullstop/lib/fullstop/repo.rb:18:in `clone_from'
|
292
|
+
from bin/fullstop:16:in `block (2 levels) in <class:App>'
|
293
|
+
from bin/fullstop:15:in `chdir'
|
294
|
+
from bin/fullstop:15:in `block in <class:App>'
|
295
|
+
from /Users/davec/.rvm/gems/ruby-1.9.3-p0@methadone-tutorial/gems/methadone-1.0.0.rc2/lib/methadone/main.rb:273:in `call'
|
296
|
+
from /Users/davec/.rvm/gems/ruby-1.9.3-p0@methadone-tutorial/gems/methadone-1.0.0.rc2/lib/methadone/main.rb:273:in `call_main'
|
297
|
+
from /Users/davec/.rvm/gems/ruby-1.9.3-p0@methadone-tutorial/gems/methadone-1.0.0.rc2/lib/methadone/main.rb:147:in `go!'
|
298
|
+
from bin/fullstop:42:in `<class:App>'
|
299
|
+
from bin/fullstop:8:in `<main>'
|
300
|
+
```
|
301
|
+
|
302
|
+
Occasionally, you might *always* want the exceptions to leak through. For example, if your app is being run as part of some
|
303
|
+
other system that you don't have precise control over, such as [monit][monit], the backtrace will tell you what went wrong if the
|
304
|
+
system can't properly start your app. In this case, use the method `leak_exceptions` to permanently show the backtrace. Note that this method will only leak exceptions that *aren't* of type `Methadone::Error`.
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# Conclusion
|
2
|
+
|
3
|
+
Wow, what a whirlwind tour we've had! Hopefully, you've gained some confidence in writing command-line apps, and know the
|
4
|
+
features Methadone provides to make it a snap.
|
5
|
+
|
6
|
+
This is just the tip of the iceberg; [my book][clibook] covers the topic of command-line application development in Ruby in much
|
7
|
+
more detail.
|
8
|
+
|
9
|
+
There's also more to Methadone than we've been able to cover here; the Rubydoc is extensive and up-to-date, so spend a few
|
10
|
+
minutes bouncing around to see what else there is. And, feel free to [log issues][issues] or submit pull requests on the
|
11
|
+
[Github page][methadone-github].
|
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.3@methadone-tutorial --create
|