lux-hammer 0.0.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.version +1 -0
- data/README.md +1013 -0
- data/bin/hammer +9 -0
- data/lib/hammer/builder.rb +36 -0
- data/lib/hammer/command.rb +43 -0
- data/lib/hammer/command_builder.rb +30 -0
- data/lib/hammer/loader.rb +82 -0
- data/lib/hammer/option.rb +77 -0
- data/lib/hammer/parser.rb +83 -0
- data/lib/hammer/shell.rb +166 -0
- data/lib/lux-hammer.rb +613 -2
- metadata +34 -8
data/README.md
ADDED
|
@@ -0,0 +1,1013 @@
|
|
|
1
|
+
# hammer
|
|
2
|
+
|
|
3
|
+
A modern CLI builder for Ruby. The good parts of
|
|
4
|
+
[Rake](https://github.com/ruby/rake) and
|
|
5
|
+
[Thor](https://github.com/rails/thor) without the cruft. Drop a
|
|
6
|
+
`Hammerfile`, run `hammer`, ship.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
gem 'lux-hammer'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or from the command line:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
gem install lux-hammer
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This installs the `hammer` binary and exposes `require 'lux-hammer'`.
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
Create a `Hammerfile` in your project root:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
define :hello do
|
|
28
|
+
desc 'say hi'
|
|
29
|
+
proc do |opts|
|
|
30
|
+
say.green "hello #{opts[:args].first || 'world'}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
$ hammer hello
|
|
39
|
+
hello world
|
|
40
|
+
$ hammer hello dino
|
|
41
|
+
hello dino
|
|
42
|
+
$ hammer
|
|
43
|
+
Usage: hammer COMMAND [ARGS]
|
|
44
|
+
|
|
45
|
+
Commands:
|
|
46
|
+
hammer hello # say hi
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
That's it. `hammer` walks up from your current directory looking for a
|
|
50
|
+
`Hammerfile`, evaluates it, and dispatches.
|
|
51
|
+
|
|
52
|
+
## Why hammer (the short pitch)
|
|
53
|
+
|
|
54
|
+
A handful of papercuts from Rake and Thor that hammer just doesn't have.
|
|
55
|
+
|
|
56
|
+
### Rake task arguments are awkward
|
|
57
|
+
|
|
58
|
+
Rake forces you into `task[arg1,arg2]` syntax with no types, no flags,
|
|
59
|
+
no help, and shell-hostile brackets:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
# Rakefile
|
|
63
|
+
task :greet, [:name, :loud] do |_, args|
|
|
64
|
+
puts args[:loud] == 'true' ? "HELLO #{args[:name].upcase}" : "hello #{args[:name]}"
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
```sh
|
|
68
|
+
$ rake 'greet[dino,true]' # quotes required - zsh/bash treat [] as globs
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Hammer takes typed options, positional fill, and any common flag form:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
# Hammerfile
|
|
75
|
+
define :greet do
|
|
76
|
+
desc 'Say hello'
|
|
77
|
+
opt :name
|
|
78
|
+
opt :loud, type: :boolean, alias: :l
|
|
79
|
+
proc { |o| say o[:loud] ? "HELLO #{o[:name].upcase}" : "hello #{o[:name]}" }
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
```sh
|
|
83
|
+
$ hammer greet dino -l # positional fills :name, -l sets :loud
|
|
84
|
+
$ hammer greet --name=dino --loud # or be explicit
|
|
85
|
+
$ hammer greet -h # real help, with defaults and examples
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Invoking another task
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
# Rake
|
|
92
|
+
Rake::Task['db:migrate'].invoke('prod')
|
|
93
|
+
```
|
|
94
|
+
```ruby
|
|
95
|
+
# hammer - reads like a normal Ruby method call
|
|
96
|
+
hammer_db_migrate(env: 'prod')
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Thor: `desc` welded to a method, no aliases, two arg systems
|
|
100
|
+
|
|
101
|
+
Thor splits arguments between method parameters and `method_option`,
|
|
102
|
+
needs a usage string repeated in `desc`, and has no first-class command
|
|
103
|
+
aliases (you reach for `map`):
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
# Thor
|
|
107
|
+
class MyCli < Thor
|
|
108
|
+
desc 'greet NAME', 'Say hello' # usage repeated by hand
|
|
109
|
+
method_option :loud, type: :boolean, aliases: '-l'
|
|
110
|
+
def greet(name) # name is a method param...
|
|
111
|
+
options[:loud] ? puts(name.upcase) : puts(name) # ...options live elsewhere
|
|
112
|
+
end
|
|
113
|
+
map 'g' => :greet # aliases bolted on
|
|
114
|
+
end
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
# hammer - one arg system, real aliases, no usage string to maintain
|
|
119
|
+
define :greet do
|
|
120
|
+
desc 'Say hello'
|
|
121
|
+
alt :g
|
|
122
|
+
opt :name
|
|
123
|
+
opt :loud, type: :boolean, alias: :l
|
|
124
|
+
proc { |o| say o[:loud] ? o[:name].upcase : o[:name] }
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Usage is generated from your `opt`s, `alt :g` registers a real alias,
|
|
129
|
+
and there's one place to look for everything the command takes.
|
|
130
|
+
|
|
131
|
+
## The two styles
|
|
132
|
+
|
|
133
|
+
### `define :name do ... end` (block DSL)
|
|
134
|
+
|
|
135
|
+
The block's **last expression must be `proc do |opts| ... end`**. That
|
|
136
|
+
proc is the handler. Everything before it is metadata.
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
define :build do
|
|
140
|
+
desc 'Build the project'
|
|
141
|
+
example 'build prod -v'
|
|
142
|
+
opt :verbose, type: :boolean, alias: :v
|
|
143
|
+
opt :env, default: 'dev'
|
|
144
|
+
|
|
145
|
+
proc do |opts|
|
|
146
|
+
say.green "building #{opts[:env]}"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### `desc` + `def` (classic DSL)
|
|
152
|
+
|
|
153
|
+
For when you'd rather write a Ruby method:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
class MyCli < Hammer
|
|
157
|
+
desc 'Build the project'
|
|
158
|
+
opt :verbose, type: :boolean, alias: :v
|
|
159
|
+
opt :env, default: 'dev'
|
|
160
|
+
def build(opts)
|
|
161
|
+
say.green "building #{opts[:env]}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
desc 'Ping with no opts'
|
|
165
|
+
def ping
|
|
166
|
+
say 'pong'
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
MyCli.start(ARGV)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`desc` is the trigger - a `def` without a preceding `desc` is just a
|
|
174
|
+
regular method, never a command. Methods with arity 0 are called
|
|
175
|
+
without opts; methods that take an argument receive the opts hash.
|
|
176
|
+
|
|
177
|
+
Both styles can coexist in the same class.
|
|
178
|
+
|
|
179
|
+
## Options (`opt`)
|
|
180
|
+
|
|
181
|
+
### Declaration
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
opt :name,
|
|
185
|
+
type: :string, # :string (default) :boolean :integer :float :array
|
|
186
|
+
default: nil, # default value when omitted
|
|
187
|
+
alias: :n, # one or many - see below
|
|
188
|
+
desc: 'help text',# shown in `help COMMAND`
|
|
189
|
+
req: false # raise at parse time if not supplied
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Underscores in the name become dashes in the flag:
|
|
193
|
+
`opt :dry_run` → `--dry-run`. The kwarg key in `opts` is still
|
|
194
|
+
`:dry_run`.
|
|
195
|
+
|
|
196
|
+
### Invocation forms
|
|
197
|
+
|
|
198
|
+
For value options (anything that's not `:boolean`), all three forms work:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
--port=3000 # long with equals
|
|
202
|
+
--port 3000 # long with space
|
|
203
|
+
-p 3000 # short alias with space (requires alias: :p)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Not supported: attached short form (`-p3000`), combined short flags
|
|
207
|
+
(`-vf`).
|
|
208
|
+
|
|
209
|
+
For boolean options:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
--verbose # set to true
|
|
213
|
+
--no-verbose # set to false (only if a default of true is in play)
|
|
214
|
+
-v # short alias if declared
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Per-type behavior
|
|
218
|
+
|
|
219
|
+
#### `:string` (default)
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
opt :env, default: 'dev'
|
|
223
|
+
```
|
|
224
|
+
```
|
|
225
|
+
hammer build --env prod # opts[:env] = "prod"
|
|
226
|
+
hammer build --env=prod # opts[:env] = "prod"
|
|
227
|
+
hammer build # opts[:env] = "dev" (default)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
#### `:boolean`
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
opt :verbose, type: :boolean, alias: :v
|
|
234
|
+
opt :cache, type: :boolean, default: true
|
|
235
|
+
```
|
|
236
|
+
```
|
|
237
|
+
hammer build -v # opts[:verbose] = true
|
|
238
|
+
hammer build --verbose # opts[:verbose] = true
|
|
239
|
+
hammer build --no-cache # opts[:cache] = false (negates default)
|
|
240
|
+
hammer build # opts[:cache] = true (default)
|
|
241
|
+
# opts[:verbose] = nil (no default)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Booleans never consume a positional. `--no-X` only meaningfully overrides
|
|
245
|
+
a `default: true`.
|
|
246
|
+
|
|
247
|
+
#### `:integer`
|
|
248
|
+
|
|
249
|
+
```ruby
|
|
250
|
+
opt :port, type: :integer, default: 3000, alias: :p
|
|
251
|
+
```
|
|
252
|
+
```
|
|
253
|
+
hammer serve --port 8080 # opts[:port] = 8080
|
|
254
|
+
hammer serve -p 8080 # opts[:port] = 8080
|
|
255
|
+
hammer serve # opts[:port] = 3000
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Bad input raises a parse error: `--port=abc` → `invalid value for Integer()`.
|
|
259
|
+
|
|
260
|
+
#### `:float`
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
opt :threshold, type: :float, default: 0.5
|
|
264
|
+
```
|
|
265
|
+
```
|
|
266
|
+
hammer scan --threshold 0.95 # opts[:threshold] = 0.95
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
#### `:array`
|
|
270
|
+
|
|
271
|
+
Comma-separated. No surrounding whitespace.
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
opt :tags, type: :array, default: []
|
|
275
|
+
```
|
|
276
|
+
```
|
|
277
|
+
hammer deploy --tags a,b,c # opts[:tags] = ["a", "b", "c"]
|
|
278
|
+
hammer deploy --tags=foo # opts[:tags] = ["foo"]
|
|
279
|
+
hammer deploy # opts[:tags] = []
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Aliases (`alias:`)
|
|
283
|
+
|
|
284
|
+
A symbol becomes a flag automatically (1 char -> short, more -> long).
|
|
285
|
+
Strings starting with `-` pass through. One value or an array:
|
|
286
|
+
|
|
287
|
+
```ruby
|
|
288
|
+
opt :port, alias: :p # -> -p
|
|
289
|
+
opt :pretend, alias: :p # -> -p
|
|
290
|
+
opt :rollback, alias: :back # -> --back (multi-letter symbol)
|
|
291
|
+
opt :verbose, alias: [:v, :V, :loud] # -> -v, -V, --loud
|
|
292
|
+
opt :env, alias: '-E' # string with `-` passes through
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Required
|
|
296
|
+
|
|
297
|
+
`req: true` raises a parse error if neither a flag nor a positional
|
|
298
|
+
fills the opt:
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
opt :url, req: true
|
|
302
|
+
```
|
|
303
|
+
```
|
|
304
|
+
$ hammer deploy
|
|
305
|
+
[error] missing required --url
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
A positional satisfies it: `hammer deploy https://x.com` works because
|
|
309
|
+
of the declaration-order positional fill (see below).
|
|
310
|
+
|
|
311
|
+
### Defaults
|
|
312
|
+
|
|
313
|
+
`default:` is used when neither a flag nor a positional supplies the
|
|
314
|
+
value:
|
|
315
|
+
|
|
316
|
+
```ruby
|
|
317
|
+
opt :env, default: 'dev'
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Note: boolean defaults of `nil` (the implicit default) and `false` are
|
|
321
|
+
not the same. `nil` means "not set; key absent from opts unless a flag
|
|
322
|
+
appears". Explicit `default: false` means "key always present, value
|
|
323
|
+
false unless `--flag` is passed".
|
|
324
|
+
|
|
325
|
+
### Positional fill (declaration order)
|
|
326
|
+
|
|
327
|
+
Anything in ARGV without `-` / `--` fills the next un-set
|
|
328
|
+
**non-boolean** opt, in declaration order:
|
|
329
|
+
|
|
330
|
+
```ruby
|
|
331
|
+
define :deploy do
|
|
332
|
+
opt :url
|
|
333
|
+
opt :env, default: 'dev'
|
|
334
|
+
proc { |opts| ... }
|
|
335
|
+
end
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
These all produce the same `opts`:
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
hammer deploy https://x.com prod # both positional
|
|
342
|
+
hammer deploy https://x.com --env=prod # mixed
|
|
343
|
+
hammer deploy --url=https://x.com prod # mixed reverse
|
|
344
|
+
hammer deploy --url=https://x.com --env=prod # both flags
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Rules recap:
|
|
348
|
+
|
|
349
|
+
* Boolean opts are skipped during positional fill.
|
|
350
|
+
* A flag value wins over a positional for the same opt.
|
|
351
|
+
* Leftover positionals go to `opts[:args]`.
|
|
352
|
+
* A positional satisfying a `req: true` opt counts as supplied.
|
|
353
|
+
|
|
354
|
+
### The `opts` hash
|
|
355
|
+
|
|
356
|
+
Always a `Hash` with **symbol keys**. Keys present:
|
|
357
|
+
|
|
358
|
+
* one per declared option that was supplied (via flag, positional, or
|
|
359
|
+
default)
|
|
360
|
+
* `opts[:args]` - array of positional ARGV not absorbed by an opt
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
define :show do
|
|
364
|
+
opt :env, default: 'dev'
|
|
365
|
+
opt :loud, type: :boolean
|
|
366
|
+
proc { |opts| p opts }
|
|
367
|
+
end
|
|
368
|
+
```
|
|
369
|
+
```
|
|
370
|
+
$ hammer show foo bar --env=prod --loud
|
|
371
|
+
{env: "prod", loud: true, args: ["foo", "bar"]}
|
|
372
|
+
|
|
373
|
+
$ hammer show
|
|
374
|
+
{env: "dev", args: []}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Stopping option parsing (`--`)
|
|
378
|
+
|
|
379
|
+
A bare `--` ends option parsing; everything after goes to `opts[:args]`
|
|
380
|
+
verbatim, even if it looks like a flag:
|
|
381
|
+
|
|
382
|
+
```
|
|
383
|
+
hammer build --env=prod -- --not-a-flag foo
|
|
384
|
+
# opts[:env] = "prod"
|
|
385
|
+
# opts[:args] = ["--not-a-flag", "foo"]
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Namespaces (Rake-style colon paths)
|
|
389
|
+
|
|
390
|
+
Commands inside a `namespace :name do ... end` block are reached via
|
|
391
|
+
colon-paths from the root binary - just like `rake db:migrate`:
|
|
392
|
+
|
|
393
|
+
```ruby
|
|
394
|
+
namespace :db do
|
|
395
|
+
define :migrate do
|
|
396
|
+
proc { |opts| ... }
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
namespace :users do
|
|
400
|
+
define :list do
|
|
401
|
+
proc { |opts| ... }
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Then:
|
|
408
|
+
|
|
409
|
+
```
|
|
410
|
+
hammer db:migrate
|
|
411
|
+
hammer db:users:list
|
|
412
|
+
hammer db # bare namespace lists everything under it
|
|
413
|
+
hammer db:migrate -h # per-command help
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Namespaces nest to any depth. There is no per-level dispatch - the root
|
|
417
|
+
parses the whole colon path and walks the namespace tree.
|
|
418
|
+
|
|
419
|
+
## Pre-hooks (`before`)
|
|
420
|
+
|
|
421
|
+
A `before do ... end` block at the root scope or inside a `namespace`
|
|
422
|
+
runs before every command in that scope (and its nested namespaces).
|
|
423
|
+
Hooks fire outer -> inner, then the command's handler:
|
|
424
|
+
|
|
425
|
+
```ruby
|
|
426
|
+
before { Dotenv.load } # runs before every command
|
|
427
|
+
|
|
428
|
+
namespace :db do
|
|
429
|
+
before { hammer_env } # runs before every db:* command
|
|
430
|
+
define :migrate do
|
|
431
|
+
proc { |opts| ... } # no boilerplate require inside
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
`before` is intentionally not available inside `define` - the proc body
|
|
437
|
+
*is* the command body, just put the setup line at the top of the proc.
|
|
438
|
+
|
|
439
|
+
Pairs naturally with hidden commands (next section): keep `:env` /
|
|
440
|
+
`:app` setup as undocumented helpers and pull them in via `before`.
|
|
441
|
+
|
|
442
|
+
## Hidden commands (no `desc`)
|
|
443
|
+
|
|
444
|
+
A command declared without a `desc` is **hidden from help listings**
|
|
445
|
+
but stays fully dispatchable and `hammer_*`-callable:
|
|
446
|
+
|
|
447
|
+
```ruby
|
|
448
|
+
define :env do
|
|
449
|
+
proc { |_| require './config/env' } # no desc -> hidden
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
namespace :db do
|
|
453
|
+
before { hammer_env } # call it from a hook
|
|
454
|
+
define :migrate do
|
|
455
|
+
desc 'Run migrations'
|
|
456
|
+
proc { |_| ... }
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
`hammer` and `hammer db` won't list `env`, but `hammer env`,
|
|
462
|
+
`hammer_env` from another proc, and `before { hammer_env }` all work.
|
|
463
|
+
|
|
464
|
+
## Prereqs (`needs`)
|
|
465
|
+
|
|
466
|
+
Declare commands that must run before this one (Rake-style task deps):
|
|
467
|
+
|
|
468
|
+
```ruby
|
|
469
|
+
define :env do
|
|
470
|
+
proc { |_| require './config/env' } # hidden helper
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
define :app do
|
|
474
|
+
needs :env # runs `env` first
|
|
475
|
+
desc 'start the app'
|
|
476
|
+
proc { |opts| App.start }
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
define :deploy do
|
|
480
|
+
needs :env, :build # multiple prereqs, in order
|
|
481
|
+
proc { |opts| ... }
|
|
482
|
+
end
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
Prereq names are colon paths resolved against the root class - same
|
|
486
|
+
lookup as `hammer_*`. Use `needs 'db:env'` to depend on a namespaced
|
|
487
|
+
command.
|
|
488
|
+
|
|
489
|
+
Each prereq fires **at most once per top-level invocation**, so if
|
|
490
|
+
`:app` needs `:env` and `:build` also needs `:env`, `:env` still runs
|
|
491
|
+
only once. Prereqs run with default options (no argv passed through).
|
|
492
|
+
|
|
493
|
+
`needs` vs `before`:
|
|
494
|
+
* `before { hammer_env }` - fires for *every* command in a scope.
|
|
495
|
+
* `needs :env` - declared per command, deduped across the call chain.
|
|
496
|
+
|
|
497
|
+
## Command aliases (`alt`)
|
|
498
|
+
|
|
499
|
+
`alt :short_name` (or several) registers extra names for a command:
|
|
500
|
+
|
|
501
|
+
```ruby
|
|
502
|
+
define :server do
|
|
503
|
+
alt :s, :srv
|
|
504
|
+
proc { |opts| ... }
|
|
505
|
+
end
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Then `hammer server`, `hammer s`, and `hammer srv` all dispatch to the
|
|
509
|
+
same command. Alts work inside namespaces too: `alt :m` on `db:migrate`
|
|
510
|
+
makes `db:m` resolve.
|
|
511
|
+
|
|
512
|
+
## Cross-invocation (`hammer_*`)
|
|
513
|
+
|
|
514
|
+
From inside any command's proc - or from outside via the class - you can
|
|
515
|
+
invoke other commands without re-shelling out:
|
|
516
|
+
|
|
517
|
+
```ruby
|
|
518
|
+
define :deploy do
|
|
519
|
+
proc do |opts|
|
|
520
|
+
hammer_build(env: 'prod', verbose: true)
|
|
521
|
+
hammer_db_migrate
|
|
522
|
+
say.green 'deployed'
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
The mapping mirrors the CLI literally:
|
|
528
|
+
|
|
529
|
+
* `hammer_X_Y_Z` → command path `X:Y:Z` (underscores in the method
|
|
530
|
+
name become colons)
|
|
531
|
+
* positional args → positional ARGV
|
|
532
|
+
* `verbose: true` → `--verbose`
|
|
533
|
+
* `no_cache: true` → `--no-cache` (just the same rule - underscores in
|
|
534
|
+
the kwarg key become dashes)
|
|
535
|
+
* `dry_run: true` → `--dry-run`
|
|
536
|
+
* `env: 'prod'` → `--env=prod`
|
|
537
|
+
* `anything: false` → skipped (no-op; use `no_x: true` to negate)
|
|
538
|
+
|
|
539
|
+
`MyCli.hammer_db_users_list("a", verbose: true)` also works at the
|
|
540
|
+
class level, useful for tests and scripting.
|
|
541
|
+
|
|
542
|
+
## Shell helpers
|
|
543
|
+
|
|
544
|
+
These are mixed into every handler (and also live on `Hammer::Shell` for
|
|
545
|
+
direct use).
|
|
546
|
+
|
|
547
|
+
### `say` - print a line
|
|
548
|
+
|
|
549
|
+
```ruby
|
|
550
|
+
say 'plain output' # no color
|
|
551
|
+
say.green 'ok' # color via proxy (preferred)
|
|
552
|
+
say.cyan "env=#{env}" # interpolation works the same
|
|
553
|
+
say 'equivalent', :green # two-arg form is still supported
|
|
554
|
+
say '' # blank line
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
`say` with no args returns a tiny proxy that exposes one method per
|
|
558
|
+
color: `say.red 'x'` is just `say('x', :red)`. The proxy form reads
|
|
559
|
+
better when colors are the common case. Use `say ''` for an explicit
|
|
560
|
+
blank line.
|
|
561
|
+
|
|
562
|
+
### `String#color` - paint a string without printing
|
|
563
|
+
|
|
564
|
+
```ruby
|
|
565
|
+
label = 'OK'.color(:green)
|
|
566
|
+
puts "[#{label}] done" # embed colored chunks anywhere
|
|
567
|
+
|
|
568
|
+
Hammer::Shell.paint('x', :red) # the underlying primitive `say` uses
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### Colors
|
|
572
|
+
|
|
573
|
+
Valid names: `:black :red :green :yellow :blue :magenta :cyan :white :gray`.
|
|
574
|
+
Unknown colors (in `say`, `say.<color>`, `paint`, or `String#color`)
|
|
575
|
+
raise `Hammer::Error` listing the valid names - even when colors are
|
|
576
|
+
disabled, so typos fail loudly in CI.
|
|
577
|
+
|
|
578
|
+
Colors are auto-disabled when stdout isn't a TTY or when `NO_COLOR` is
|
|
579
|
+
set. Force on/off programmatically:
|
|
580
|
+
|
|
581
|
+
```ruby
|
|
582
|
+
Hammer::Shell.color!(true) # force ANSI on
|
|
583
|
+
Hammer::Shell.color!(false) # force off
|
|
584
|
+
Hammer::Shell.color? # current state (bool)
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### `error` - controlled failure
|
|
588
|
+
|
|
589
|
+
```ruby
|
|
590
|
+
error 'config file missing' unless File.exist?('config.yml')
|
|
591
|
+
# -> dispatcher prints "[error] config file missing" in red, exits 1
|
|
592
|
+
# No backtrace, no per-command help spam.
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
Internally it just raises `Hammer::Error`; the dispatcher catches it.
|
|
596
|
+
|
|
597
|
+
### `ask` - read one line from stdin
|
|
598
|
+
|
|
599
|
+
```ruby
|
|
600
|
+
name = ask 'your name' # required-style prompt
|
|
601
|
+
env = ask 'env', default: 'dev' # blank input -> "dev"
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
The prompt is shown in cyan with the default in brackets when present.
|
|
605
|
+
Returns the typed line (chomped) or the default on a blank line.
|
|
606
|
+
|
|
607
|
+
### `yes?` - y/N confirmation
|
|
608
|
+
|
|
609
|
+
```ruby
|
|
610
|
+
exit 0 unless yes? 'continue?' # anything starting with y/Y -> true
|
|
611
|
+
# blank, n, anything else -> false
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### `choose` - arrow-key picker
|
|
615
|
+
|
|
616
|
+
```ruby
|
|
617
|
+
envs = %w[dev staging prod]
|
|
618
|
+
idx = choose 'Pick env', envs
|
|
619
|
+
say.green "deploying to #{envs[idx]}" if idx
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
While the picker is up: `j`/`k` or `↑`/`↓` to move, `Enter` to confirm,
|
|
623
|
+
`q` / `ESC` / Ctrl-C to cancel. Returns the integer index of the
|
|
624
|
+
selected item, or `nil` on cancel.
|
|
625
|
+
|
|
626
|
+
When stdin isn't a TTY (pipes, redirected scripts, tests), `choose`
|
|
627
|
+
falls back to a numbered prompt: it prints each item with a number and
|
|
628
|
+
reads a line - same return contract, so calling code doesn't change.
|
|
629
|
+
|
|
630
|
+
```
|
|
631
|
+
$ echo 2 | mycli some-cmd
|
|
632
|
+
Pick env
|
|
633
|
+
1) dev
|
|
634
|
+
2) staging
|
|
635
|
+
3) prod
|
|
636
|
+
select [1-3]:
|
|
637
|
+
# idx -> 1 (zero-based)
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### `sh` - run a shell command, abort on failure
|
|
641
|
+
|
|
642
|
+
```ruby
|
|
643
|
+
sh 'bundle install' # echoes "$ bundle install" in gray
|
|
644
|
+
sh "git tag v#{version}" # raises Hammer::Error on non-zero
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
Returns `true` on success. On non-zero exit it raises `Hammer::Error`,
|
|
648
|
+
which the dispatcher turns into `[error] command failed: ...` + exit 1.
|
|
649
|
+
|
|
650
|
+
## Splitting across files (`load`)
|
|
651
|
+
|
|
652
|
+
Once a `Hammerfile` grows past a screen or two, split it. Drop fragments
|
|
653
|
+
in any file ending in `_hammer.rb` and pull them in with `load`:
|
|
654
|
+
|
|
655
|
+
```ruby
|
|
656
|
+
# Hammerfile
|
|
657
|
+
load auto: true # recursive scan for *_hammer.rb from here
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
```ruby
|
|
661
|
+
# tasks/db_hammer.rb
|
|
662
|
+
namespace :db do
|
|
663
|
+
define :migrate do
|
|
664
|
+
desc 'Run pending migrations'
|
|
665
|
+
opt :pretend, type: :boolean, alias: :p
|
|
666
|
+
proc { |o| say.green "migrating pretend=#{o[:pretend].inspect}" }
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
```ruby
|
|
672
|
+
# tasks/deploy_hammer.rb
|
|
673
|
+
define :deploy do
|
|
674
|
+
desc 'Deploy to prod'
|
|
675
|
+
proc do |_|
|
|
676
|
+
hammer_db_migrate # cross-file invocation just works
|
|
677
|
+
say.cyan 'deployed'
|
|
678
|
+
end
|
|
679
|
+
end
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### Call shapes
|
|
683
|
+
|
|
684
|
+
```ruby
|
|
685
|
+
load # same as load auto: true
|
|
686
|
+
load auto: true # recursive scan for *_hammer.rb under caller dir
|
|
687
|
+
load 'tasks/db_hammer.rb' # one file (path relative to caller)
|
|
688
|
+
load 'tasks/*_hammer.rb' # glob
|
|
689
|
+
load 'a.rb', 'b.rb' # several explicit paths
|
|
690
|
+
load 'tasks' # directory -> recursive scan under it (empty OK)
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
Paths resolve relative to the file calling `load`, not cwd. Inside a
|
|
694
|
+
`Hammerfile` that means "relative to the Hammerfile"; inside a class
|
|
695
|
+
body it means "relative to that file".
|
|
696
|
+
|
|
697
|
+
A directory argument is the explicit-anchor twin of `load auto: true` -
|
|
698
|
+
walks the directory for `*_hammer.rb` with the same skip rules. Useful
|
|
699
|
+
when you want to pull fragments from a known sibling tree without
|
|
700
|
+
making it the caller's dir.
|
|
701
|
+
|
|
702
|
+
### What's skipped
|
|
703
|
+
|
|
704
|
+
Auto-discovery walks recursively but skips `.git`, `.bundle`,
|
|
705
|
+
`node_modules`, `tmp`, `vendor`, `dist`, `build`, `coverage`, and any
|
|
706
|
+
hidden directory.
|
|
707
|
+
|
|
708
|
+
### Fragment shape
|
|
709
|
+
|
|
710
|
+
A `*_hammer.rb` file is a **block-DSL fragment** - same surface as a
|
|
711
|
+
`Hammerfile`: `define`, `namespace`, and nested `load`. Not a class
|
|
712
|
+
re-open. If you want to extend a `Hammer` subclass in the classic
|
|
713
|
+
`desc` + `def` style across files, use plain `require_relative`.
|
|
714
|
+
|
|
715
|
+
### Dedup and re-entrancy
|
|
716
|
+
|
|
717
|
+
Each file loads at most once per target class, keyed by absolute path.
|
|
718
|
+
A fragment can `load` other fragments without worrying about cycles.
|
|
719
|
+
|
|
720
|
+
### Errors
|
|
721
|
+
|
|
722
|
+
If a fragment raises during load, the error surfaces as
|
|
723
|
+
`failed loading <path>: <message>` so you know which file blew up.
|
|
724
|
+
An explicit pattern (`load 'x_hammer.rb'`, `load 'tasks/*.rb'`) that
|
|
725
|
+
matches zero files raises; auto-mode finding nothing is silent.
|
|
726
|
+
|
|
727
|
+
## Block DSL outside a Hammerfile
|
|
728
|
+
|
|
729
|
+
Same shape as a Hammerfile, just inline:
|
|
730
|
+
|
|
731
|
+
```ruby
|
|
732
|
+
require 'lux-hammer'
|
|
733
|
+
|
|
734
|
+
Hammer.run(ARGV) do
|
|
735
|
+
define :hello do
|
|
736
|
+
desc 'say hi'
|
|
737
|
+
opt :loud, type: :boolean, alias: :l
|
|
738
|
+
proc do |opts|
|
|
739
|
+
msg = "hello #{opts[:args].first || 'world'}"
|
|
740
|
+
msg = msg.upcase if opts[:loud]
|
|
741
|
+
say.cyan msg
|
|
742
|
+
end
|
|
743
|
+
end
|
|
744
|
+
end
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### `Hammer.run` without a block
|
|
748
|
+
|
|
749
|
+
If you call `Hammer.run(ARGV)` with no block, it does the obvious thing
|
|
750
|
+
relative to `Dir.pwd`:
|
|
751
|
+
|
|
752
|
+
* If `./Hammerfile` exists, it's evaluated as the block DSL.
|
|
753
|
+
* Otherwise, `*_hammer.rb` files under `Dir.pwd` are auto-discovered
|
|
754
|
+
(same walk + skip rules as `load auto: true`).
|
|
755
|
+
|
|
756
|
+
Either way ARGV is dispatched against the resulting CLI. Useful for a
|
|
757
|
+
one-line custom bin:
|
|
758
|
+
|
|
759
|
+
```ruby
|
|
760
|
+
#!/usr/bin/env ruby
|
|
761
|
+
require 'lux-hammer'
|
|
762
|
+
Hammer.run ARGV
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
For more control - e.g. loading a Hammerfile from a fixed path *and*
|
|
766
|
+
auto-discovering from cwd - pass a block and use `load` explicitly:
|
|
767
|
+
|
|
768
|
+
```ruby
|
|
769
|
+
Hammer.run ARGV do
|
|
770
|
+
load File.join(MY_ROOT, 'Hammerfile')
|
|
771
|
+
load Dir.pwd if Dir.pwd != MY_ROOT
|
|
772
|
+
end
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
## Complete example (every feature)
|
|
776
|
+
|
|
777
|
+
```ruby
|
|
778
|
+
# Simple top-level command
|
|
779
|
+
define :build do
|
|
780
|
+
desc 'Build the project'
|
|
781
|
+
example 'build prod -v'
|
|
782
|
+
example 'build --env=staging'
|
|
783
|
+
opt :verbose, type: :boolean, alias: :v, desc: 'verbose output'
|
|
784
|
+
opt :env, default: 'dev', desc: 'target env'
|
|
785
|
+
|
|
786
|
+
proc do |opts|
|
|
787
|
+
target = opts[:args].first || opts[:env]
|
|
788
|
+
say.green "building #{target}"
|
|
789
|
+
say ' verbose on' if opts[:verbose]
|
|
790
|
+
end
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
# Command that calls another command
|
|
794
|
+
define :deploy do
|
|
795
|
+
desc 'Deploy to URL'
|
|
796
|
+
alt :ship
|
|
797
|
+
opt :url, req: true
|
|
798
|
+
opt :force, type: :boolean
|
|
799
|
+
|
|
800
|
+
proc do |opts|
|
|
801
|
+
hammer_build(env: 'prod')
|
|
802
|
+
exit 0 unless yes? "deploy to #{opts[:url]}?" unless opts[:force]
|
|
803
|
+
say.yellow "deploying to #{opts[:url]}"
|
|
804
|
+
end
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
# Namespace with two levels of nesting
|
|
808
|
+
namespace :db do
|
|
809
|
+
define :migrate do
|
|
810
|
+
desc 'Run pending migrations'
|
|
811
|
+
alt :m
|
|
812
|
+
example 'db:migrate 3 --pretend'
|
|
813
|
+
opt :pretend, type: :boolean, alias: :p
|
|
814
|
+
|
|
815
|
+
proc do |opts|
|
|
816
|
+
step = opts[:args].first || 'all'
|
|
817
|
+
say.green "migrating #{step} pretend=#{opts[:pretend].inspect}"
|
|
818
|
+
end
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
namespace :users do
|
|
822
|
+
define :list do
|
|
823
|
+
desc 'List users'
|
|
824
|
+
opt :role, default: 'all'
|
|
825
|
+
opt :limit, type: :integer, default: 100
|
|
826
|
+
|
|
827
|
+
proc do |opts|
|
|
828
|
+
say.cyan "users role=#{opts[:role]} limit=#{opts[:limit]}"
|
|
829
|
+
end
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
define :create do
|
|
833
|
+
desc 'Create a user'
|
|
834
|
+
opt :email, req: true
|
|
835
|
+
opt :admin, type: :boolean
|
|
836
|
+
|
|
837
|
+
proc do |opts|
|
|
838
|
+
say "create #{opts[:email]} admin=#{opts[:admin]}"
|
|
839
|
+
end
|
|
840
|
+
end
|
|
841
|
+
end
|
|
842
|
+
end
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
```sh
|
|
846
|
+
$ hammer
|
|
847
|
+
Usage: demo COMMAND [ARGS]
|
|
848
|
+
|
|
849
|
+
Commands:
|
|
850
|
+
demo build # Build the project
|
|
851
|
+
demo deploy (alt: ship) # Deploy to URL
|
|
852
|
+
|
|
853
|
+
db:
|
|
854
|
+
demo db:migrate (alt: m) # Run pending migrations
|
|
855
|
+
|
|
856
|
+
db:users:
|
|
857
|
+
demo db:users:list # List users
|
|
858
|
+
demo db:users:create # Create a user
|
|
859
|
+
|
|
860
|
+
$ hammer build prod -v
|
|
861
|
+
building prod
|
|
862
|
+
verbose on
|
|
863
|
+
|
|
864
|
+
$ hammer deploy --url=https://example.com --force
|
|
865
|
+
building prod
|
|
866
|
+
deploying to https://example.com
|
|
867
|
+
|
|
868
|
+
$ hammer db:m 3 -p # alt 'm' + positional + short bool
|
|
869
|
+
migrating 3 pretend=true
|
|
870
|
+
|
|
871
|
+
$ hammer db:users:create --email=dino@example.com --admin
|
|
872
|
+
create dino@example.com admin=true
|
|
873
|
+
|
|
874
|
+
$ hammer db # bare namespace shows its contents
|
|
875
|
+
Usage: demo db:COMMAND [ARGS]
|
|
876
|
+
|
|
877
|
+
Commands:
|
|
878
|
+
demo db:migrate (alt: m) # Run pending migrations
|
|
879
|
+
|
|
880
|
+
users:
|
|
881
|
+
demo db:users:list # List users
|
|
882
|
+
demo db:users:create # Create a user
|
|
883
|
+
|
|
884
|
+
$ hammer db:users:create -h
|
|
885
|
+
Usage: demo db:users:create EMAIL [OPTIONS]
|
|
886
|
+
Create a user
|
|
887
|
+
|
|
888
|
+
Options:
|
|
889
|
+
--email EMAIL (required)
|
|
890
|
+
--admin
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
## Programmatic use
|
|
894
|
+
|
|
895
|
+
Outside a Hammerfile, you can build a `Hammer` subclass and run it
|
|
896
|
+
directly. Useful for embedding or testing:
|
|
897
|
+
|
|
898
|
+
```ruby
|
|
899
|
+
require 'lux-hammer'
|
|
900
|
+
|
|
901
|
+
class MyCli < Hammer
|
|
902
|
+
define :greet do
|
|
903
|
+
opt :loud, type: :boolean
|
|
904
|
+
proc do |opts|
|
|
905
|
+
msg = "hello #{opts[:args].first}"
|
|
906
|
+
say(opts[:loud] ? msg.upcase : msg)
|
|
907
|
+
end
|
|
908
|
+
end
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
MyCli.start(ARGV) # or:
|
|
912
|
+
MyCli.hammer_greet('dino', loud: true)
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
## Development
|
|
916
|
+
|
|
917
|
+
```sh
|
|
918
|
+
git clone https://github.com/dux/hammer
|
|
919
|
+
cd lux-hammer
|
|
920
|
+
bundle install
|
|
921
|
+
bundle exec rake test
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
Tests live in `test/` and use minitest. Run a single file with
|
|
925
|
+
`bundle exec ruby -Ilib -Itest test/parser_test.rb`.
|
|
926
|
+
|
|
927
|
+
## How hammer compares to Thor and Rake
|
|
928
|
+
|
|
929
|
+
Short version: hammer carves a sweet spot between the two. It's a tiny
|
|
930
|
+
CLI builder with Rake's namespacing and a cleaner DSL than Thor, plus a
|
|
931
|
+
few small things that have been bugging me about both for years.
|
|
932
|
+
|
|
933
|
+
### Versus Thor
|
|
934
|
+
|
|
935
|
+
| | Thor | hammer |
|
|
936
|
+
|-|-|-|
|
|
937
|
+
| Lines of code | ~6,000 | ~400 |
|
|
938
|
+
| Runtime deps | a few | zero |
|
|
939
|
+
| Root constants | `Thor`, `Thor::Group`, `Thor::Shell`, `Thor::Actions`, ... | just `Hammer` |
|
|
940
|
+
| Command DSL | `desc 'usage', 'help'` + `method_option` + `def name(arg)` | `define :name do ... proc do \|opts\| end end` (or classic `desc` + `def`) |
|
|
941
|
+
| Opts container | `Thor::CoreExt::HashWithIndifferentAccess` | plain `Hash` with symbol keys |
|
|
942
|
+
| Positional args | method positional params + `method_option`, two parallel systems | declared-order opts fill from positional, single system |
|
|
943
|
+
| Sub-namespaces | `register SubClass, 'name', '...'` (inheritance ceremony) | `namespace :name do ... end` (no classes needed) |
|
|
944
|
+
| Cross-invoke | `invoke 'name', [args], opts` | `hammer_name(*args, **kwargs)` (looks like a method call) |
|
|
945
|
+
| Inline CLI | class only | class DSL **or** `Hammer.run do ... end` block DSL **or** a `Hammerfile` |
|
|
946
|
+
|
|
947
|
+
**What hammer does better and why:**
|
|
948
|
+
|
|
949
|
+
* **One root constant.** Thor exposes `Thor`, `Thor::Group`, `Thor::Shell`,
|
|
950
|
+
`Thor::Actions` at the top level - Bundler had to vendor its own copy at
|
|
951
|
+
`Bundler::Thor` to avoid clashes. Hammer is just `Hammer`.
|
|
952
|
+
* **The opts hash is just a Hash.** Symbol keys, always. No magic accessor
|
|
953
|
+
object to remember, no string-vs-symbol confusion, no method_missing.
|
|
954
|
+
* **Positional args fill opts in declaration order.** Thor either forces
|
|
955
|
+
you into method params (which then clash with options) or makes you read
|
|
956
|
+
`ARGV` yourself. Hammer just says: opts you declared come first, leftover
|
|
957
|
+
goes to `opts[:args]`.
|
|
958
|
+
* **Cross-invocation reads as Ruby.** `hammer_db_migrate(env: 'prod')`
|
|
959
|
+
looks like a method call. Thor's `invoke('db:migrate', [], env: 'prod')`
|
|
960
|
+
always feels like reflection.
|
|
961
|
+
* **No generator complexity.** Thor's other half is file scaffolding and
|
|
962
|
+
ERB templates. If you don't need that (and most CLIs don't), Thor still
|
|
963
|
+
drags it along.
|
|
964
|
+
|
|
965
|
+
### Versus Rake
|
|
966
|
+
|
|
967
|
+
| | Rake | hammer |
|
|
968
|
+
|-|-|-|
|
|
969
|
+
| Primary use case | build/task automation with file deps | general CLIs |
|
|
970
|
+
| Task file | `Rakefile` | `Hammerfile` |
|
|
971
|
+
| Namespacing | colon paths (`db:migrate`) | colon paths (`db:migrate`) - parity |
|
|
972
|
+
| Per-task options | `task[a,b,c]` positional only | typed `opt`s with flags, aliases, defaults, required |
|
|
973
|
+
| Help | `rake -T` (plain list) | bare `hammer` lists everything grouped by namespace; `hammer X -h` for per-command help with examples and defaults |
|
|
974
|
+
| Cross-invoke | `Rake::Task['db:migrate'].invoke` | `hammer_db_migrate` |
|
|
975
|
+
| Prerequisites | `task :build => [:clean, :compile]` (declarative DAG) | explicit - call `hammer_clean; hammer_compile` in the proc |
|
|
976
|
+
| File tasks | yes (mtime-based) | no |
|
|
977
|
+
| Aliases | none (workarounds via re-defined tasks) | `alt :short_name` |
|
|
978
|
+
| Split across files | `import 'other.rake'` | `load auto: true` (or explicit paths/globs) |
|
|
979
|
+
|
|
980
|
+
**What hammer does better and why:**
|
|
981
|
+
|
|
982
|
+
* **Per-command options with types.** Rake's `task[a,b]` syntax is a
|
|
983
|
+
long-standing wart - no types, no validation, awkward to type in the
|
|
984
|
+
shell, no help. `opt :port, type: :integer, default: 3000` is what
|
|
985
|
+
every CLI library has converged on.
|
|
986
|
+
* **Help is actually useful.** `hammer build -h` shows usage, options
|
|
987
|
+
with defaults and required markers, and examples. `rake -T` is just a
|
|
988
|
+
list of one-liners.
|
|
989
|
+
* **Command aliases.** `alt :m` for `db:migrate` is two characters of
|
|
990
|
+
declaration. Rake makes you redefine the task or use prerequisites.
|
|
991
|
+
* **CLI semantics.** Rake assumes "build artifacts from sources"; it's
|
|
992
|
+
great at that. Hammer assumes "give me commands with arguments and
|
|
993
|
+
flags"; it's better at that.
|
|
994
|
+
|
|
995
|
+
**What Rake does better:**
|
|
996
|
+
|
|
997
|
+
* **File tasks with mtime tracking.** `file 'foo.o' => 'foo.c' do ... end`
|
|
998
|
+
skips work when the target is newer than the source. Genuine win for
|
|
999
|
+
compilation pipelines. Hammer doesn't have this and isn't going to -
|
|
1000
|
+
it's not what a CLI builder is for.
|
|
1001
|
+
|
|
1002
|
+
### When to pick which
|
|
1003
|
+
|
|
1004
|
+
* **CLI for a tool, app, or service** (run servers, manage data, ship
|
|
1005
|
+
releases, scripts your team uses) - **hammer**.
|
|
1006
|
+
* **Build pipeline with file-mtime dependencies** (compiling assets,
|
|
1007
|
+
generating code, classic Make-style work) - **Rake**.
|
|
1008
|
+
* **Need to ship file generators / templates** (Rails-style
|
|
1009
|
+
scaffolding) - **Thor**.
|
|
1010
|
+
|
|
1011
|
+
## License
|
|
1012
|
+
|
|
1013
|
+
MIT - see [LICENSE](LICENSE).
|