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.
Files changed (44) hide show
  1. data/lib/methadone/cli.rb +2 -0
  2. data/lib/methadone/cli_logging.rb +3 -0
  3. data/lib/methadone/cucumber.rb +1 -1
  4. data/lib/methadone/error.rb +8 -1
  5. data/lib/methadone/execution_strategy/jvm.rb +2 -0
  6. data/lib/methadone/execution_strategy/mri.rb +2 -0
  7. data/lib/methadone/execution_strategy/open_3.rb +2 -0
  8. data/lib/methadone/execution_strategy/open_4.rb +2 -0
  9. data/lib/methadone/execution_strategy/rbx_open_4.rb +2 -0
  10. data/lib/methadone/exit_now.rb +18 -3
  11. data/lib/methadone/main.rb +34 -6
  12. data/lib/methadone/process_status.rb +45 -0
  13. data/lib/methadone/sh.rb +52 -29
  14. data/lib/methadone/version.rb +1 -1
  15. data/methadone.gemspec +10 -0
  16. data/test/test_main.rb +20 -0
  17. data/test/test_sh.rb +104 -3
  18. metadata +23 -47
  19. data/tutorial/.vimrc +0 -6
  20. data/tutorial/1_intro.md +0 -52
  21. data/tutorial/2_bootstrap.md +0 -174
  22. data/tutorial/3_ui.md +0 -336
  23. data/tutorial/4_happy_path.md +0 -405
  24. data/tutorial/5_more_features.md +0 -693
  25. data/tutorial/6_refactor.md +0 -220
  26. data/tutorial/7_logging_debugging.md +0 -274
  27. data/tutorial/8_conclusion.md +0 -11
  28. data/tutorial/code/.rvmrc +0 -1
  29. data/tutorial/code/fullstop/.gitignore +0 -5
  30. data/tutorial/code/fullstop/Gemfile +0 -4
  31. data/tutorial/code/fullstop/LICENSE.txt +0 -202
  32. data/tutorial/code/fullstop/README.rdoc +0 -23
  33. data/tutorial/code/fullstop/Rakefile +0 -31
  34. data/tutorial/code/fullstop/bin/fullstop +0 -43
  35. data/tutorial/code/fullstop/features/fullstop.feature +0 -40
  36. data/tutorial/code/fullstop/features/step_definitions/fullstop_steps.rb +0 -64
  37. data/tutorial/code/fullstop/features/support/env.rb +0 -22
  38. data/tutorial/code/fullstop/fullstop.gemspec +0 -28
  39. data/tutorial/code/fullstop/lib/fullstop.rb +0 -2
  40. data/tutorial/code/fullstop/lib/fullstop/repo.rb +0 -38
  41. data/tutorial/code/fullstop/lib/fullstop/version.rb +0 -3
  42. data/tutorial/code/fullstop/test/tc_something.rb +0 -7
  43. data/tutorial/en.utf-8.add +0 -18
  44. data/tutorial/toc.md +0 -27
@@ -1,693 +0,0 @@
1
- # Adding Features
2
-
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
-
6
- There's a lot of features we can add and error cases we can handle, for example:
7
-
8
- * The app will blow up if the git repo is already cloned
9
- * The app might blow up if the files are already symlinked
10
- * The app won't symlink new files
11
- * The app checks out your dotfiles repo in your home directory
12
- * The app uses symlinks, but we might want copies instead
13
-
14
- You can probably think of even more features. To demonstrate how Methadone works, we're going to add these features:
15
-
16
- * a "force" switch that will blow away the git repo and re-clone it, called `--force`
17
- * a "location" flag that will allow us to control where the repo gets cloned, called `--checkout-dir` (which we'll also make available via `-d` as a mnemonic for "directory". See [my book][clibook] for an in-depth discussion on why you should provide long-form options along with short-form options)
18
-
19
- Since we're working outside-in, we'll first create the user interface by modifying our UI scenario:
20
-
21
- ```cucumber
22
- Feature: Checkout dotfiles
23
- In order to get my dotfiles onto a new computer
24
- I want a one-command way to keep them up to date
25
- So I don't have to do it myself
26
-
27
- Scenario: Basic UI
28
- When I get help for "fullstop"
29
- Then the exit status should be 0
30
- And the banner should be present
31
- And there should be a one line summary of what the app does
32
- And the banner should include the version
33
- And the banner should document that this app takes options
34
- And the banner should document that this app's arguments are:
35
- |repo_url|which is required|
36
- # vvv
37
- And the following options should be documented:
38
- | --force |
39
- | --checkout-dir |
40
- | -d |
41
- # ^^^
42
-
43
- Scenario: Happy Path
44
- Given a git repo with some dotfiles at "/tmp/dotfiles.git"
45
- When I successfully run `fullstop file:///tmp/dotfiles.git`
46
- Then the dotfiles should be checked out in the directory "~/dotfiles"
47
- And the files in "~/dotfiles" should be symlinked in my home directory
48
- ```
49
-
50
- We've added the step "And the following options should be documented". This will allow us to write the code necessary to handle
51
- these options. First, we'll run our tests to see that we have a failing scenario:
52
-
53
- ```sh
54
- $ rake features
55
- Feature: Checkout dotfiles
56
- In order to get my dotfiles onto a new computer
57
- I want a one-command way to keep them up to date
58
- So I don't have to do it myself
59
-
60
- Scenario: Basic UI
61
- When I get help for "fullstop"
62
- Then the exit status should be 0
63
- And the banner should be present
64
- And there should be a one line summary of what the app does
65
- And the banner should include the version
66
- And the banner should document that this app takes options
67
- And the banner should document that this app's arguments are:
68
- | repo_url | which is required |
69
- And the following options should be documented:
70
- | --force |
71
- | --checkout-dir |
72
- | -d |
73
- expected: /^\s*--force\s+\w\w\w+/m
74
- got: "Usage: fullstop [options] repo_url\n\nManages dotfiles from a git repo\n\nv0.0.1\n\nOptions:\n --version Show help/version info\n --log-level LEVEL Set the logging level (debug|info|warn|error|fatal)\n (Default: info)\n" (using =~)
75
- Diff:
76
- @@ -1,2 +1,11 @@
77
- -/^\s*--force\s+\w\w\w+/m
78
- +Usage: fullstop [options] repo_url
79
- +
80
- +Manages dotfiles from a git repo
81
- +
82
- +v0.0.1
83
- +
84
- +Options:
85
- + --version Show help/version info
86
- + --log-level LEVEL Set the logging level (debug|info|warn|error|fatal)
87
- + (Default: info)
88
- (RSpec::Expectations::ExpectationNotMetError)
89
- features/fullstop.feature:15:in `And the following options should be documented:'
90
-
91
- Scenario: Happy Path
92
- Given a git repo with some dotfiles at "/tmp/dotfiles.git"
93
- When I successfully run `fullstop file:///tmp/dotfiles.git`
94
- Then the dotfiles should be checked out in the directory "~/dotfiles"
95
- And the files in "~/dotfiles" should be symlinked in my home directory
96
-
97
- Failing Scenarios:
98
- cucumber features/fullstop.feature:6
99
-
100
- 2 scenarios (1 failed, 1 passed)
101
- 12 steps (1 failed, 11 passed)
102
- 0m0.574s
103
- rake aborted!
104
- Cucumber failed
105
-
106
- Tasks: TOP => features
107
- (See full trace by running task with --trace)
108
- ```
109
-
110
- As you can see, our tests are failing, because the documentation of the existence of our command-line option couldn't be found in
111
- the help output.
112
-
113
- To make this test pass in an `OptionParser`-driven application, you might write something like this:
114
-
115
- ```ruby
116
- options = {
117
- 'checkout-dir' => ENV['HOME'],
118
- }
119
- parser = OptionParser.new do |opts|
120
- opts.on("--force","Force overwriting existing files") do
121
- options[:force] = true
122
- end
123
- opts.on("-d DIR","--checkout-dir",
124
- "Set the location of the checkout dir",
125
- "(default: #{options['checkout-dir']})") do |dir|
126
- options['checkout-dir'] = dir
127
- end
128
- end
129
- parser.parse!
130
- ```
131
-
132
- As we learned earlier, Methadone manages an instance of `OptionParser` for us (and calls `parse!` in its `go!` method), so we could reduce this code to:
133
-
134
- ```ruby
135
- options = {
136
- 'checkout-dir' => ENV['HOME'],
137
- }
138
- opts.on("--force","Force overwriting existing files") do
139
- options[:force] = true
140
- end
141
- opts.on("-d DIR","--checkout-dir",
142
- "Set the location of the checkout dir",
143
- "(default: #{options['checkout-dir']})") do |dir|
144
- options['checkout-dir'] = dir
145
- end
146
- ```
147
-
148
- Methadone *also* manages an options hash for us, so that it can be made available to the `main` block. It's available via the
149
- method `options`. Methadone also provides a method `on` that delegates all of its arguments to the underlying `OptionParser`'s
150
- `on` method. With both of these in mind, we could further reduce the code to:
151
-
152
- ```ruby
153
- options['checkout-dir'] = ENV['HOME']
154
- on("--force","Force overwriting existing files") do
155
- options[:force] = true
156
- end
157
- on("-d DIR","--checkout-dir",
158
- "Set the location of the checkout dir",
159
- "(default: #{options['checkout-dir']})") do |dir|
160
- options['checkout-dir'] = dir
161
- end
162
- ```
163
-
164
- This is still pretty tedious:
165
-
166
- * Both blocks to `on` just set the value in the `options` hash.
167
- * We have to include the default value of `checkout-dir` in the documentation string.
168
-
169
- For apps with a lot of options, this can be a real pain to maintain. Methadone has us covered. The `on` method has more smarts
170
- than just delegating to `OptionParser`. Specifically:
171
-
172
- * If you do *not* pass a block to `on`, it will provide `OptionParser` a block that sets the value of the command-line option inside the `options` hash.
173
- * If there is a default value for your flag (option that takes an argument), it will be included in the help string automatically.
174
-
175
- In other words, we can reduce our option parsing code to these three lines:
176
-
177
- ```ruby
178
- options['checkout-dir'] = ENV['HOME']
179
-
180
- on("--force","Force overwriting of existing files")
181
- on("-d DIR","--checkout-dir","Set the location of the checkout dir")
182
- ```
183
-
184
- That's it! 14 lines become 3.
185
- When `main` executes, the following keys in `options` will be available:
186
-
187
- * `"force"` - true if the user specified `--force`
188
- * `:force` - the same
189
- * `"d"` - the value of the checkout dir (as given to `-d` or `--checkout-dir`), or the default, i.e. never `nil`
190
- * `:d` - the same
191
- * `"checkout-dir"` - the same
192
- * `:'checkout-dir'` - the same
193
-
194
- Notice that each flag is available as a `String` or `Symbol` and that all forms of each option are present in the hash, meaning
195
- you can refer to the options in whichever way makes the code most readable.
196
-
197
- Coming back to our app, let's add this code and see if our test passes. Here's what `bin/fullstop` looks like now:
198
-
199
- ```ruby
200
- #!/usr/bin/env ruby
201
-
202
- require 'optparse'
203
- require 'methadone'
204
- require 'fullstop'
205
- require 'fileutils'
206
-
207
- class App
208
- include Methadone::Main
209
- include Methadone::CLILogging
210
- include Methadone::SH
211
-
212
- main do |repo_url|
213
-
214
- # vvv
215
- Dir.chdir options['checkout-dir'] do
216
- # ^^^
217
- sh "git clone #{repo_url}"
218
- basedir = repo_url.split(/\//)[-1].gsub(/\.git$/,'')
219
- Dir.entries(basedir).each do |file|
220
- next if file == '.' || file == '..' || file == '.git'
221
- FileUtils.ln_s file,'.'
222
- end
223
- end
224
- end
225
-
226
- version Fullstop::VERSION
227
-
228
- description 'Manages dotfiles from a git repo'
229
-
230
- # vvv
231
- options['checkout-dir'] = ENV['HOME']
232
- on("--force","Overwrite files if they exist")
233
- on("-d DIR","--checkout-dir","Where to clone the repo")
234
- # ^^^
235
-
236
- arg :repo_url, "URL to the git repository containing your dotfiles"
237
-
238
- use_log_level_option
239
-
240
- go!
241
- end
242
- ```
243
-
244
- We've highlighted the lines we changed. Note that, because we added the option to change the checkout dir, we started using it
245
- inside `main`. If we wanted to be very strict in our TDD, we would've written a test for that, but that's a bit outside the
246
- scope of Methadone (feel free to do this on your own, however!).
247
-
248
- Now, we can see that our test passes, *and* our other scenario doesn't fail, meaning we didn't introduce any bugs.
249
-
250
- ```sh
251
- $ rake features
252
- Feature: Checkout dotfiles
253
- In order to get my dotfiles onto a new computer
254
- I want a one-command way to keep them up to date
255
- So I don't have to do it myself
256
-
257
- Scenario: Basic UI
258
- When I get help for "fullstop"
259
- Then the exit status should be 0
260
- And the banner should be present
261
- And there should be a one line summary of what the app does
262
- And the banner should include the version
263
- And the banner should document that this app takes options
264
- And the banner should document that this app's arguments are:
265
- | repo_url | which is required |
266
- And the following options should be documented:
267
- | --force |
268
- | --checkout-dir |
269
- | -d |
270
-
271
- Scenario: Happy Path
272
- Given a git repo with some dotfiles at "/tmp/dotfiles.git"
273
- When I successfully run `fullstop file:///tmp/dotfiles.git`
274
- Then the dotfiles should be checked out in the directory "~/dotfiles"
275
- And the files in "~/dotfiles" should be symlinked in my home directory
276
-
277
- 2 scenarios (2 passed)
278
- 12 steps (12 passed)
279
- 0m0.398s
280
- ```
281
-
282
- We can also see the UI that's generated by this small amount of code:
283
-
284
- ```sh
285
- $ bundle exec bin/fullstop --help
286
- Usage: fullstop [options] repo_url
287
-
288
- Manages dotfiles from a git repo
289
-
290
- v0.0.1
291
-
292
- Options:
293
- --version Show help/version info
294
- --force Overwrite files if they exist
295
- -d, --checkout-dir DIR Where to clone the repo
296
- (default: /Users/davec)
297
- --log-level LEVEL Set the logging level (debug|info|warn|error|fatal)
298
- (Default: info)
299
- ```
300
-
301
- Take a moment to reflect on everything you're getting. We specify only the names of options and their description, and Methadone
302
- handles the rest. *And*, you can avoid all of this magic entirely, if you really need to, since you have access to the
303
- `OptionParser` instance via the `opts` method. You get all of the power of `OptionParser`, but without any framework lock-in.
304
-
305
- For completeness, let's go ahead an implement the two features now that we have the UI in place. To do this, we'll create two
306
- new scenarios.
307
-
308
- First, let's implement the `--force` flag with the following scneario:
309
-
310
- ```cucumber
311
- Scenario: Force overwrite
312
- Given a git repo with some dotfiles at "/tmp/dotfiles.git"
313
- And I have my dotfiles cloned and symlinked to "~/dotfiles"
314
- And there's a new file in the git repo
315
- When I run `fullstop --force file:///tmp/dotfiles.git`
316
- Then the dotfiles in "~/dotfiles" should be re-cloned
317
- And the files in "~/dotfiles" should be symlinked in my home directory
318
- ```
319
-
320
- Several of these steps aren't there, so we'll have to implement them ourselves. Rather than get wrapped up into
321
- that, let's focus on getting our app to pass this scenario. Supposing these steps are implemented, we now have a failing
322
- scenario:
323
-
324
- ```sh
325
- $ rake features
326
- Feature: Checkout dotfiles
327
- In order to get my dotfiles onto a new computer
328
- I want a one-command way to keep them up to date
329
- So I don't have to do it myself
330
-
331
- Scenario: Basic UI
332
- When I get help for "fullstop"
333
- Then the exit status should be 0
334
- And the banner should be present
335
- And there should be a one line summary of what the app does
336
- And the banner should include the version
337
- And the banner should document that this app takes options
338
- And the banner should document that this app's arguments are:
339
- | repo_url | which is required |
340
- And the following options should be documented:
341
- | --force |
342
- | --checkout-dir |
343
- | -d |
344
-
345
- Scenario: Happy Path
346
- Given a git repo with some dotfiles at "/tmp/dotfiles.git"
347
- When I successfully run `fullstop file:///tmp/dotfiles.git`
348
- Then the dotfiles should be checked out in the directory "~/dotfiles"
349
- And the files in "~/dotfiles" should be symlinked in my home directory
350
-
351
- Scenario: Force overwrite
352
- Given a git repo with some dotfiles at "/tmp/dotfiles.git"
353
- And I have my dotfiles cloned and symlinked to "~/dotfiles"
354
- And there's a new file in the git repo
355
- When I run `fullstop --force file:///tmp/dotfiles.git`
356
- Then the dotfiles in "~/dotfiles" should be re-cloned
357
- expected: true
358
- got: false (using ==) (RSpec::Expectations::ExpectationNotMetError)
359
- ./features/step_definitions/fullstop_steps.rb:35:in `block (3 levels) in <top (required)>'
360
- ./features/step_definitions/fullstop_steps.rb:33:in `each'
361
- ./features/step_definitions/fullstop_steps.rb:33:in `block (2 levels) in <top (required)>'
362
- ./features/step_definitions/fullstop_steps.rb:32:in `chdir'
363
- ./features/step_definitions/fullstop_steps.rb:32:in `/^the dotfiles should be checked out in the directory "([^"]*)"$/'
364
- features/fullstop.feature:31:in `Then the dotfiles in "~/dotfiles" should be re-cloned'
365
- And the files in "~/dotfiles" should be symlinked in my home directory
366
-
367
- Failing Scenarios:
368
- cucumber features/fullstop.feature:26
369
-
370
- 3 scenarios (1 failed, 2 passed)
371
- 18 steps (1 failed, 1 skipped, 16 passed)
372
- 0m0.703s
373
- rake aborted!
374
- Cucumber failed
375
-
376
- Tasks: TOP => features
377
- (See full trace by running task with --trace)
378
- ```
379
-
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
-
382
- ```sh
383
- $ HOME=/tmp/fake-home bundle exec bin/fullstop file:///tmp/dotfiles.git
384
- $ HOME=/tmp/fake-home bundle exec bin/fullstop file:///tmp/dotfiles.git
385
- Error output of 'git clone file:///tmp/dotfiles.git': fatal: destination path 'dotfiles' already exists and is not an empty directory.
386
- Output of 'git clone file:///tmp/dotfiles.git':
387
- Error running 'git clone file:///tmp/dotfiles.git'
388
- File exists - (.bashrc, ./.bashrc)
389
- $ echo $?
390
- 70
391
- ```
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 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.
394
-
395
- We'll write a scenario to reveal the bug:
396
-
397
- ```cucumber
398
- Scenario: Fail if directory is cloned
399
- Given a git repo with some dotfiles at "/tmp/dotfiles.git"
400
- And I have my dotfiles cloned and symlinked to "~/dotfiles"
401
- And there's a new file in the git repo
402
- When I run `fullstop file:///tmp/dotfiles.git`
403
- Then the exit status should not be 0
404
- And the stderr should contain "checkout dir already exists, use --force to overwrite"
405
- ```
406
-
407
- Now, we can see that it doesn't pass:
408
- ```sh
409
- $ rake features
410
- Feature: Checkout dotfiles
411
- In order to get my dotfiles onto a new computer
412
- I want a one-command way to keep them up to date
413
- So I don't have to do it myself
414
-
415
- Scenario: Basic UI
416
- When I get help for "fullstop"
417
- Then the exit status should be 0
418
- And the banner should be present
419
- And there should be a one line summary of what the app does
420
- And the banner should include the version
421
- And the banner should document that this app takes options
422
- And the banner should document that this app's arguments are:
423
- | repo_url | which is required |
424
- And the following options should be documented:
425
- | --force |
426
- | --checkout-dir |
427
- | -d |
428
-
429
- Scenario: Happy Path
430
- Given a git repo with some dotfiles at "/tmp/dotfiles.git"
431
- When I successfully run `fullstop file:///tmp/dotfiles.git`
432
- Then the dotfiles should be checked out in the directory "~/dotfiles"
433
- And the files in "~/dotfiles" should be symlinked in my home directory
434
-
435
- Scenario: Fail if directory is cloned
436
- Given a git repo with some dotfiles at "/tmp/dotfiles.git"
437
- And I have my dotfiles cloned and symlinked to "~/dotfiles"
438
- And there's a new file in the git repo
439
- When I run `fullstop file:///tmp/dotfiles.git`
440
- Then the exit status should not be 0
441
- And the stderr should contain "checkout dir already exists, uses --force to overwrite"
442
- expected "W, [2012-02-12T15:12:35.720851 #43530] WARN -- : Error output of 'git clone file:///tmp/dotfiles.git': fatal: destination path 'dotfiles' already exists and is not an empty directory.\nW, [2012-02-12T15:12:35.720964 #43530] WARN -- : Error running 'git clone file:///tmp/dotfiles.git'\nE, [2012-02-12T15:12:35.721503 #43530] ERROR -- : File exists - (.bashrc, ./.bashrc)\n" to include "checkout dir already exists, uses --force to overwrite"
443
- Diff:
444
- @@ -1,2 +1,4 @@
445
- -["checkout dir already exists, uses --force to overwrite"]
446
- +W, [2012-02-12T15:12:35.720851 #43530] WARN -- : Error output of 'git clone file:///tmp/dotfiles.git': fatal: destination path 'dotfiles' already exists and is not an empty directory.
447
- +W, [2012-02-12T15:12:35.720964 #43530] WARN -- : Error running 'git clone file:///tmp/dotfiles.git'
448
- +E, [2012-02-12T15:12:35.721503 #43530] ERROR -- : File exists - (.bashrc, ./.bashrc)
449
- (RSpec::Expectations::ExpectationNotMetError)
450
- features/fullstop.feature:32:in `And the stderr should contain "checkout dir already exists, uses --force to overwrite"'
451
-
452
- Failing Scenarios:
453
- cucumber features/fullstop.feature:26
454
-
455
- 3 scenarios (1 failed, 2 passed)
456
- 18 steps (1 failed, 17 passed)
457
- 0m0.694s
458
- rake aborted!
459
- Cucumber failed
460
-
461
- Tasks: TOP => features
462
- (See full trace by running task with --trace)
463
- ```
464
-
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.
466
-
467
- Here's the changes we'll make to `bin/fullstop` to check for this:
468
-
469
- ```ruby
470
- #!/usr/bin/env ruby
471
-
472
- require 'optparse'
473
- require 'methadone'
474
- require 'fullstop'
475
- require 'fileutils'
476
-
477
- class App
478
- include Methadone::Main
479
- include Methadone::CLILogging
480
- include Methadone::SH
481
-
482
- main do |repo_url|
483
-
484
- Dir.chdir options['checkout-dir'] do
485
- # vvv
486
- if sh("git clone #{repo_url}") == 0
487
- # ^^^
488
- basedir = repo_url.split(/\//)[-1].gsub(/\.git$/,'')
489
- Dir.entries(basedir).each do |file|
490
- next if file == '.' || file == '..' || file == '.git'
491
- FileUtils.ln_s file,'.'
492
- end
493
- else
494
- # vvv
495
- exit_now!("checkout dir already exists, use --force to overwrite")
496
- # ^^^
497
- end
498
- end
499
- end
500
-
501
- version Fullstop::VERSION
502
-
503
- description 'Manages dotfiles from a git repo'
504
-
505
- options['checkout-dir'] = ENV['HOME']
506
- on("--force","Overwrite files if they exist")
507
- on("-d DIR","--checkout-dir","Where to clone the repo")
508
-
509
- arg :repo_url, "URL to the git repository containing your dotfiles"
510
-
511
- use_log_level_option
512
-
513
- go!
514
- end
515
- ```
516
-
517
- Since an exit of zero is considered success, we branch on that value, and call the method `exit_now!`, provided by Methadone, to
518
- stop the app with an error. The argument is an error message to
519
- print to the standard error to let the user know why the app stopped abnormally. The app will then stop and exit nonzero. If
520
- you want to customize the exit code, you can provide it as the first argument, with the message being the second argument.
521
-
522
- As you can see, our test now passes:
523
-
524
- ```sh
525
- $ rake features
526
- Feature: Checkout dotfiles
527
- In order to get my dotfiles onto a new computer
528
- I want a one-command way to keep them up to date
529
- So I don't have to do it myself
530
-
531
- Scenario: Basic UI
532
- When I get help for "fullstop"
533
- Then the exit status should be 0
534
- And the banner should be present
535
- And there should be a one line summary of what the app does
536
- And the banner should include the version
537
- And the banner should document that this app takes options
538
- And the banner should document that this app's arguments are:
539
- | repo_url | which is required |
540
- And the following options should be documented:
541
- | --force |
542
- | --checkout-dir |
543
- | -d |
544
-
545
- Scenario: Happy Path
546
- Given a git repo with some dotfiles at "/tmp/dotfiles.git"
547
- When I successfully run `fullstop file:///tmp/dotfiles.git`
548
- Then the dotfiles should be checked out in the directory "~/dotfiles"
549
- And the files in "~/dotfiles" should be symlinked in my home directory
550
-
551
- Scenario: Fail if directory is cloned
552
- Given a git repo with some dotfiles at "/tmp/dotfiles.git"
553
- And I have my dotfiles cloned and symlinked to "~/dotfiles"
554
- And there's a new file in the git repo
555
- When I run `fullstop file:///tmp/dotfiles.git`
556
- Then the exit status should not be 0
557
- And the stderr should contain "checkout dir already exists, use --force to overwrite"
558
-
559
- 3 scenarios (3 passed)
560
- 18 steps (18 passed)
561
- 0m0.789s
562
- ```
563
-
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:
565
-
566
- ```ruby
567
- main do |repo_url|
568
-
569
- Dir.chdir options['checkout-dir'] do
570
- # vvv
571
- sh! git clone #{repo_url}", :on_fail => "checkout dir already exists, use --force to overwrite"
572
- # ^^^
573
- basedir = repo_url.split(/\//)[-1].gsub(/\.git$/,'')
574
- Dir.entries(basedir).each do |file|
575
- next if file == '.' || file == '..' || file == '.git'
576
- FileUtils.ln_s file,'.'
577
- end
578
- end
579
- end
580
- ```
581
-
582
- Which method to use is purely stylistic and up to you.
583
-
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.
585
-
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.
587
-
588
- ```ruby
589
- #!/usr/bin/env ruby
590
-
591
- require 'optparse'
592
- require 'methadone'
593
- require 'fullstop'
594
- require 'fileutils'
595
-
596
- class App
597
- include Methadone::Main
598
- include Methadone::CLILogging
599
- include Methadone::SH
600
-
601
- main do |repo_url|
602
-
603
- Dir.chdir options['checkout-dir'] do
604
- basedir = repo_url.split(/\//)[-1].gsub(/\.git$/,'')
605
- # vvv
606
- if options[:force] && Dir.exists?(basedir)
607
- warn "deleting #{basedir} before cloning"
608
- FileUtils.rm_rf basedir
609
- end
610
- # ^^^
611
- if sh("git clone #{repo_url}") == 0
612
- Dir.entries(basedir).each do |file|
613
- next if file == '.' || file == '..' || file == '.git'
614
- source_file = File.join(basedir,file)
615
- # vvv
616
- FileUtils.rm(file) if File.exists?(file) && options[:force]
617
- # ^^^
618
- FileUtils.ln_s source_file,'.'
619
- end
620
- else
621
- exit_now!("checkout dir already exists, use --force to overwrite")
622
- end
623
- end
624
- end
625
-
626
- version Fullstop::VERSION
627
-
628
- description 'Manages dotfiles from a git repo'
629
-
630
- options['checkout-dir'] = ENV['HOME']
631
- on("--force","Overwrite files if they exist")
632
- on("-d DIR","--checkout-dir","Where to clone the repo")
633
-
634
- arg :repo_url, "URL to the git repository containing your dotfiles"
635
-
636
- use_log_level_option
637
-
638
- go!
639
- end
640
- ```
641
-
642
- Now, we can see that our scenario passes:
643
-
644
- ```sh
645
- $ rake features
646
- Feature: Checkout dotfiles
647
- In order to get my dotfiles onto a new computer
648
- I want a one-command way to keep them up to date
649
- So I don't have to do it myself
650
-
651
- Scenario: Basic UI
652
- When I get help for "fullstop"
653
- Then the exit status should be 0
654
- And the banner should be present
655
- And there should be a one line summary of what the app does
656
- And the banner should include the version
657
- And the banner should document that this app takes options
658
- And the banner should document that this app's arguments are:
659
- | repo_url | which is required |
660
- And the following options should be documented:
661
- | --force |
662
- | --checkout-dir |
663
- | -d |
664
-
665
- Scenario: Happy Path
666
- Given a git repo with some dotfiles at "/tmp/dotfiles.git"
667
- When I successfully run `fullstop file:///tmp/dotfiles.git`
668
- Then the dotfiles should be checked out in the directory "~/dotfiles"
669
- And the files in "~/dotfiles" should be symlinked in my home directory
670
-
671
- Scenario: Fail if directory is cloned
672
- Given a git repo with some dotfiles at "/tmp/dotfiles.git"
673
- And I have my dotfiles cloned and symlinked to "~/dotfiles"
674
- And there's a new file in the git repo
675
- When I run `fullstop file:///tmp/dotfiles.git`
676
- Then the exit status should not be 0
677
- And the stderr should contain "checkout dir already exists, use --force to overwrite"
678
-
679
- Scenario: Force overwrite
680
- Given a git repo with some dotfiles at "/tmp/dotfiles.git"
681
- And I have my dotfiles cloned and symlinked to "~/dotfiles"
682
- And there's a new file in the git repo
683
- When I successfully run `fullstop --force file:///tmp/dotfiles.git`
684
- Then the dotfiles in "~/dotfiles" should be re-cloned
685
- And the files in "~/dotfiles" should be symlinked in my home directory
686
-
687
- 4 scenarios (4 passed)
688
- 24 steps (24 passed)
689
- 0m1.171s
690
- ```
691
-
692
- We're starting to see a few features of Methadone that need some explanation, such as what those log messages are, and how the
693
- output of our commands is getting there. But first, our `main` method is also becoming pretty messy. Since Methadone allows and encourages you to write your app cleanly, we can refactor that code into classes that live inside `lib`. We'll see how to do that in the next section.