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.
Files changed (40) hide show
  1. data/bin/methadone +2 -2
  2. data/lib/methadone/cucumber.rb +6 -3
  3. data/lib/methadone/error.rb +3 -2
  4. data/lib/methadone/exit_now.rb +25 -0
  5. data/lib/methadone/main.rb +21 -10
  6. data/lib/methadone/sh.rb +19 -4
  7. data/lib/methadone/version.rb +1 -1
  8. data/lib/methadone.rb +1 -0
  9. data/templates/full/Rakefile.erb +1 -1
  10. data/templates/full/features/step_definitions/executable_steps.rb.erb +1 -1
  11. data/test/test_exit_now.rb +37 -0
  12. data/test/test_main.rb +11 -5
  13. data/test/test_sh.rb +17 -0
  14. data/tutorial/.vimrc +6 -0
  15. data/tutorial/1_intro.md +52 -0
  16. data/tutorial/2_bootstrap.md +176 -0
  17. data/tutorial/3_ui.md +349 -0
  18. data/tutorial/4_happy_path.md +437 -0
  19. data/tutorial/5_more_features.md +702 -0
  20. data/tutorial/6_refactor.md +220 -0
  21. data/tutorial/7_logging_debugging.md +304 -0
  22. data/tutorial/8_conclusion.md +11 -0
  23. data/tutorial/code/.rvmrc +1 -0
  24. data/tutorial/code/fullstop/.gitignore +5 -0
  25. data/tutorial/code/fullstop/Gemfile +4 -0
  26. data/tutorial/code/fullstop/LICENSE.txt +202 -0
  27. data/tutorial/code/fullstop/README.rdoc +23 -0
  28. data/tutorial/code/fullstop/Rakefile +31 -0
  29. data/tutorial/code/fullstop/bin/fullstop +43 -0
  30. data/tutorial/code/fullstop/features/fullstop.feature +40 -0
  31. data/tutorial/code/fullstop/features/step_definitions/fullstop_steps.rb +64 -0
  32. data/tutorial/code/fullstop/features/support/env.rb +22 -0
  33. data/tutorial/code/fullstop/fullstop.gemspec +28 -0
  34. data/tutorial/code/fullstop/lib/fullstop/repo.rb +38 -0
  35. data/tutorial/code/fullstop/lib/fullstop/version.rb +3 -0
  36. data/tutorial/code/fullstop/lib/fullstop.rb +2 -0
  37. data/tutorial/code/fullstop/test/tc_something.rb +7 -0
  38. data/tutorial/en.utf-8.add +18 -0
  39. data/tutorial/toc.md +27 -0
  40. 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
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ results.html
3
+ pkg
4
+ html
5
+ .*sw?
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in fullstop.gemspec
4
+ gemspec