Class: Hammer
- Inherits:
-
Object
- Object
- Hammer
- Includes:
- Shell
- Defined in:
- lib/lux-hammer.rb,
lib/hammer/shell.rb,
lib/hammer/loader.rb,
lib/hammer/option.rb,
lib/hammer/parser.rb,
lib/hammer/builder.rb,
lib/hammer/command.rb,
lib/hammer/command_builder.rb
Overview
Thor-inspired tiny CLI builder.
Class DSL:
class MyCli < Hammer
define :build do
desc 'Build the project'
example 'build -v --env=prod'
opt :verbose, type: :boolean, alias: :v
opt :env, type: :string, default: 'dev'
proc do |opts|
say.green "building #{opts[:env]} args=#{opts[:args].inspect}"
end
end
end
MyCli.start(ARGV)
Block DSL is identical, just inside ‘Hammer.run`:
Hammer.run(ARGV) do
define :hello do
desc 'Greet someone'
opt :loud, type: :boolean, alias: :l
proc do |opts|
msg = "hello #{opts[:args].first || 'world'}"
msg = msg.upcase if opts[:loud]
say msg, :cyan
end
end
end
Defined Under Namespace
Modules: Shell Classes: Builder, Command, CommandBuilder, Loader, Option, Parser
Class Method Summary collapse
- .alt(*names) ⇒ Object
-
.ancestor_chain ⇒ Object
Root -> …
-
.before(&block) ⇒ Object
Register a hook to run before every command in this class (root or namespace).
- .before_hooks ⇒ Object
-
.cli(argv = ARGV) ⇒ Object
Entry point for the ‘hammer` binary.
- .commands ⇒ Object
-
.default_program_name ⇒ Object
Program name shown in help/usage: the invocation path relative to cwd if the script lives inside it (e.g. ‘bin/foo` when invoked from the project root), otherwise the basename (e.g. `lux` for a globally installed bin in PATH).
-
.define(name, &block) ⇒ Object
Define a command.
-
.desc(text) ⇒ Object
—– class-level DSL for ‘def`-style commands ——————— Set pending metadata that the next `def` will consume.
-
.each_command(prefix = nil, &block) ⇒ Object
Yield [full_colon_path, Command] for every command in this class and all nested namespaces.
- .emit_rows(rows, width) ⇒ Object
- .example(text) ⇒ Object
-
.find_command(name) ⇒ Object
Find a command by canonical name or alt within this class.
-
.find_hammerfile(start) ⇒ Object
Walk up the directory tree looking for a Hammerfile.
-
.hammer(name, *args, **opts) ⇒ Object
Programmatic dispatch by name.
- .help_requested?(argv) ⇒ Boolean
- .inherited(sub) ⇒ Object
-
.label_for(full, cmd) ⇒ Object
“db:migrate” or “db:migrate (alt: m)”.
-
.load(*paths, **kwargs) ⇒ Object
Load Hammerfile fragments and register their commands on this class.
-
.loader ⇒ Object
Per-target Loader instance.
- .method_added(method_name) ⇒ Object
-
.namespace(name, &block) ⇒ Object
Open a namespace (group of commands).
- .namespaces ⇒ Object
- .needs(*names) ⇒ Object
- .opt(name, **o) ⇒ Object
- .parent ⇒ Object
- .print_command_help(cmd, full = nil) ⇒ Object
- .print_command_list(klass, prefix = nil) ⇒ Object
- .print_footer ⇒ Object
- .print_help(target = nil) ⇒ Object
- .print_namespace_help(prefix, ns) ⇒ Object
-
.program_name ⇒ Object
Resolved lazily on first read and memoized, so callers that need the cwd-relative form (see ‘default_program_name`) can warm the cache before chdir-ing elsewhere.
-
.resolve(path) ⇒ Object
Walk “ns1:ns2:cmd” -> [command, owning_class].
-
.resolve_namespace(path) ⇒ Object
Walk “ns1:ns2” -> namespace class, or nil if any segment missing.
-
.root ⇒ Object
Topmost class in this CLI tree.
-
.run(argv = ARGV, &block) ⇒ Object
Define and run a CLI inline.
-
.run_before_hooks(instance, opts) ⇒ Object
Fire ‘before` hooks from root down through the namespace chain.
- .run_command(cmd, argv, full: nil) ⇒ Object
-
.run_needs(cmd) ⇒ Object
Dispatch a command’s declared ‘needs` through the root class, with per-invocation dedupe.
-
.section_for(full, prefix) ⇒ Object
‘db’ for ‘db:migrate’ or ‘db:users:list’ viewed from root; ‘users’ for ‘db:users:list’ viewed from ‘db’; :root if the command sits at the view’s top level.
-
.start(argv = ARGV) ⇒ Object
Entry point.
-
.usage_signature(cmd) ⇒ Object
“ URL [ENV] [OPTIONS]” - shows the positional-fill names for declared non-boolean opts (required bare, optional bracketed), plus a generic [OPTIONS] tail if any flags exist.
Instance Method Summary collapse
-
#hammer(name, *args, **opts) ⇒ Object
Inside a command’s ‘proc do |opts| …
Methods included from Shell
ask, choose, choose_numbered, color!, color?, error, paint, print_error, say, sh, yes?
Class Method Details
.alt(*names) ⇒ Object
74 |
# File 'lib/lux-hammer.rb', line 74 def alt(*names) ; @pending_alts.concat(names) end |
.ancestor_chain ⇒ Object
Root -> … -> self. Used to gather ‘before` hooks for a command.
207 208 209 210 211 212 213 214 215 |
# File 'lib/lux-hammer.rb', line 207 def ancestor_chain chain = [] klass = self while klass chain.unshift klass klass = klass.parent end chain end |
.before(&block) ⇒ Object
Register a hook to run before every command in this class (root or namespace). Hooks receive the command’s ‘opts` hash. All hooks run outer -> inner, once per top-level `start` (prereqs don’t re-trigger).
before { |opts| Dotenv.load }
namespace :db do
before { hammer :env }
define :migrate do ... end
end
194 195 196 |
# File 'lib/lux-hammer.rb', line 194 def before(&block) before_hooks << block end |
.before_hooks ⇒ Object
198 199 200 |
# File 'lib/lux-hammer.rb', line 198 def before_hooks @before_hooks end |
.cli(argv = ARGV) ⇒ Object
Entry point for the ‘hammer` binary. Walks up from CWD until it finds a Hammerfile, evaluates it as the block DSL, then dispatches ARGV against the resulting CLI.
589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 |
# File 'lib/lux-hammer.rb', line 589 def self.cli(argv = ARGV) path = find_hammerfile(Dir.pwd) unless path Shell.print_error "no Hammerfile found in #{Dir.pwd} or any parent directory" # Heuristic: *.rb files referencing `Hammer.` are likely inline CLIs # the user could promote into a Hammerfile. excludes = %w[.git node_modules tmp vendor coverage dist build] .map { |d| "--exclude-dir=#{d}" }.join(' ') candidates = `grep -rl --include='*.rb' #{excludes} 'Hammer\\.' . 2>/dev/null` .lines.map(&:strip).reject(&:empty?) unless candidates.empty? Shell.say "possible CLI implementation(s) - files referencing `Hammer.`:", :yellow candidates.first(10).each { |f| Shell.say " #{f.sub(%r{\A\./}, '')}" } Shell.say '' end Shell.say "create one - example:" puts Shell.say <<~RUBY define :hello do desc 'say hello' proc do |opts| say.green "hello \#{opts[:args].first || 'world'}" end end RUBY exit 1 end klass = Class.new(Hammer) # Resolve before chdir so paths like `bin/foo` stay relative to the # cwd the user actually invoked from. `program_name` memoizes. klass.program_name # chdir into the Hammerfile's directory for the entire run so commands # operate on the project root (Rake-style). Dir.chdir(File.dirname(path)) Builder.new(klass).instance_eval(File.read(path), path) klass.start(argv) end |
.commands ⇒ Object
224 225 226 |
# File 'lib/lux-hammer.rb', line 224 def commands @commands ||= {} end |
.default_program_name ⇒ Object
Program name shown in help/usage: the invocation path relative to cwd if the script lives inside it (e.g. ‘bin/foo` when invoked from the project root), otherwise the basename (e.g. `lux` for a globally installed bin in PATH).
112 113 114 115 116 117 118 119 120 121 |
# File 'lib/lux-hammer.rb', line 112 def default_program_name prog = $PROGRAM_NAME return File.basename(prog) unless prog.include?('/') # Resolve symlinks on both sides so e.g. macOS `/tmp` -> `/private/tmp` # doesn't cause a false miss when comparing prefixes. abs = File.realpath(prog) rescue File.(prog) cwd = File.realpath(Dir.pwd) rescue Dir.pwd return abs[(cwd.length + 1)..] if abs.start_with?("#{cwd}/") File.basename(prog) end |
.define(name, &block) ⇒ Object
Define a command. Block runs in a CommandBuilder context and must return a Proc as its last expression. That proc is the handler and receives a single ‘opts` hash with symbol keys; positional ARGV lives at `opts`.
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/lux-hammer.rb', line 127 def define(name, &block) cmd = Command.new(name: name.to_s) handler = CommandBuilder.new(cmd).instance_eval(&block) unless handler.is_a?(Proc) raise Error, <<~MSG define(:#{name}) block must end with a `proc do |opts| ... end`. The proc's return value is what becomes the command handler. Example: define :#{name} do desc 'what it does' example '#{name} foo --env=prod' opt :env, default: 'dev' proc do |opts| # your code here - opts[:env], opts[:args], ... end end MSG end cmd.handler = handler commands[cmd.name] = cmd # `define` ignores pending class-level state, but clear it so a # later `def` doesn't accidentally consume stale metadata. @pending_desc = nil @pending_examples = [] @pending_options = [] @pending_alts = [] @pending_needs = [] end |
.desc(text) ⇒ Object
—– class-level DSL for ‘def`-style commands ——————— Set pending metadata that the next `def` will consume.
class MyCli < Hammer
desc 'Build'
opt :env, default: 'dev'
def build(opts)
say "building #{opts[:env]}"
end
end
71 |
# File 'lib/lux-hammer.rb', line 71 def desc(text) ; @pending_desc = text.to_s.rstrip end |
.each_command(prefix = nil, &block) ⇒ Object
Yield [full_colon_path, Command] for every command in this class and all nested namespaces.
366 367 368 369 370 371 372 373 374 375 |
# File 'lib/lux-hammer.rb', line 366 def each_command(prefix = nil, &block) commands.each_value do |c| full = prefix ? "#{prefix}:#{c.name}" : c.name yield full, c end namespaces.each do |ns_name, sub| sub_prefix = prefix ? "#{prefix}:#{ns_name}" : ns_name sub.each_command(sub_prefix, &block) end end |
.emit_rows(rows, width) ⇒ Object
490 491 492 493 494 495 |
# File 'lib/lux-hammer.rb', line 490 def emit_rows(rows, width) rows.each do |full, c| label = label_for(full, c) Shell.say " #{program_name} #{label.ljust(width)} # #{c.brief}" end end |
.example(text) ⇒ Object
72 |
# File 'lib/lux-hammer.rb', line 72 def example(text) ; @pending_examples << text end |
.find_command(name) ⇒ Object
Find a command by canonical name or alt within this class.
317 318 319 |
# File 'lib/lux-hammer.rb', line 317 def find_command(name) commands[name.to_s] || commands.values.find { |c| c.matches?(name) } end |
.find_hammerfile(start) ⇒ Object
Walk up the directory tree looking for a Hammerfile.
632 633 634 635 636 637 638 639 640 641 |
# File 'lib/lux-hammer.rb', line 632 def self.find_hammerfile(start) dir = File.(start) loop do candidate = File.join(dir, 'Hammerfile') return candidate if File.file?(candidate) parent = File.dirname(dir) return nil if parent == dir dir = parent end end |
.hammer(name, *args, **opts) ⇒ Object
Programmatic dispatch by name. Useful for scripting and tests.
MyCli.hammer :build -> start(["build"])
MyCli.hammer 'db:users:list' -> start(["db:users:list"])
MyCli.hammer :eval, 'puts 42' -> start(["eval", "puts 42"])
MyCli.hammer :build, env: 'prod' -> start(["build", "--env=prod"])
MyCli.hammer :build, verbose: true -> start(["build", "--verbose"])
MyCli.hammer :build, no_cache: true -> start(["build", "--no-cache"])
MyCli.hammer :build, cache: false -> skipped (no-op)
Symbols are single-segment names; pass a string with colons for namespaced paths. Trailing positionals become positional ARGV. Underscores in option keys become dashes in flags.
354 355 356 357 358 359 360 361 362 |
# File 'lib/lux-hammer.rb', line 354 def hammer(name, *args, **opts) argv = [name.to_s, *args.map(&:to_s)] opts.each do |k, v| next if v == false flag = "--#{k.to_s.tr('_', '-')}" argv << (v == true ? flag : "#{flag}=#{v}") end start(argv) end |
.help_requested?(argv) ⇒ Boolean
426 427 428 429 430 |
# File 'lib/lux-hammer.rb', line 426 def help_requested?(argv) stop = argv.index('--') scan = stop ? argv[0...stop] : argv scan.include?('-h') || scan.include?('--help') end |
.inherited(sub) ⇒ Object
46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/lux-hammer.rb', line 46 def inherited(sub) super sub.instance_variable_set(:@commands, {}) sub.instance_variable_set(:@namespaces, {}) sub.instance_variable_set(:@before_hooks, []) sub.instance_variable_set(:@parent, nil) sub.instance_variable_set(:@program_name, nil) sub.instance_variable_set(:@pending_desc, nil) sub.instance_variable_set(:@pending_examples, []) sub.instance_variable_set(:@pending_options, []) sub.instance_variable_set(:@pending_alts, []) sub.instance_variable_set(:@pending_needs, []) end |
.label_for(full, cmd) ⇒ Object
“db:migrate” or “db:migrate (alt: m)”
510 511 512 |
# File 'lib/lux-hammer.rb', line 510 def label_for(full, cmd) cmd.alts.empty? ? full : "#{full} (alt: #{cmd.alts.join(', ')})" end |
.load(*paths, **kwargs) ⇒ Object
Load Hammerfile fragments and register their commands on this class. Rake-style: split a CLI across multiple files.
load # auto-discover *_hammer.rb under caller dir
load auto: true # same
load 'tasks/db_hammer.rb' # one file
load 'tasks/*_hammer.rb' # glob
Paths resolve relative to the file calling ‘load`. See `Hammer::Loader` for the full implementation.
248 249 250 251 252 253 254 255 |
# File 'lib/lux-hammer.rb', line 248 def load(*paths, **kwargs) if self == Hammer raise Error, 'use `load` from inside a Hammerfile / Hammer.run block / Hammer subclass body, ' \ 'or call SubClass.load - Hammer.load itself has no target' end anchor = Loader.caller_anchor(caller_locations(1, 1).first) loader.load(anchor, paths, kwargs) end |
.loader ⇒ Object
Per-target Loader instance. Owns the dedup cache, so re-entrant ‘load` from inside a fragment is safe and idempotent.
234 235 236 |
# File 'lib/lux-hammer.rb', line 234 def loader @loader ||= Loader.new(self) end |
.method_added(method_name) ⇒ Object
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/lux-hammer.rb', line 77 def method_added(method_name) super return unless @pending_desc cmd = Command.new(name: method_name.to_s, desc: @pending_desc) @pending_examples.each { |e| cmd.add_example(e) } @pending_options.each { |o| cmd.add_option(o) } @pending_alts.each { |n| cmd.add_alt(n) } @pending_needs.each { |n| cmd.add_need(n) } # If the method takes no args, call it without opts. Otherwise pass # opts. So both `def build` and `def build(opts)` work. m = method_name arity = instance_method(method_name).arity cmd.handler = arity.zero? ? proc { send(m) } : proc { |opts| send(m, opts) } commands[cmd.name] = cmd @pending_desc = nil @pending_examples = [] @pending_options = [] @pending_alts = [] @pending_needs = [] end |
.namespace(name, &block) ⇒ Object
Open a namespace (group of commands). Everything inside the block (define, nested namespace, …) belongs to that namespace, evaluated against an anonymous Hammer subclass.
namespace :db do
define :migrate do ... end
namespace :users do ... end
end
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
# File 'lib/lux-hammer.rb', line 168 def namespace(name, &block) sub = Class.new(Hammer) # Track the top-level CLI class so cross-invocation # (`hammer 'ns:cmd'`) from inside a namespaced command dispatches # against the full tree, not just the current namespace. sub.instance_variable_set(:@root, root) # Parent link, so `before` hooks defined further up the namespace # tree can be collected and run outer -> inner before a command. sub.instance_variable_set(:@parent, self) # Share the parent's resolved program_name so help banners show # "myapp ns:cmd" with the same prefix everywhere - and so the value # captured pre-chdir (see `Hammer.cli`) survives into nested classes. sub.instance_variable_set(:@program_name, program_name) sub.class_eval(&block) if block @namespaces[name.to_s] = sub end |
.namespaces ⇒ Object
228 229 230 |
# File 'lib/lux-hammer.rb', line 228 def namespaces @namespaces ||= {} end |
.needs(*names) ⇒ Object
75 |
# File 'lib/lux-hammer.rb', line 75 def needs(*names) ; @pending_needs.concat(names) end |
.opt(name, **o) ⇒ Object
73 |
# File 'lib/lux-hammer.rb', line 73 def opt(name, **o) ; @pending_options << Option.new(name, **o) end |
.parent ⇒ Object
202 203 204 |
# File 'lib/lux-hammer.rb', line 202 def parent @parent end |
.print_command_help(cmd, full = nil) ⇒ Object
528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 |
# File 'lib/lux-hammer.rb', line 528 def print_command_help(cmd, full = nil) full ||= cmd.name Shell.say "Usage: #{program_name} #{full}#{usage_signature(cmd)}", :cyan cmd.desc.each_line do |line| stripped = line.chomp Shell.say(stripped.empty? ? '' : " #{stripped}") end unless cmd.desc.empty? Shell.say " alias: #{cmd.alts.join(', ')}" unless cmd.alts.empty? unless cmd..empty? Shell.say '' Shell.say 'Options:', :yellow cmd..each { |o| Shell.say " #{o.usage}" } end unless cmd.examples.empty? Shell.say '' Shell.say 'Examples:', :yellow cmd.examples.each { |e| Shell.say " #{program_name} #{e}" } end end |
.print_command_list(klass, prefix = nil) ⇒ Object
462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 |
# File 'lib/lux-hammer.rb', line 462 def print_command_list(klass, prefix = nil) rows = [] # Commands without a `desc` are hidden from listings but still # dispatchable + `hammer`-callable - useful for private helpers # invoked from `before` hooks or other commands (e.g. `:env`, `:app`). klass.each_command(prefix) { |full, c| rows << [full, c] unless c.desc.empty? } return if rows.empty? # group by "section" = everything between the view prefix and the # leaf name. Bare leaves go in :root. groups = rows.group_by { |full, _| section_for(full, prefix) } width = rows.map { |full, c| label_for(full, c).length }.max first = true if (rooted = groups.delete(:root)) Shell.say 'Commands:', :yellow emit_rows(rooted.sort_by { |full, _| full }, width) first = false end groups.each do |section, items| Shell.say unless first first = false Shell.say "#{section}:", :yellow emit_rows(items.sort_by { |full, _| full }, width) end end |
.print_footer ⇒ Object
457 458 459 460 |
# File 'lib/lux-hammer.rb', line 457 def Shell.say '' Shell.say "powered by hammer - #{HOMEPAGE}", :gray end |
.print_help(target = nil) ⇒ Object
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 |
# File 'lib/lux-hammer.rb', line 432 def print_help(target = nil) if target cmd, _ = resolve(target) return print_command_help(cmd, target) if cmd ns = resolve_namespace(target) return print_namespace_help(target, ns) if ns Shell.print_error("unknown: #{target}") return end Shell.say "Usage: #{program_name} COMMAND [ARGS]", :cyan Shell.say '' print_command_list(self) end |
.print_namespace_help(prefix, ns) ⇒ Object
448 449 450 451 452 453 |
# File 'lib/lux-hammer.rb', line 448 def print_namespace_help(prefix, ns) Shell.say "Usage: #{program_name} #{prefix}:COMMAND [ARGS]", :cyan Shell.say '' print_command_list(ns, prefix) end |
.program_name ⇒ Object
Resolved lazily on first read and memoized, so callers that need the cwd-relative form (see ‘default_program_name`) can warm the cache before chdir-ing elsewhere.
104 105 106 |
# File 'lib/lux-hammer.rb', line 104 def program_name @program_name ||= default_program_name end |
.resolve(path) ⇒ Object
Walk “ns1:ns2:cmd” -> [command, owning_class]. Returns [nil, nil] if any segment is missing or the final segment isn’t a command.
323 324 325 326 327 328 329 330 331 |
# File 'lib/lux-hammer.rb', line 323 def resolve(path) parts = path.to_s.split(':') klass = self parts[0..-2].each do |ns| klass = klass.namespaces[ns] or return [nil, nil] end cmd = klass.find_command(parts.last) cmd ? [cmd, klass] : [nil, nil] end |
.resolve_namespace(path) ⇒ Object
Walk “ns1:ns2” -> namespace class, or nil if any segment missing.
334 335 336 337 338 339 |
# File 'lib/lux-hammer.rb', line 334 def resolve_namespace(path) parts = path.to_s.split(':') klass = self parts.each { |ns| klass = klass.namespaces[ns] or return nil } klass end |
.root ⇒ Object
Topmost class in this CLI tree. For user-defined ‘class MyCli < Hammer` or `Class.new(Hammer)` it’s self; for namespace subclasses it’s whichever class opened the namespace.
220 221 222 |
# File 'lib/lux-hammer.rb', line 220 def root @root || self end |
.run(argv = ARGV, &block) ⇒ Object
Define and run a CLI inline. Inside the block use ‘define :name do … end`, `namespace`, and `load`.
Without a block: load ./Hammerfile if it exists, otherwise auto-discover *_hammer.rb under Dir.pwd, then dispatch ARGV.
571 572 573 574 575 576 577 578 579 580 581 582 583 584 |
# File 'lib/lux-hammer.rb', line 571 def self.run(argv = ARGV, &block) klass = Class.new(Hammer) if block Builder.new(klass).instance_eval(&block) else hf = File.join(Dir.pwd, 'Hammerfile') if File.file?(hf) Builder.new(klass).instance_eval(File.read(hf), hf) else klass.loader.load(Dir.pwd, [], auto: true) end end klass.start(argv) end |
.run_before_hooks(instance, opts) ⇒ Object
Fire ‘before` hooks from root down through the namespace chain. Each class’s hooks fire at most once per top-level ‘start`, so prereqs dispatched via `needs` won’t re-trigger them.
402 403 404 405 406 407 408 409 |
# File 'lib/lux-hammer.rb', line 402 def run_before_hooks(instance, opts) ran = Thread.current[:hammer_before_ran] ||= {} ancestor_chain.each do |klass| next if ran[klass.object_id] ran[klass.object_id] = true klass.before_hooks.each { |hook| instance.instance_exec(opts, &hook) } end end |
.run_command(cmd, argv, full: nil) ⇒ Object
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 |
# File 'lib/lux-hammer.rb', line 377 def run_command(cmd, argv, full: nil) # -h / --help is reserved on every command. Anywhere before a `--` # stop-marker, it short-circuits to per-command help. return print_command_help(cmd, full) if help_requested?(argv) positional, opts = Parser.new(cmd.).parse(argv) opts[:args] = positional instance = new run_before_hooks(instance, opts) run_needs(cmd) instance.instance_exec(opts, &cmd.handler) rescue Parser::Error => e Shell.print_error(e.) print_command_help(cmd, full) exit 1 rescue Hammer::Error => e # Raised by `error 'msg'` inside a handler - controlled exit, no # backtrace, no per-command help spam. Shell.print_error(e.) exit 1 end |
.run_needs(cmd) ⇒ Object
Dispatch a command’s declared ‘needs` through the root class, with per-invocation dedupe. Prereqs run with default options (no argv).
413 414 415 416 417 418 419 420 421 422 423 424 |
# File 'lib/lux-hammer.rb', line 413 def run_needs(cmd) return if cmd.needs.empty? ran = Thread.current[:hammer_needs_ran] ||= {} cmd.needs.each do |path| key = path.to_s next if ran[key] ran[key] = true target, = root.resolve(key) raise Error, "needs: unknown command '#{key}' in #{cmd.name}" unless target root.start([key]) end end |
.section_for(full, prefix) ⇒ Object
‘db’ for ‘db:migrate’ or ‘db:users:list’ viewed from root; ‘users’ for ‘db:users:list’ viewed from ‘db’; :root if the command sits at the view’s top level. Only the first segment under the view groups, so deeper paths fold into their top-level section.
501 502 503 504 505 506 507 |
# File 'lib/lux-hammer.rb', line 501 def section_for(full, prefix) segs = full.split(':')[0..-2] if prefix && !prefix.empty? segs = segs[prefix.split(':').size..] || [] end segs.empty? ? :root : segs.first end |
.start(argv = ARGV) ⇒ Object
Entry point. Parses ARGV, finds the right command, runs it. Command names are Rake-style colon paths: “build”, “db:migrate”, “db:users:list”.
Rake-style chained dispatch: ‘hammer build + deploy + notify`. A bare `+` argv token separates commands; `++` escapes to a literal `+` positional. Quoted shell args (`–foo=“a + b”`) arrive as a single token and are not split.
265 266 267 268 269 270 271 272 273 274 275 276 277 278 |
# File 'lib/lux-hammer.rb', line 265 def start(argv = ARGV) # Track prereqs fired during this top-level invocation so a `needs` # chain runs each prereq at most once. Nested `start` calls (e.g. # `needs` -> `hammer` -> `start`, or a `+` chain) share the set; # the outermost call owns its lifetime. outer = Thread.current[:hammer_needs_ran].nil? Thread.current[:hammer_needs_ran] ||= {} Thread.current[:hammer_before_ran] ||= {} split_chain(argv).each { |seg| dispatch(seg) } ensure Thread.current[:hammer_needs_ran] = nil if outer Thread.current[:hammer_before_ran] = nil if outer end |
.usage_signature(cmd) ⇒ Object
“ URL [ENV] [OPTIONS]” - shows the positional-fill names for declared non-boolean opts (required bare, optional bracketed), plus a generic [OPTIONS] tail if any flags exist.
517 518 519 520 521 522 523 524 525 526 |
# File 'lib/lux-hammer.rb', line 517 def usage_signature(cmd) pos = cmd..reject(&:boolean?).map { |o| name = o.name.to_s.upcase o.required ? name : "[#{name}]" } out = pos.join(' ') out = "#{out} ".lstrip unless out.empty? out += '[OPTIONS]' unless cmd..empty? out.empty? ? '' : " #{out}" end |
Instance Method Details
#hammer(name, *args, **opts) ⇒ Object
Inside a command’s ‘proc do |opts| … end`, call sibling commands:
define :deploy do
proc do |opts|
hammer :build
hammer 'db:migrate', pretend: true
end
end
Dispatches from the root class so colon paths resolve against the full tree even when called from inside a namespaced command.
560 561 562 |
# File 'lib/lux-hammer.rb', line 560 def hammer(name, *args, **opts) self.class.root.hammer(name, *args, **opts) end |