methadone 1.0.0.rc5 → 1.0.0.rc6
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/methadone/cli.rb +2 -0
- data/lib/methadone/cli_logging.rb +3 -0
- data/lib/methadone/cucumber.rb +1 -1
- data/lib/methadone/error.rb +8 -1
- data/lib/methadone/execution_strategy/jvm.rb +2 -0
- data/lib/methadone/execution_strategy/mri.rb +2 -0
- data/lib/methadone/execution_strategy/open_3.rb +2 -0
- data/lib/methadone/execution_strategy/open_4.rb +2 -0
- data/lib/methadone/execution_strategy/rbx_open_4.rb +2 -0
- data/lib/methadone/exit_now.rb +18 -3
- data/lib/methadone/main.rb +34 -6
- data/lib/methadone/process_status.rb +45 -0
- data/lib/methadone/sh.rb +52 -29
- data/lib/methadone/version.rb +1 -1
- data/methadone.gemspec +10 -0
- data/test/test_main.rb +20 -0
- data/test/test_sh.rb +104 -3
- metadata +23 -47
- data/tutorial/.vimrc +0 -6
- data/tutorial/1_intro.md +0 -52
- data/tutorial/2_bootstrap.md +0 -174
- data/tutorial/3_ui.md +0 -336
- data/tutorial/4_happy_path.md +0 -405
- data/tutorial/5_more_features.md +0 -693
- data/tutorial/6_refactor.md +0 -220
- data/tutorial/7_logging_debugging.md +0 -274
- data/tutorial/8_conclusion.md +0 -11
- data/tutorial/code/.rvmrc +0 -1
- data/tutorial/code/fullstop/.gitignore +0 -5
- data/tutorial/code/fullstop/Gemfile +0 -4
- data/tutorial/code/fullstop/LICENSE.txt +0 -202
- data/tutorial/code/fullstop/README.rdoc +0 -23
- data/tutorial/code/fullstop/Rakefile +0 -31
- data/tutorial/code/fullstop/bin/fullstop +0 -43
- data/tutorial/code/fullstop/features/fullstop.feature +0 -40
- data/tutorial/code/fullstop/features/step_definitions/fullstop_steps.rb +0 -64
- data/tutorial/code/fullstop/features/support/env.rb +0 -22
- data/tutorial/code/fullstop/fullstop.gemspec +0 -28
- data/tutorial/code/fullstop/lib/fullstop.rb +0 -2
- data/tutorial/code/fullstop/lib/fullstop/repo.rb +0 -38
- data/tutorial/code/fullstop/lib/fullstop/version.rb +0 -3
- data/tutorial/code/fullstop/test/tc_something.rb +0 -7
- data/tutorial/en.utf-8.add +0 -18
- data/tutorial/toc.md +0 -27
data/tutorial/6_refactor.md
DELETED
@@ -1,220 +0,0 @@
|
|
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.
|
@@ -1,274 +0,0 @@
|
|
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, 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.
|
4
|
-
|
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`
|
6
|
-
statements, like so:
|
7
|
-
|
8
|
-
```ruby
|
9
|
-
if sh "rsync /apps/my_app/ backup-srv:/apps/my_app" == 0
|
10
|
-
puts "rsync successful"
|
11
|
-
# do other things
|
12
|
-
else
|
13
|
-
puts "Something, went wrong: #{$!}"
|
14
|
-
end
|
15
|
-
```
|
16
|
-
|
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.
|
18
|
-
|
19
|
-
Instead, you should use logging, and Methadone bakes logging right in.
|
20
|
-
|
21
|
-
## Logging
|
22
|
-
|
23
|
-
We've seen the module `Methadone::CLILogging` before. This module can be mixed into any class and does two things:
|
24
|
-
|
25
|
-
* Provides a shared instance of a logger, available via the method `logger`
|
26
|
-
* Provides the convienience methods `debug`, `info`, `warn`, `error`, and `fatal`, which proxy to the underlying logger.
|
27
|
-
|
28
|
-
In a Methadone app, most output should be done using the logger. The above code would look like so:
|
29
|
-
|
30
|
-
```ruby
|
31
|
-
if sh "rsync /apps/my_app/ backup-srv:/apps/my_app" == 0
|
32
|
-
debug "rsync successful"
|
33
|
-
# do other things
|
34
|
-
else
|
35
|
-
warn "Something, went wrong: #{$!}"
|
36
|
-
end
|
37
|
-
```
|
38
|
-
|
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.
|
40
|
-
|
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.
|
42
|
-
|
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.
|
44
|
-
|
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.
|
46
|
-
|
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.
|
48
|
-
|
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.
|
50
|
-
|
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:
|
52
|
-
|
53
|
-
```ruby
|
54
|
-
main do |repo_url|
|
55
|
-
Dir.chdir options['checkout-dir'] do
|
56
|
-
repo = Repo.clone_from(repo_url,options[:force])
|
57
|
-
repo.files do |file|
|
58
|
-
link_file(repo,file,options[:force])
|
59
|
-
end
|
60
|
-
end
|
61
|
-
# vvv
|
62
|
-
info "Dotfiles symlinked"
|
63
|
-
# ^^^
|
64
|
-
end
|
65
|
-
```
|
66
|
-
|
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:
|
68
|
-
|
69
|
-
```ruby
|
70
|
-
module Fullstop
|
71
|
-
class Repo
|
72
|
-
|
73
|
-
include Methadone::CLILogging
|
74
|
-
include Methadone::SH
|
75
|
-
include Methadone::ExitNow
|
76
|
-
|
77
|
-
def self.clone_from(repo_url,force=false)
|
78
|
-
repo_dir = repo_url.split(/\//)[-1].gsub(/\.git$/,'')
|
79
|
-
# vvv
|
80
|
-
debug "Cloning #{repo_url} into #{repo_dir}"
|
81
|
-
# ^^^
|
82
|
-
if force && Dir.exists?(repo_dir)
|
83
|
-
warn "deleting #{repo_dir} before cloning"
|
84
|
-
FileUtils.rm_rf repo_dir
|
85
|
-
end
|
86
|
-
if sh("git clone #{repo_url}") == 0
|
87
|
-
exit_now!(1,"checkout dir already exists, use --force to overwrite")
|
88
|
-
end
|
89
|
-
Repo.new(repo_dir)
|
90
|
-
end
|
91
|
-
|
92
|
-
attr_reader :repo_dir
|
93
|
-
def initialize(repo_dir)
|
94
|
-
@repo_dir = repo_dir
|
95
|
-
end
|
96
|
-
|
97
|
-
def files
|
98
|
-
Dir.entries(@repo_dir).each do |file|
|
99
|
-
next if file == '.' || file == '..' || file == '.git'
|
100
|
-
# vvv
|
101
|
-
debug "Yielding #{file}"
|
102
|
-
# ^^^
|
103
|
-
yield file
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
```
|
109
|
-
|
110
|
-
Let's run our app on the command-line:
|
111
|
-
|
112
|
-
```sh
|
113
|
-
$ HOME=/tmp/fake-home bundle exec bin/fullstop file:///tmp/dotfiles.git
|
114
|
-
Dotfiles symlinked
|
115
|
-
```
|
116
|
-
|
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:
|
118
|
-
|
119
|
-
```sh
|
120
|
-
$ rm -rf /tmp/fake-home ; mkdir /tmp/fake-home/
|
121
|
-
$ HOME=/tmp/fake-home bundle exec bin/fullstop --log-level=debug file:///tmp/dotfiles.git
|
122
|
-
Cloning file:///tmp/dotfiles.git into dotfiles
|
123
|
-
Executing 'git clone file:///tmp/dotfiles.git'
|
124
|
-
Output of 'git clone file:///tmp/dotfiles.git': Cloning into dotfiles...
|
125
|
-
Yielding .bashrc
|
126
|
-
Yielding .exrc
|
127
|
-
Yielding .inputrc
|
128
|
-
Yielding .vimrc
|
129
|
-
Dotfiles symlinked
|
130
|
-
```
|
131
|
-
|
132
|
-
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.
|
133
|
-
|
134
|
-
```sh
|
135
|
-
$ rm -rf /tmp/fake-home ; mkdir /tmp/fake-home/
|
136
|
-
$ HOME=/tmp/fake-home bundle exec bin/fullstop --log-level=debug file:///tmp/dotfiles.git > fullstop.log
|
137
|
-
$ cat fullstop.log
|
138
|
-
D, [2012-02-13T21:11:05.924220 #49986] DEBUG -- : Cloning file:///tmp/dotfiles.git into dotfiles
|
139
|
-
D, [2012-02-13T21:11:05.928311 #49986] DEBUG -- : Executing 'git clone file:///tmp/dotfiles.git'
|
140
|
-
D, [2012-02-13T21:11:05.950333 #49986] DEBUG -- : Output of 'git clone file:///tmp/dotfiles.git': Cloning into dotfiles...
|
141
|
-
D, [2012-02-13T21:11:05.950566 #49986] DEBUG -- : Yielding .bashrc
|
142
|
-
D, [2012-02-13T21:11:05.950752 #49986] DEBUG -- : Yielding .exrc
|
143
|
-
D, [2012-02-13T21:11:05.950866 #49986] DEBUG -- : Yielding .inputrc
|
144
|
-
D, [2012-02-13T21:11:05.950968 #49986] DEBUG -- : Yielding .vimrc
|
145
|
-
I, [2012-02-13T21:11:05.951086 #49986] INFO -- : Dotfiles symlinked
|
146
|
-
```
|
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.
|
148
|
-
|
149
|
-
Now, let's run the app again, but without "resetting" our fake home directory in `/tmp/fake-home`.
|
150
|
-
|
151
|
-
```sh
|
152
|
-
$ HOME=/tmp/fake-home bundle exec bin/fullstop file:///tmp/dotfiles.git
|
153
|
-
Error output of 'git clone file:///tmp/dotfiles.git': fatal: destination path 'dotfiles' already exists and is not an empty directory.
|
154
|
-
Output of 'git clone file:///tmp/dotfiles.git':
|
155
|
-
Error running 'git clone file:///tmp/dotfiles.git'
|
156
|
-
checkout dir already exists, use --force to overwrite
|
157
|
-
```
|
158
|
-
|
159
|
-
We get several error messages. Let's redirect the standard output to a file and try again.
|
160
|
-
|
161
|
-
```ruby
|
162
|
-
$ HOME=/tmp/fake-home bundle exec bin/fullstop file:///tmp/dotfiles.git > fullstop.log
|
163
|
-
Error output of 'git clone file:///tmp/dotfiles.git': fatal: destination path 'dotfiles' already exists and is not an empty directory.
|
164
|
-
Error running 'git clone file:///tmp/dotfiles.git'
|
165
|
-
checkout dir already exists, use --force to overwrite
|
166
|
-
$ cat fullstop.log
|
167
|
-
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.
|
168
|
-
I, [2012-02-13T21:13:29.820076 #50061] INFO -- : Output of 'git clone file:///tmp/dotfiles.git':
|
169
|
-
W, [2012-02-13T21:13:29.820120 #50061] WARN -- : Error running 'git clone file:///tmp/dotfiles.git'
|
170
|
-
E, [2012-02-13T21:13:29.820251 #50061] ERROR -- : checkout dir already exists, use --force to overwrite
|
171
|
-
```
|
172
|
-
|
173
|
-
As you can see, our terminal (which is only showing the standard error output) shows us only the warning and log messages, and
|
174
|
-
ourlog file has *all* the messages. This gives you a *lot* of flexibility.
|
175
|
-
|
176
|
-
The normal Ruby `Logger` doesn't have quite these smarts; it produces messages onto one `IO` device. Methadone is sending
|
177
|
-
messages to potentially many places. How does this work?
|
178
|
-
|
179
|
-
## Methadone's Special Logger
|
180
|
-
|
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.
|
182
|
-
|
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.
|
184
|
-
|
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.
|
188
|
-
|
189
|
-
We just need to change one line in `bin/fullstop`, to call `change_logger` inside our `main` block:
|
190
|
-
|
191
|
-
```ruby
|
192
|
-
main do |repo_url|
|
193
|
-
# vvv
|
194
|
-
change_logger(Logger.new(STDERR))
|
195
|
-
# ^^^
|
196
|
-
Dir.chdir options['checkout-dir'] do
|
197
|
-
repo = Repo.clone_from(repo_url,options[:force])
|
198
|
-
repo.files do |file|
|
199
|
-
link_file(repo,file,options[:force])
|
200
|
-
end
|
201
|
-
end
|
202
|
-
info "Dotfiles symlinked"
|
203
|
-
end
|
204
|
-
```
|
205
|
-
|
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.
|
207
|
-
|
208
|
-
```sh
|
209
|
-
$ rm -rf /tmp/fake-home ; mkdir /tmp/fake-home/
|
210
|
-
$ HOME=/tmp/fake-home bundle exec bin/fullstop --log-level=debug file:///tmp/dotfiles.git > fullstop.log
|
211
|
-
D, [2012-02-13T21:18:11.492004 #50317] DEBUG -- : Cloning file:///tmp/dotfiles.git into dotfiles
|
212
|
-
D, [2012-02-13T21:18:11.492125 #50317] DEBUG -- : Executing 'git clone file:///tmp/dotfiles.git'
|
213
|
-
D, [2012-02-13T21:18:11.513846 #50317] DEBUG -- : Output of 'git clone file:///tmp/dotfiles.git': Cloning into dotfiles...
|
214
|
-
D, [2012-02-13T21:18:11.514113 #50317] DEBUG -- : Yielding .bashrc
|
215
|
-
D, [2012-02-13T21:18:11.514339 #50317] DEBUG -- : Yielding .exrc
|
216
|
-
D, [2012-02-13T21:18:11.514516 #50317] DEBUG -- : Yielding .inputrc
|
217
|
-
D, [2012-02-13T21:18:11.514719 #50317] DEBUG -- : Yielding .vimrc
|
218
|
-
I, [2012-02-13T21:18:11.514899 #50317] INFO -- : Dotfiles symlinked
|
219
|
-
HOME=/tmp/fake-home bundle exec bin/fullstop --log-level=debug file:///tmp/dotfiles.git > fullstop.log
|
220
|
-
D, [2012-02-13T21:18:17.181995 #50348] DEBUG -- : Cloning file:///tmp/dotfiles.git into dotfiles
|
221
|
-
D, [2012-02-13T21:18:17.182112 #50348] DEBUG -- : Executing 'git clone file:///tmp/dotfiles.git'
|
222
|
-
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.
|
223
|
-
I, [2012-02-13T21:18:17.186579 #50348] INFO -- : Output of 'git clone file:///tmp/dotfiles.git':
|
224
|
-
W, [2012-02-13T21:18:17.186621 #50348] WARN -- : Error running 'git clone file:///tmp/dotfiles.git'
|
225
|
-
E, [2012-02-13T21:18:17.186798 #50348] ERROR -- : checkout dir already exists, use --force to overwrite
|
226
|
-
```
|
227
|
-
|
228
|
-
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
|
229
|
-
though we redirected standard out to a log, we still saw all the messages. Since our `Logger` was configured to use the standard
|
230
|
-
error stream, our terminal gets all the messages.
|
231
|
-
|
232
|
-
Note that all of our code, include code in `lib/fullstop/repo.rb` uses this logger via the convienience methods provided by
|
233
|
-
`Methadone::CLILogging`. This is a great way to avoid global variables, and can provide central control over your logging and
|
234
|
-
output.
|
235
|
-
|
236
|
-
## Exceptions
|
237
|
-
|
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.
|
239
|
-
|
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.
|
241
|
-
|
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.
|
243
|
-
|
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:
|
245
|
-
|
246
|
-
```sh
|
247
|
-
$ HOME=/tmp/fake-home bundle exec bin/fullstop --log-level=debug file:///tmp/dotfiles.git
|
248
|
-
Cloning file:///tmp/dotfiles.git into dotfiles
|
249
|
-
Executing 'git clone file:///tmp/dotfiles.git'
|
250
|
-
Error output of 'git clone file:///tmp/dotfiles.git': fatal: destination path 'dotfiles' already exists and is not an empty directory.
|
251
|
-
Output of 'git clone file:///tmp/dotfiles.git':
|
252
|
-
Error running 'git clone file:///tmp/dotfiles.git'
|
253
|
-
checkout dir already exists, use --force to overwrite
|
254
|
-
$ DEBUG=true HOME=/tmp/fake-home bundle exec bin/fullstop --log-level=debug file:///tmp/dotfiles.git
|
255
|
-
Cloning file:///tmp/dotfiles.git into dotfiles
|
256
|
-
Executing 'git clone file:///tmp/dotfiles.git'
|
257
|
-
Error output of 'git clone file:///tmp/dotfiles.git': fatal: destination path 'dotfiles' already exists and is not an empty directory.
|
258
|
-
Output of 'git clone file:///tmp/dotfiles.git':
|
259
|
-
Error running 'git clone file:///tmp/dotfiles.git'
|
260
|
-
/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)
|
261
|
-
from /Users/davec/Projects/methadone/tutorial/code/fullstop/lib/fullstop/repo.rb:18:in `clone_from'
|
262
|
-
from bin/fullstop:16:in `block (2 levels) in <class:App>'
|
263
|
-
from bin/fullstop:15:in `chdir'
|
264
|
-
from bin/fullstop:15:in `block in <class:App>'
|
265
|
-
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'
|
266
|
-
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'
|
267
|
-
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!'
|
268
|
-
from bin/fullstop:42:in `<class:App>'
|
269
|
-
from bin/fullstop:8:in `<main>'
|
270
|
-
```
|
271
|
-
|
272
|
-
Occasionally, you might *always* want the exceptions to leak through. For example, if your app is being run as part of some
|
273
|
-
other system that you don't have precise control over, such as [monit][monit], the backtrace will tell you what went wrong if the
|
274
|
-
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`.
|