lux-hammer 0.3.17 → 0.3.20
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 -1
- data/AGENTS.md +16 -6
- data/README.md +83 -13
- data/lib/hammer/builder.rb +6 -1
- data/lib/hammer/input.rb +121 -0
- data/lib/hammer/option.rb +23 -9
- data/lib/hammer/parser.rb +12 -14
- data/lib/lux-hammer.rb +98 -16
- data/recipes/lib/deploy/commands.rb +1 -1
- data/recipes/lib/deploy/hammer.rb +1 -1
- data/recipes/lib/llm/usage.rb +353 -16
- data/recipes/lib/llm/wrap.rb +691 -0
- data/recipes/llm.rb +97 -13
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0547c6b7b08fe92297694e5d3f19d4726bc3ec3781e9a7169bfeefd1191f7cdd
|
|
4
|
+
data.tar.gz: e2c8bd882af53a49a24e881ec2329241430c63ec1d6a9f90dd310c4e258364ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 94bf60197a56713381b1201a8344941595083770c4b3d8d753b624d455326f7cf5208923d49f3046d436bbfe560f28bbfc97bcfe44f5cf4fe85657b856e5f9e8
|
|
7
|
+
data.tar.gz: 5d5d8d807fd5dee383a528d0e8b38d8a275b4d553ac873c30e703641ed6d85d2f203fbd604384624c25c03f4971b4eb51e3d9c28145a0af06f5067cb189083f9
|
data/.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3.
|
|
1
|
+
0.3.20
|
data/AGENTS.md
CHANGED
|
@@ -38,7 +38,8 @@ usable as a library (`require 'lux-hammer'`, subclass `Hammer`, call
|
|
|
38
38
|
bin/hammer # CLI entry point
|
|
39
39
|
lib/lux-hammer.rb # Entry - defines class Hammer and its DSL
|
|
40
40
|
lib/hammer/shell.rb # ANSI/IO helpers
|
|
41
|
-
lib/hammer/option.rb # One option definition
|
|
41
|
+
lib/hammer/option.rb # One option definition (types: string/boolean/integer/float/array/json)
|
|
42
|
+
lib/hammer/input.rb # opts[:stdin], prepare_json!, JSON @file / pipe helpers
|
|
42
43
|
lib/hammer/parser.rb # ARGV -> [positional, opts_hash]
|
|
43
44
|
lib/hammer/command.rb # One registered command (name, opts, alts, handler)
|
|
44
45
|
lib/hammer/cron.rb # `cron '<expr>'` schedule parser (cron/interval/@shortcut)
|
|
@@ -72,12 +73,19 @@ Inside a `task :name do ... end` block (CommandBuilder context):
|
|
|
72
73
|
the brief shown in listings, full text renders in per-command help)
|
|
73
74
|
* `example 'invocation example'` (callable many times)
|
|
74
75
|
* `opt :name, type:, default:, alias:, desc:, req:` - any other kwarg
|
|
75
|
-
raises `Hammer::Error` ("unknown opt parameter(s)")
|
|
76
|
+
raises `Hammer::Error` ("unknown opt parameter(s)"). Types include
|
|
77
|
+
`:string`, `:boolean`, `:integer`, `:float`, `:array`, `:json`
|
|
78
|
+
(`:json` is flag/file/pipe only — never bare positional).
|
|
79
|
+
* `global_opt :name, ...` - same kwargs as `opt`, available on every
|
|
80
|
+
command under the declaring class/namespace (help: "Global options").
|
|
76
81
|
* `alt :other_name` (callable many times)
|
|
77
82
|
* `needs :other_cmd, 'ns:cmd'` - prereqs run before the handler;
|
|
78
83
|
resolved against root (same lookup as `hammer`), deduped per
|
|
79
84
|
top-level `start` so each prereq fires at most once. Dedupe also
|
|
80
85
|
spans `+`-chained segments. Unknown prereq raises `Hammer::Error`.
|
|
86
|
+
* `opts[:stdin]` is always attached in `run_command` (piped non-TTY
|
|
87
|
+
body or `nil`). JSON body coercion is opt-in via
|
|
88
|
+
`Hammer.prepare_json!(opts)` (usually from `before`).
|
|
81
89
|
* `cron '<expr>'` - schedule for the `h:cron` job server. Accepts
|
|
82
90
|
5-field cron (`'*/10 * * * *'`), an interval (`'10s'`, `'10m'`,
|
|
83
91
|
`'2h'`, `'1d'`, counted from the last run) or `@hourly` / `@daily` /
|
|
@@ -217,10 +225,12 @@ explicit ADR-level discussion. Keys:
|
|
|
217
225
|
`> prog cmd --opt=val ARG` banner so it's visible which command was
|
|
218
226
|
actually picked when fuzzy matching kicks in. Only options that
|
|
219
227
|
differ from their default are listed; booleans render as `--flag`
|
|
220
|
-
|
|
221
|
-
before the banner. Set `HAMMER_QUIET=1` to suppress
|
|
222
|
-
globally - useful when a task writes machine-readable
|
|
223
|
-
stdout (e.g. a JSON-emitting Claude Code / Codex hook).
|
|
228
|
+
when true (no auto `--no-flag`). Help paths (`-h`, bare namespace)
|
|
229
|
+
short-circuit before the banner. Set `HAMMER_QUIET=1` to suppress
|
|
230
|
+
the banner globally - useful when a task writes machine-readable
|
|
231
|
+
output to stdout (e.g. a JSON-emitting Claude Code / Codex hook).
|
|
232
|
+
Boolean opts: presence of the switch/alias sets true; there is no
|
|
233
|
+
auto-negation from a `--no-` prefix.
|
|
224
234
|
* There is **no per-level dispatch**. A namespace is a container, not a
|
|
225
235
|
CLI of its own. Do not reintroduce `subclass.start(remaining_argv)`.
|
|
226
236
|
* `start(argv)` is a two-step pipeline: `split_chain(argv)` (private)
|
data/README.md
CHANGED
|
@@ -274,11 +274,14 @@ Not supported: attached short form (`-p3000`), combined short flags
|
|
|
274
274
|
For boolean options:
|
|
275
275
|
|
|
276
276
|
```
|
|
277
|
-
--verbose # set to true
|
|
278
|
-
--no-verbose # set to false (only if a default of true is in play)
|
|
277
|
+
--verbose # set to true (presence alone)
|
|
279
278
|
-v # short alias if declared
|
|
279
|
+
--no-reset # also true if the opt is named :no_reset
|
|
280
280
|
```
|
|
281
281
|
|
|
282
|
+
There is **no** auto `--no-X` negation. Name the flag what you mean
|
|
283
|
+
(`:verbose`, `:no_reset`, `:force`).
|
|
284
|
+
|
|
282
285
|
### Per-type behavior
|
|
283
286
|
|
|
284
287
|
#### `:string` (default)
|
|
@@ -295,19 +298,20 @@ hammer build # opts[:env] = "dev" (default)
|
|
|
295
298
|
#### `:boolean`
|
|
296
299
|
|
|
297
300
|
```ruby
|
|
298
|
-
opt :verbose,
|
|
299
|
-
opt :
|
|
301
|
+
opt :verbose, type: :boolean, alias: :v
|
|
302
|
+
opt :no_reset, type: :boolean
|
|
303
|
+
opt :cache, type: :boolean, default: true
|
|
300
304
|
```
|
|
301
305
|
```
|
|
302
|
-
hammer build -v # opts[:verbose]
|
|
303
|
-
hammer build --verbose # opts[:verbose]
|
|
304
|
-
hammer build --no-
|
|
305
|
-
hammer build # opts[:cache]
|
|
306
|
-
# opts[:verbose]
|
|
306
|
+
hammer build -v # opts[:verbose] = true
|
|
307
|
+
hammer build --verbose # opts[:verbose] = true
|
|
308
|
+
hammer build --no-reset # opts[:no_reset] = true (name is the flag)
|
|
309
|
+
hammer build # opts[:cache] = true (default)
|
|
310
|
+
# opts[:verbose] = nil (no default)
|
|
307
311
|
```
|
|
308
312
|
|
|
309
|
-
Booleans never consume a positional.
|
|
310
|
-
|
|
313
|
+
Booleans never consume a positional. Presence → true; omitted → default
|
|
314
|
+
(or nil). Use `--flag=false` only if you need an explicit false value.
|
|
311
315
|
|
|
312
316
|
#### `:integer`
|
|
313
317
|
|
|
@@ -500,6 +504,72 @@ namespace :db do
|
|
|
500
504
|
end
|
|
501
505
|
```
|
|
502
506
|
|
|
507
|
+
## Stdin, JSON input, and global options
|
|
508
|
+
|
|
509
|
+
Every command receives **`opts[:stdin]`** automatically when stdin is a
|
|
510
|
+
pipe (non-TTY) and non-empty; otherwise it is `nil`. Interactive TTYs are
|
|
511
|
+
never consumed.
|
|
512
|
+
|
|
513
|
+
```ruby
|
|
514
|
+
task :count do
|
|
515
|
+
proc { |o| say "bytes=#{o[:stdin]&.bytesize || 0}" }
|
|
516
|
+
end
|
|
517
|
+
# cat file.txt | hammer count
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### `type: :json` and `Hammer.prepare_json!`
|
|
521
|
+
|
|
522
|
+
For JSON bodies, declare `type: :json` and (optionally) coerce in a
|
|
523
|
+
`before` hook so pipes, `@file`, `-`, and inline JSON all land as a
|
|
524
|
+
**Hash/Array** on the same key:
|
|
525
|
+
|
|
526
|
+
```ruby
|
|
527
|
+
global_opt :json, type: :json, desc: 'JSON body (inline, @file, -, or pipe)'
|
|
528
|
+
before { |o| Hammer.prepare_json!(o) } # fills o[:json] from stdin when unset
|
|
529
|
+
|
|
530
|
+
task :create do
|
|
531
|
+
opt :name
|
|
532
|
+
proc do |o|
|
|
533
|
+
body = o[:json] || {}
|
|
534
|
+
body[:name] ||= o[:name]
|
|
535
|
+
# body is a Hash with symbol keys
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Supply the body any of these ways (equivalent when `prepare_json!` runs):
|
|
541
|
+
|
|
542
|
+
```sh
|
|
543
|
+
hammer create --json '{"name":"Ship"}'
|
|
544
|
+
hammer create --json @payload.json
|
|
545
|
+
cat payload.json | hammer create
|
|
546
|
+
cat payload.json | hammer create --json - # explicit stdin
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Rules for `Hammer.prepare_json!(opts, key: :json)`:
|
|
550
|
+
|
|
551
|
+
1. Already a Hash/Array → leave (symbolize keys).
|
|
552
|
+
2. String → parse inline JSON, `@path`, or `-` (stdin).
|
|
553
|
+
3. `nil` + stdin looks like `{` / `[` → parse stdin into `opts[key]`.
|
|
554
|
+
4. Boolean left alone (legacy `opt :json, type: :boolean` output flags).
|
|
555
|
+
|
|
556
|
+
`type: :json` **never** takes a bare positional (flag / file / pipe only),
|
|
557
|
+
so it won't steal `REF` from `update REF`.
|
|
558
|
+
|
|
559
|
+
### `global_opt`
|
|
560
|
+
|
|
561
|
+
Register options once at root (or a namespace); every command under that
|
|
562
|
+
scope accepts them. Shown under **Global options:** in help. Declared
|
|
563
|
+
**after** per-task opts for positional fill order.
|
|
564
|
+
|
|
565
|
+
```ruby
|
|
566
|
+
global_opt :json, type: :json
|
|
567
|
+
global_opt :as_json, type: :boolean, alias: :j, default: false,
|
|
568
|
+
desc: 'Force JSON on stdout'
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
Helpers: `Hammer.prepare_json!`, `Hammer.attach_stdin!`, `Hammer::Input.*`.
|
|
572
|
+
|
|
503
573
|
`.env` and `.env.local` next to the `Hammerfile` are loaded
|
|
504
574
|
automatically by the `hammer` binary - no `before` hook needed.
|
|
505
575
|
Shell-set vars are never overwritten, and `.env.local` overrides
|
|
@@ -794,10 +864,10 @@ positionals become positional ARGV; kwargs become CLI flags:
|
|
|
794
864
|
* `hammer 'db:users:list'` → `db:users:list`
|
|
795
865
|
* `hammer :evaluate, 'puts 42'` → `evaluate "puts 42"` (positional ARGV)
|
|
796
866
|
* `verbose: true` → `--verbose`
|
|
797
|
-
* `
|
|
867
|
+
* `no_reset: true` → `--no-reset` (underscores in the key become dashes)
|
|
798
868
|
* `dry_run: true` → `--dry-run`
|
|
799
869
|
* `env: 'prod'` → `--env=prod`
|
|
800
|
-
* `anything: false` → skipped (
|
|
870
|
+
* `anything: false` → skipped (false flags are omitted)
|
|
801
871
|
|
|
802
872
|
`MyCli.hammer 'db:users:list', verbose: true` also works at the
|
|
803
873
|
class level, useful for tests and scripting.
|
data/lib/hammer/builder.rb
CHANGED
|
@@ -45,6 +45,11 @@ class Hammer
|
|
|
45
45
|
@klass.before(&block)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Options available on every command (see Hammer.global_opt).
|
|
49
|
+
def global_opt(name, **opts)
|
|
50
|
+
@klass.global_opt(name, **opts)
|
|
51
|
+
end
|
|
52
|
+
|
|
48
53
|
# Opt out of auto `.env` loading in `Hammer.cli`. Default is on.
|
|
49
54
|
def dotenv(flag = true)
|
|
50
55
|
@klass.dotenv(flag)
|
|
@@ -75,7 +80,7 @@ class Hammer
|
|
|
75
80
|
# still call `task`, `namespace`, and `before`. Delegates to whichever
|
|
76
81
|
# Hammer subclass is currently being evaluated.
|
|
77
82
|
module DSL
|
|
78
|
-
%i[task namespace before dotenv].each do |m|
|
|
83
|
+
%i[task namespace before dotenv global_opt].each do |m|
|
|
79
84
|
define_method(m) do |*args, &block|
|
|
80
85
|
target = Thread.current[:hammer_target]
|
|
81
86
|
raise Hammer::Error, "`#{m}` called outside a Hammer context " \
|
data/lib/hammer/input.rb
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
class Hammer
|
|
6
|
+
# Stdin + JSON helpers for opts. Hammer always attaches piped stdin as
|
|
7
|
+
# `opts[:stdin]`; recipes opt into JSON body handling via
|
|
8
|
+
# `Hammer::Input.prepare_json!` (typically from a `before` hook) and/or
|
|
9
|
+
# `opt :json, type: :json` / `global_opt :json, type: :json`.
|
|
10
|
+
module Input
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Read piped stdin once. Returns nil when stdin is a TTY or empty.
|
|
14
|
+
# Does not consume an interactive TTY.
|
|
15
|
+
def read_stdin
|
|
16
|
+
return nil if $stdin.closed?
|
|
17
|
+
return nil if $stdin.tty?
|
|
18
|
+
|
|
19
|
+
data = $stdin.read
|
|
20
|
+
return nil if data.nil?
|
|
21
|
+
|
|
22
|
+
data = data.dup.force_encoding(Encoding::UTF_8) if data.respond_to?(:force_encoding)
|
|
23
|
+
data.empty? ? nil : data
|
|
24
|
+
rescue StandardError
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Idempotent: sets opts[:stdin] if not already present.
|
|
29
|
+
def attach_stdin!(opts)
|
|
30
|
+
return opts[:stdin] if opts.key?(:stdin)
|
|
31
|
+
|
|
32
|
+
opts[:stdin] = read_stdin
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Parse a JSON source string into a Hash/Array with symbol keys.
|
|
36
|
+
# source is only used in error messages ('stdin', '--json', path, ...).
|
|
37
|
+
def parse_json(raw, source: 'json')
|
|
38
|
+
data = JSON.parse(raw)
|
|
39
|
+
symbolize(data)
|
|
40
|
+
rescue JSON::ParserError => e
|
|
41
|
+
raise Hammer::Parser::Error, "invalid JSON (#{source}): #{e.message}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Resolve a value that may be:
|
|
45
|
+
# - Hash / Array -> returned as-is (symbolized if Hash has string keys)
|
|
46
|
+
# - "-" -> use opts[:stdin]
|
|
47
|
+
# - "@path" -> File.read(path)
|
|
48
|
+
# - JSON string -> parse
|
|
49
|
+
def resolve_json_value(value, opts = {}, source: 'json')
|
|
50
|
+
return symbolize(value) if value.is_a?(Hash) || value.is_a?(Array)
|
|
51
|
+
|
|
52
|
+
s = value.to_s
|
|
53
|
+
if s == '-'
|
|
54
|
+
attach_stdin!(opts)
|
|
55
|
+
raw = opts[:stdin]
|
|
56
|
+
raise Hammer::Parser::Error, "no JSON on stdin for #{source}" if raw.nil? || raw.empty?
|
|
57
|
+
|
|
58
|
+
return parse_json(raw, source: 'stdin')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if s.start_with?('@')
|
|
62
|
+
path = s[1..]
|
|
63
|
+
raise Hammer::Parser::Error, "JSON file not found: #{path}" unless File.file?(path)
|
|
64
|
+
|
|
65
|
+
return parse_json(File.read(path), source: path)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
parse_json(s, source: source)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Prepare opts[key] as a Hash/Array when possible.
|
|
72
|
+
#
|
|
73
|
+
# 1. If opts[key] is already Hash/Array -> symbolize, done
|
|
74
|
+
# 2. If opts[key] is a String ("...", @file, -) -> parse
|
|
75
|
+
# 3. If opts[key] is nil/absent and opts[:stdin] looks like JSON
|
|
76
|
+
# object/array -> fill from stdin
|
|
77
|
+
# 4. Boolean true/false left alone (legacy `opt :json, type: :boolean`)
|
|
78
|
+
#
|
|
79
|
+
# Returns the resolved value (or nil). Mutates opts.
|
|
80
|
+
def prepare_json!(opts, key: :json)
|
|
81
|
+
attach_stdin!(opts)
|
|
82
|
+
key = key.to_sym
|
|
83
|
+
val = opts[key]
|
|
84
|
+
|
|
85
|
+
if val.is_a?(Hash) || val.is_a?(Array)
|
|
86
|
+
opts[key] = symbolize(val)
|
|
87
|
+
return opts[key]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if val.is_a?(String) && !val.empty?
|
|
91
|
+
opts[key] = resolve_json_value(val, opts, source: "--#{key.to_s.tr('_', '-')}")
|
|
92
|
+
return opts[key]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Skip booleans (output-mode flags) and other non-nil junk.
|
|
96
|
+
return val unless val.nil?
|
|
97
|
+
|
|
98
|
+
raw = opts[:stdin]
|
|
99
|
+
return nil if raw.nil? || raw.empty?
|
|
100
|
+
|
|
101
|
+
stripped = raw.lstrip
|
|
102
|
+
return nil unless stripped.start_with?('{', '[')
|
|
103
|
+
|
|
104
|
+
opts[key] = parse_json(raw, source: 'stdin')
|
|
105
|
+
opts[key]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def symbolize(obj)
|
|
109
|
+
case obj
|
|
110
|
+
when Hash
|
|
111
|
+
obj.each_with_object({}) do |(k, v), h|
|
|
112
|
+
h[k.respond_to?(:to_sym) ? k.to_sym : k] = symbolize(v)
|
|
113
|
+
end
|
|
114
|
+
when Array
|
|
115
|
+
obj.map { |v| symbolize(v) }
|
|
116
|
+
else
|
|
117
|
+
obj
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
data/lib/hammer/option.rb
CHANGED
|
@@ -3,7 +3,7 @@ class Hammer
|
|
|
3
3
|
class Option
|
|
4
4
|
attr_reader :name, :type, :default, :desc, :aliases, :required, :placeholder
|
|
5
5
|
|
|
6
|
-
ALLOWED_KEYS ||= %i[type default desc alias req placeholder].freeze
|
|
6
|
+
ALLOWED_KEYS ||= %i[type default desc alias req placeholder positional].freeze
|
|
7
7
|
RESERVED_FLAGS ||= %w[-h --help].freeze
|
|
8
8
|
|
|
9
9
|
def initialize(name, **opts)
|
|
@@ -22,6 +22,11 @@ class Hammer
|
|
|
22
22
|
# Custom usage placeholder, e.g. `placeholder: 't/f'` -> `--log t/f`.
|
|
23
23
|
# Falls back to uppercased name (`--log LOG`) when nil.
|
|
24
24
|
@placeholder = opts[:placeholder]
|
|
25
|
+
# `positional: false` means "flag form only" - the option never claims a
|
|
26
|
+
# bare positional. For commands that forward their positionals somewhere
|
|
27
|
+
# else (`llm wrap --lines 3 -- claude`), the default fill would otherwise
|
|
28
|
+
# swallow the first one.
|
|
29
|
+
@positional = opts.key?(:positional) ? !!opts[:positional] : true
|
|
25
30
|
|
|
26
31
|
# Reserve -h / --help so every command supports them uniformly.
|
|
27
32
|
if RESERVED_FLAGS.include?(switch)
|
|
@@ -47,22 +52,33 @@ class Hammer
|
|
|
47
52
|
"--#{name.to_s.tr('_', '-')}"
|
|
48
53
|
end
|
|
49
54
|
|
|
50
|
-
def negation
|
|
51
|
-
"--no-#{name.to_s.tr('_', '-')}"
|
|
52
|
-
end
|
|
53
|
-
|
|
54
55
|
def cast(value)
|
|
55
56
|
case type
|
|
56
57
|
when :boolean then !!value && value != 'false' && value != '0'
|
|
57
58
|
when :integer then Integer(value)
|
|
58
59
|
when :float then Float(value)
|
|
59
60
|
when :array then value.is_a?(Array) ? value : value.to_s.split(',')
|
|
61
|
+
when :json
|
|
62
|
+
# Hash/Array pass through. Strings may be inline JSON, @file, or "-"
|
|
63
|
+
# (stdin resolved later by Hammer::Input.prepare_json!). "@" and
|
|
64
|
+
# inline JSON are parsed here so opts[:x] is already a Hash in
|
|
65
|
+
# the handler when the flag is used.
|
|
66
|
+
return value if value.is_a?(Hash) || value.is_a?(Array)
|
|
67
|
+
s = value.to_s
|
|
68
|
+
return s if s == '-' || s.empty?
|
|
69
|
+
Hammer::Input.resolve_json_value(s, {}, source: switch)
|
|
60
70
|
else value.to_s
|
|
61
71
|
end
|
|
62
72
|
rescue ArgumentError, TypeError
|
|
63
73
|
raise Hammer::Parser::Error, "invalid #{type} value for --#{name}: #{value.inspect}"
|
|
64
74
|
end
|
|
65
75
|
|
|
76
|
+
# JSON bodies are flags / stdin / @file — never bare positionals. Anything
|
|
77
|
+
# declared `positional: false` opts out the same way.
|
|
78
|
+
def skip_positional_fill?
|
|
79
|
+
type == :json || !@positional
|
|
80
|
+
end
|
|
81
|
+
|
|
66
82
|
def usage
|
|
67
83
|
flag = switch
|
|
68
84
|
flag += " #{placeholder || name.to_s.upcase}" unless boolean?
|
|
@@ -81,9 +97,9 @@ class Hammer
|
|
|
81
97
|
|
|
82
98
|
# Structured form for JSON export (`h:json`). The GUI maps `type`
|
|
83
99
|
# to a form widget; `default`/`required`/`desc` decorate it. Mirrors
|
|
84
|
-
# the data behind `usage`.
|
|
100
|
+
# the data behind `usage`.
|
|
85
101
|
def to_h
|
|
86
|
-
|
|
102
|
+
{
|
|
87
103
|
name: name.to_s,
|
|
88
104
|
type: type.to_s,
|
|
89
105
|
default: default,
|
|
@@ -94,8 +110,6 @@ class Hammer
|
|
|
94
110
|
aliases: aliases,
|
|
95
111
|
usage: usage.strip
|
|
96
112
|
}
|
|
97
|
-
h[:negation] = negation if boolean?
|
|
98
|
-
h
|
|
99
113
|
end
|
|
100
114
|
end
|
|
101
115
|
end
|
data/lib/hammer/parser.rb
CHANGED
|
@@ -5,11 +5,10 @@ class Hammer
|
|
|
5
5
|
Error ||= Class.new(StandardError)
|
|
6
6
|
|
|
7
7
|
def initialize(options)
|
|
8
|
-
@options
|
|
9
|
-
@by_switch
|
|
8
|
+
@options = options
|
|
9
|
+
@by_switch = {}
|
|
10
10
|
options.each do |opt|
|
|
11
11
|
register_switch(opt.switch, opt)
|
|
12
|
-
register_switch(opt.negation, opt) if opt.boolean?
|
|
13
12
|
opt.aliases.each { |a| register_switch(a, opt) }
|
|
14
13
|
end
|
|
15
14
|
end
|
|
@@ -31,11 +30,7 @@ class Hammer
|
|
|
31
30
|
if token.start_with?('--') && token.include?('=')
|
|
32
31
|
key, val = token.split('=', 2)
|
|
33
32
|
opt = lookup!(key)
|
|
34
|
-
|
|
35
|
-
values[opt.name] = !opt.cast(val) # `--no-x=false` -> true
|
|
36
|
-
else
|
|
37
|
-
values[opt.name] = opt.cast(val)
|
|
38
|
-
end
|
|
33
|
+
values[opt.name] = opt.cast(val)
|
|
39
34
|
i += 1
|
|
40
35
|
next
|
|
41
36
|
end
|
|
@@ -43,7 +38,8 @@ class Hammer
|
|
|
43
38
|
if @by_switch.key?(token)
|
|
44
39
|
opt = @by_switch[token]
|
|
45
40
|
if opt.boolean?
|
|
46
|
-
|
|
41
|
+
# Presence alone is enough — the flag is true regardless of name.
|
|
42
|
+
values[opt.name] = true
|
|
47
43
|
i += 1
|
|
48
44
|
else
|
|
49
45
|
val = argv[i + 1]
|
|
@@ -74,12 +70,14 @@ class Hammer
|
|
|
74
70
|
end
|
|
75
71
|
|
|
76
72
|
# Fill un-set non-boolean opts from positional args in declaration
|
|
77
|
-
# order. Booleans always need an explicit flag.
|
|
78
|
-
# positional
|
|
79
|
-
#
|
|
73
|
+
# order. Booleans always need an explicit flag. type: :json and
|
|
74
|
+
# `positional: false` are flag-only (skip_positional_fill?). Scalars
|
|
75
|
+
# take one positional each; an :array opt slurps whatever's left,
|
|
76
|
+
# so it's filled last
|
|
77
|
+
# and never starves a later-declared scalar opt.
|
|
80
78
|
array_opt = nil
|
|
81
79
|
@options.each do |opt|
|
|
82
|
-
next if opt.boolean? || values.key?(opt.name)
|
|
80
|
+
next if opt.boolean? || opt.skip_positional_fill? || values.key?(opt.name)
|
|
83
81
|
if opt.type == :array
|
|
84
82
|
array_opt = opt
|
|
85
83
|
next
|
|
@@ -102,7 +100,7 @@ class Hammer
|
|
|
102
100
|
private
|
|
103
101
|
|
|
104
102
|
# Map a flag string to its option, refusing silent shadowing when two
|
|
105
|
-
# options would claim the same switch/
|
|
103
|
+
# options would claim the same switch/alias.
|
|
106
104
|
def register_switch(flag, opt)
|
|
107
105
|
if (prev = @by_switch[flag]) && prev != opt
|
|
108
106
|
raise Error, "flag #{flag} claimed by both :#{prev.name} and :#{opt.name}"
|