ergane 0.1.0 → 0.2.0
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/CHANGELOG.md +20 -0
- data/README.md +12 -0
- data/lib/ergane/argument_definition.rb +3 -1
- data/lib/ergane/command.rb +90 -16
- data/lib/ergane/concerns/inheritance.rb +3 -5
- data/lib/ergane/core_ext/object.rb +6 -2
- data/lib/ergane/core_ext/option_parser.rb +9 -7
- data/lib/ergane/dsl/command_dsl.rb +4 -4
- data/lib/ergane/dsl/macros.rb +23 -0
- data/lib/ergane/help_formatter.rb +23 -20
- data/lib/ergane/path_registry.rb +9 -6
- data/lib/ergane/runner.rb +4 -0
- data/lib/ergane/tool.rb +7 -22
- data/lib/ergane/version.rb +1 -1
- metadata +2 -3
- data/lib/ergane/util/debug.rb +0 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5bac11b2362d538571dc708483d4cbc83ee2a66e51983c5e9c312e5a1a98d3f3
|
|
4
|
+
data.tar.gz: e27be5089a4034d7a67d26b17f4b5572d723a6ed38f3c29275d2089d6ead65cd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e136d486d292c5bac857f0cd8a950189d562aef43f76320b65c672bcc11f98af37658cac4b201a607c8ba103f73d84a94b5bfeafefa4044ccbd98a62707240ad
|
|
7
|
+
data.tar.gz: c36af86e50cb12b0cdd270744cea2f4c3a3f88570c8bd305bcd976f8466ffa8e6d34a827df3619b317b2c86fcc30d73fbcecc4b10d7a9b4eed250d028baf9913
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.2.0] - 2026-05-25
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **Unknown subcommands on a command group now raise `CommandNotFound`** (with a did-you-mean suggestion) and exit non-zero, instead of silently printing help. Scoped to command groups — leaf commands still treat unmatched tokens as positional arguments.
|
|
9
|
+
- **Positional `argument` declarations are now enforced.** Required-ness is derived from the command's `run` signature — a required parameter (`run(name)`) makes the argument required, while an optional one (`run(name = nil)`) or a splat (`run(*)`) makes it optional; an explicit `required:` on the `argument` overrides the signature. A missing required argument raises `MissingArgument`; an absent optional argument takes its `default:`; values are coerced to the declared `type:` (`String` is identity, `Integer`/`Float` via Kernel conversion raising `InvalidOption` on bad input). Previously these keywords affected only help text.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `Ergane::DSL::Macros.dsl_value` — a class-level getter/setter accessor generator used by the DSL.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- `PathRegistry#abbreviate` now expands its input before matching (mirroring `#register`), so abbreviation is consistent across platforms (notably Windows, where un-expanded inputs failed to match drive-qualified prefixes) and accepts `~`-relative input.
|
|
16
|
+
- `OptionParser#order_recognized!` no longer drops the trailing tokens of a multi-token unknown option, and preserves argument order.
|
|
17
|
+
- `String#blank?` is guarded against external definitions (e.g. ActiveSupport) instead of unconditionally overriding them.
|
|
18
|
+
- Tool-rooted abstract intermediate commands are no longer stranded in the tool's registry when marked abstract.
|
|
19
|
+
|
|
20
|
+
### Internal
|
|
21
|
+
- Command registration unified into a single `register!` path (removed the duplicate `define_singleton_method`'d `inherited`/`inherited_command_name_set` hooks).
|
|
22
|
+
- `HelpFormatter` renders through a shared `section` helper with a per-render color cycler (no module-level mutable state).
|
|
23
|
+
- `Util::Debug` is no longer packaged in the gem (dev-only tooling).
|
|
24
|
+
|
|
5
25
|
## [0.1.0] - 2026-05-25
|
|
6
26
|
|
|
7
27
|
First release of the rewritten framework. A near-complete rewrite of the
|
data/README.md
CHANGED
|
@@ -174,6 +174,18 @@ run do |*args|
|
|
|
174
174
|
end
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
+
### Positional Arguments
|
|
178
|
+
|
|
179
|
+
An argument's required-ness is derived from the command's `run` signature — a required parameter makes it required, while an optional parameter or a splat makes it optional:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
def run(source, destination = nil, *rest)
|
|
183
|
+
# source is required; destination is optional
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Pass `required:` on the `argument` to override the signature, `type:` to coerce the value (a non-`String` type is converted, raising on failure), and `default:` for an absent optional argument. A missing required argument raises `Ergane::MissingArgument`.
|
|
188
|
+
|
|
177
189
|
## Loading Commands from Files
|
|
178
190
|
|
|
179
191
|
For larger CLIs, organize commands in separate files:
|
|
@@ -4,7 +4,9 @@ module Ergane
|
|
|
4
4
|
class ArgumentDefinition
|
|
5
5
|
attr_reader :name, :type, :description, :required, :default
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
# +required+ defaults to nil, meaning "derive from the run signature";
|
|
8
|
+
# pass true/false to force it.
|
|
9
|
+
def initialize(name, type = String, description: nil, required: nil, default: nil)
|
|
8
10
|
@name = name.to_sym
|
|
9
11
|
@type = type
|
|
10
12
|
@description = description
|
data/lib/ergane/command.rb
CHANGED
|
@@ -11,14 +11,7 @@ module Ergane
|
|
|
11
11
|
class << self
|
|
12
12
|
def command_name=(name)
|
|
13
13
|
@command_name = name&.to_sym
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
parent = superclass
|
|
17
|
-
if parent.respond_to?(:tool) && parent.abstract_class?
|
|
18
|
-
parent.inherited_command_name_set(self)
|
|
19
|
-
else
|
|
20
|
-
register_subcommand(self)
|
|
21
|
-
end
|
|
14
|
+
register!
|
|
22
15
|
end
|
|
23
16
|
|
|
24
17
|
def command_name
|
|
@@ -33,16 +26,36 @@ module Ergane
|
|
|
33
26
|
@subcommands ||= {}
|
|
34
27
|
end
|
|
35
28
|
|
|
29
|
+
# Effective required-ness of the positional argument at +index+. An
|
|
30
|
+
# explicit DSL `required:` (true/false) wins; otherwise it's derived from
|
|
31
|
+
# the run method's matching positional parameter: a required parameter
|
|
32
|
+
# (`run(name)`) means required, while an optional one (`run(name = nil)`)
|
|
33
|
+
# or a splat (`run(*)`) means optional.
|
|
34
|
+
def argument_required?(index)
|
|
35
|
+
defn = argument_definitions[index]
|
|
36
|
+
return false unless defn
|
|
37
|
+
return defn.required unless defn.required.nil?
|
|
38
|
+
|
|
39
|
+
param = run_positional_parameters[index]
|
|
40
|
+
param ? param.first == :req : false
|
|
41
|
+
end
|
|
42
|
+
|
|
36
43
|
def inherited(subclass)
|
|
37
44
|
super
|
|
38
45
|
subclass.instance_variable_set(:@option_definitions, option_definitions.dup)
|
|
39
46
|
subclass.instance_variable_set(:@argument_definitions, argument_definitions.dup)
|
|
40
47
|
subclass.instance_variable_set(:@subcommands, {})
|
|
41
|
-
|
|
48
|
+
subclass.send(:register!)
|
|
42
49
|
end
|
|
43
50
|
|
|
44
51
|
private
|
|
45
52
|
|
|
53
|
+
# The run method's positional parameters (required/optional), in order,
|
|
54
|
+
# which line up with declared arguments. Excludes splat/keyword params.
|
|
55
|
+
def run_positional_parameters
|
|
56
|
+
instance_method(:run).parameters.select { |kind, _| kind == :req || kind == :opt }
|
|
57
|
+
end
|
|
58
|
+
|
|
46
59
|
def derive_command_name
|
|
47
60
|
return nil if self == Command || abstract_class?
|
|
48
61
|
base = name&.demodulize
|
|
@@ -50,12 +63,36 @@ module Ergane
|
|
|
50
63
|
base.sub(/Command$/, "").underscore.to_sym
|
|
51
64
|
end
|
|
52
65
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
parent
|
|
66
|
+
# The registry this command belongs in: a tool's abstract command base
|
|
67
|
+
# registers under the tool itself; a concrete parent registers under
|
|
68
|
+
# that parent. A command rooted directly on Command, or under an
|
|
69
|
+
# abstract non-tool parent, registers nowhere.
|
|
70
|
+
def registration_target
|
|
71
|
+
parent = superclass
|
|
72
|
+
if parent.respond_to?(:tool) && parent.abstract_class?
|
|
73
|
+
parent.tool
|
|
74
|
+
elsif parent != Command && !parent.abstract_class?
|
|
75
|
+
parent
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Registers (or re-registers) this command in its target registry under
|
|
80
|
+
# its current command_name, removing any prior registration when the
|
|
81
|
+
# name changes or the command becomes abstract. Idempotent — safe to
|
|
82
|
+
# call from both .inherited and command_name=.
|
|
83
|
+
def register!
|
|
84
|
+
target = registration_target
|
|
85
|
+
return unless target
|
|
86
|
+
|
|
87
|
+
name = abstract_class? ? nil : command_name
|
|
88
|
+
|
|
89
|
+
target.subcommands.delete(@registered_as) if @registered_as && @registered_as != name
|
|
90
|
+
if name
|
|
91
|
+
target.subcommands[name] = self
|
|
92
|
+
@registered_as = name
|
|
93
|
+
else
|
|
94
|
+
@registered_as = nil
|
|
95
|
+
end
|
|
59
96
|
end
|
|
60
97
|
end
|
|
61
98
|
|
|
@@ -67,7 +104,7 @@ module Ergane
|
|
|
67
104
|
|
|
68
105
|
def initialize(argv = [])
|
|
69
106
|
@options = self.class.build_default_options
|
|
70
|
-
@argv = parse_options(argv.dup)
|
|
107
|
+
@argv = process_arguments(parse_options(argv.dup))
|
|
71
108
|
end
|
|
72
109
|
|
|
73
110
|
def args
|
|
@@ -81,5 +118,42 @@ module Ergane
|
|
|
81
118
|
raise AbstractCommand, "#{self.class.name}#run is not implemented"
|
|
82
119
|
end
|
|
83
120
|
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
# Validates and coerces positional args against the command's argument
|
|
125
|
+
# definitions: missing required args raise, absent optional args take
|
|
126
|
+
# their default, and present args are coerced by type. Extra positionals
|
|
127
|
+
# beyond the declared arguments pass through untouched (e.g. for run(*)).
|
|
128
|
+
def process_arguments(argv)
|
|
129
|
+
definitions = self.class.argument_definitions
|
|
130
|
+
return argv if definitions.empty?
|
|
131
|
+
|
|
132
|
+
declared = definitions.each_with_index.map do |defn, i|
|
|
133
|
+
if i < argv.length
|
|
134
|
+
coerce_argument(argv[i], defn)
|
|
135
|
+
elsif self.class.argument_required?(i)
|
|
136
|
+
raise MissingArgument, "Missing required argument: <#{defn.name}>"
|
|
137
|
+
else
|
|
138
|
+
defn.default
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
declared + argv.drop(definitions.length)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def coerce_argument(value, defn)
|
|
145
|
+
type = defn.type
|
|
146
|
+
return value if type.nil? || type == String
|
|
147
|
+
|
|
148
|
+
if type == Integer
|
|
149
|
+
Integer(value)
|
|
150
|
+
elsif type == Float
|
|
151
|
+
Float(value)
|
|
152
|
+
else
|
|
153
|
+
value
|
|
154
|
+
end
|
|
155
|
+
rescue ArgumentError
|
|
156
|
+
raise InvalidOption, "Invalid value for <#{defn.name}>: #{value.inspect} (expected #{type})"
|
|
157
|
+
end
|
|
84
158
|
end
|
|
85
159
|
end
|
|
@@ -10,11 +10,9 @@ module Ergane
|
|
|
10
10
|
module ClassMethods
|
|
11
11
|
def abstract_class=(value)
|
|
12
12
|
@abstract_class = value
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
parent.subcommands.delete(command_name) if parent.respond_to?(:subcommands)
|
|
17
|
-
end
|
|
13
|
+
# Re-run registration so the command leaves (or rejoins) its
|
|
14
|
+
# registry to match its new abstract state.
|
|
15
|
+
register! if self < Ergane::Command && respond_to?(:register!, true)
|
|
18
16
|
end
|
|
19
17
|
|
|
20
18
|
def abstract_class?
|
|
@@ -32,10 +32,14 @@ class TrueClass
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
class String
|
|
35
|
-
#
|
|
35
|
+
# Catch whitespace-only strings, overriding the inherited Object#blank?.
|
|
36
|
+
# Guarded on String's OWN methods — not method_defined?, which would see the
|
|
37
|
+
# inherited Object#blank? and skip, leaving the whitespace-blind version. This
|
|
38
|
+
# still defines our version normally, but yields to any external String#blank?
|
|
39
|
+
# (e.g. ActiveSupport) rather than clobbering it.
|
|
36
40
|
def blank?
|
|
37
41
|
empty? || /\A[[:space:]]*\z/.match?(self)
|
|
38
|
-
end
|
|
42
|
+
end unless instance_methods(false).include?(:blank?)
|
|
39
43
|
end
|
|
40
44
|
|
|
41
45
|
class Numeric
|
|
@@ -4,13 +4,15 @@ class OptionParser
|
|
|
4
4
|
# Like order!, but leave any unrecognized --switches alone
|
|
5
5
|
# instead of raising InvalidOption.
|
|
6
6
|
def order_recognized!(args)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
leftover = []
|
|
8
|
+
until args.empty?
|
|
9
|
+
begin
|
|
10
|
+
order!(args) { |nonopt| leftover << nonopt }
|
|
11
|
+
break
|
|
12
|
+
rescue OptionParser::InvalidOption => e
|
|
13
|
+
leftover.concat(e.args)
|
|
14
|
+
end
|
|
13
15
|
end
|
|
14
|
-
args
|
|
16
|
+
args.replace(leftover)
|
|
15
17
|
end
|
|
16
18
|
end
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
module Ergane
|
|
4
4
|
module DSL
|
|
5
5
|
module CommandDSL
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
extend Macros
|
|
7
|
+
|
|
8
|
+
dsl_value :description, default: ""
|
|
9
9
|
|
|
10
10
|
def aliases(*names)
|
|
11
11
|
names.any? ? (@aliases = names.flatten.map(&:to_sym)) : (@aliases || [])
|
|
@@ -22,7 +22,7 @@ module Ergane
|
|
|
22
22
|
option(name, nil, short: short, description: description, default: false)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def argument(name, type = String, description: nil, required:
|
|
25
|
+
def argument(name, type = String, description: nil, required: nil, default: nil)
|
|
26
26
|
argument_definitions << ArgumentDefinition.new(
|
|
27
27
|
name, type, description: description, required: required, default: default
|
|
28
28
|
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ergane
|
|
4
|
+
module DSL
|
|
5
|
+
# Helpers for defining DSL methods on a host class/module.
|
|
6
|
+
module Macros
|
|
7
|
+
# Defines a class-level "value" accessor with combined getter/setter
|
|
8
|
+
# semantics: called with a truthy argument it stores and returns it;
|
|
9
|
+
# called with none (or a falsy value) it returns the stored value, or
|
|
10
|
+
# +default+ if unset.
|
|
11
|
+
#
|
|
12
|
+
# dsl_value :description, default: ""
|
|
13
|
+
# description "Deploy" # => "Deploy" (and stored)
|
|
14
|
+
# description # => "Deploy"
|
|
15
|
+
def dsl_value(name, default: nil)
|
|
16
|
+
ivar = "@#{name}"
|
|
17
|
+
define_method(name) do |value = nil|
|
|
18
|
+
value ? instance_variable_set(ivar, value) : (instance_variable_get(ivar) || default)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -37,8 +37,8 @@ module Ergane
|
|
|
37
37
|
usage = path.light_red
|
|
38
38
|
usage += " [options]".light_cyan if command_class.option_definitions.any?
|
|
39
39
|
usage += " [subcommand]".light_black.underline if command_class.subcommands.any?
|
|
40
|
-
command_class.argument_definitions.
|
|
41
|
-
label =
|
|
40
|
+
command_class.argument_definitions.each_with_index do |arg, i|
|
|
41
|
+
label = command_class.argument_required?(i) ? "<#{arg.name}>" : "[#{arg.name}]"
|
|
42
42
|
usage += " #{label}".light_yellow
|
|
43
43
|
end
|
|
44
44
|
"Usage:".light_cyan + " " + usage
|
|
@@ -49,21 +49,18 @@ module Ergane
|
|
|
49
49
|
return if subs.empty?
|
|
50
50
|
|
|
51
51
|
max_width = subs.keys.map { |k| k.to_s.length }.max
|
|
52
|
-
Util::Formatting.
|
|
53
|
-
|
|
54
|
-
lines = []
|
|
55
|
-
lines << "Subcommands:".light_cyan
|
|
56
|
-
header_len = lines.last.uncolorize.length
|
|
57
|
-
lines << (" \u250C" + ("\u2500" * (header_len - 2)) + "\u2518").light_black
|
|
52
|
+
colors = Util::Formatting::COLORS.cycle
|
|
53
|
+
title = "Subcommands"
|
|
58
54
|
|
|
55
|
+
lines = [(" \u250C" + ("\u2500" * (title.length - 1)) + "\u2518").light_black]
|
|
59
56
|
subs.each do |name, sub_class|
|
|
60
57
|
label = name.to_s.ljust(max_width + 2)
|
|
61
58
|
desc = sub_class.description.present? ? sub_class.description.light_black : ""
|
|
62
|
-
lines << " \u251C\u2500\u2510".light_black + " #{label.send(
|
|
59
|
+
lines << " \u251C\u2500\u2510".light_black + " #{label.send(colors.next)} #{desc}"
|
|
63
60
|
end
|
|
64
|
-
|
|
65
61
|
lines << (" \u2514" + "\u2500" * 40).light_black
|
|
66
|
-
|
|
62
|
+
|
|
63
|
+
section(title, lines)
|
|
67
64
|
end
|
|
68
65
|
|
|
69
66
|
def options_section
|
|
@@ -72,14 +69,13 @@ module Ergane
|
|
|
72
69
|
|
|
73
70
|
max_width = opts.values.map { |o| o.signature.length }.max
|
|
74
71
|
|
|
75
|
-
lines =
|
|
76
|
-
opts.each_value do |opt|
|
|
72
|
+
lines = opts.each_value.map do |opt|
|
|
77
73
|
sig = opt.signature.ljust(max_width + 2)
|
|
78
74
|
desc = opt.description || ""
|
|
79
75
|
default_note = opt.default_value ? " (default: #{opt.default_value})".light_black : ""
|
|
80
|
-
|
|
76
|
+
" #{sig.light_green} #{desc}#{default_note}"
|
|
81
77
|
end
|
|
82
|
-
|
|
78
|
+
section("Options", lines)
|
|
83
79
|
end
|
|
84
80
|
|
|
85
81
|
def arguments_section
|
|
@@ -88,14 +84,21 @@ module Ergane
|
|
|
88
84
|
|
|
89
85
|
max_width = args.map { |a| a.name.to_s.length }.max
|
|
90
86
|
|
|
91
|
-
lines =
|
|
92
|
-
args.each do |arg|
|
|
87
|
+
lines = args.each_with_index.map do |arg, i|
|
|
93
88
|
label = arg.name.to_s.ljust(max_width + 2)
|
|
94
89
|
desc = arg.description || ""
|
|
95
|
-
req =
|
|
96
|
-
|
|
90
|
+
req = command_class.argument_required?(i) ? " (required)".light_red : " (optional)".light_black
|
|
91
|
+
" #{label.light_yellow} #{desc}#{req}"
|
|
97
92
|
end
|
|
98
|
-
|
|
93
|
+
section("Arguments", lines)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Renders a titled block: a cyan "Title:" header followed by its lines,
|
|
97
|
+
# or nil when there are no lines (so #format compacts it away).
|
|
98
|
+
def section(title, lines)
|
|
99
|
+
return if lines.empty?
|
|
100
|
+
|
|
101
|
+
["#{title}:".light_cyan, *lines].join("\n")
|
|
99
102
|
end
|
|
100
103
|
end
|
|
101
104
|
end
|
data/lib/ergane/path_registry.rb
CHANGED
|
@@ -32,16 +32,19 @@ module Ergane
|
|
|
32
32
|
self
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
# Collapse the longest matching prefix in +path+ to its label,
|
|
36
|
-
#
|
|
35
|
+
# Collapse the longest matching prefix in +path+ to its label, returning
|
|
36
|
+
# the path unchanged when nothing matches. The input is expanded before
|
|
37
|
+
# matching (mirroring how prefixes are stored on #register), so matching
|
|
38
|
+
# is consistent across platforms and "~"-relative input is accepted.
|
|
37
39
|
def abbreviate(path)
|
|
38
|
-
|
|
40
|
+
original = path.to_s
|
|
41
|
+
expanded = File.expand_path(original)
|
|
39
42
|
best = @substitutions
|
|
40
|
-
.select { |sub|
|
|
43
|
+
.select { |sub| expanded == sub.prefix || expanded.start_with?("#{sub.prefix}/") }
|
|
41
44
|
.max_by { |sub| sub.prefix.length }
|
|
42
|
-
return
|
|
45
|
+
return original unless best
|
|
43
46
|
|
|
44
|
-
"#{best.label}#{
|
|
47
|
+
"#{best.label}#{expanded[best.prefix.length..]}"
|
|
45
48
|
end
|
|
46
49
|
end
|
|
47
50
|
end
|
data/lib/ergane/runner.rb
CHANGED
|
@@ -47,6 +47,10 @@ module Ergane
|
|
|
47
47
|
if sub
|
|
48
48
|
args.shift
|
|
49
49
|
resolve(sub, args, path)
|
|
50
|
+
elsif command_class.subcommands.any?
|
|
51
|
+
# The current command is a group, so an unmatched token is a bad
|
|
52
|
+
# subcommand — not a positional arg for a leaf command.
|
|
53
|
+
raise CommandNotFound.new(token, available: command_class.subcommands.keys)
|
|
50
54
|
else
|
|
51
55
|
[command_class, args, path]
|
|
52
56
|
end
|
data/lib/ergane/tool.rb
CHANGED
|
@@ -5,6 +5,10 @@ module Ergane
|
|
|
5
5
|
self.abstract_class = true
|
|
6
6
|
|
|
7
7
|
class << self
|
|
8
|
+
extend DSL::Macros
|
|
9
|
+
|
|
10
|
+
dsl_value :version
|
|
11
|
+
|
|
8
12
|
def command_class(klass = nil)
|
|
9
13
|
if klass
|
|
10
14
|
@command_class = klass
|
|
@@ -18,10 +22,6 @@ module Ergane
|
|
|
18
22
|
name ? (self.command_name = name) : command_name
|
|
19
23
|
end
|
|
20
24
|
|
|
21
|
-
def version(ver = nil)
|
|
22
|
-
ver ? (@version = ver) : @version
|
|
23
|
-
end
|
|
24
|
-
|
|
25
25
|
def start(argv = ARGV)
|
|
26
26
|
Runner.new(self, argv.dup).execute
|
|
27
27
|
rescue Interrupt
|
|
@@ -58,27 +58,12 @@ module Ergane
|
|
|
58
58
|
tool_subclass.command_class(base)
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
# Gives the tool's command base (and everything below it) a reference
|
|
62
|
+
# back to the tool, so Command#registration_target can route
|
|
63
|
+
# subcommands to the tool's registry.
|
|
61
64
|
def wire_command_class(klass)
|
|
62
65
|
tool = self
|
|
63
66
|
klass.define_singleton_method(:tool) { tool }
|
|
64
|
-
|
|
65
|
-
klass.define_singleton_method(:inherited) do |subclass|
|
|
66
|
-
super(subclass)
|
|
67
|
-
cmd_name = subclass.command_name
|
|
68
|
-
if cmd_name && !subclass.abstract_class? && subclass.superclass.abstract_class?
|
|
69
|
-
subclass.instance_variable_set(:@_derived_name, cmd_name)
|
|
70
|
-
tool.subcommands[cmd_name] = subclass
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
klass.define_singleton_method(:inherited_command_name_set) do |subclass|
|
|
75
|
-
cmd_name = subclass.command_name
|
|
76
|
-
if cmd_name && !subclass.abstract_class? && subclass.superclass.abstract_class?
|
|
77
|
-
derived = subclass.instance_variable_get(:@_derived_name)
|
|
78
|
-
tool.subcommands.delete(derived) if derived && derived != cmd_name
|
|
79
|
-
tool.subcommands[cmd_name] = subclass
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
67
|
end
|
|
83
68
|
end
|
|
84
69
|
end
|
data/lib/ergane/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ergane
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dale Stevens
|
|
@@ -160,6 +160,7 @@ files:
|
|
|
160
160
|
- lib/ergane/core_ext/string.rb
|
|
161
161
|
- lib/ergane/dsl/block_dsl.rb
|
|
162
162
|
- lib/ergane/dsl/command_dsl.rb
|
|
163
|
+
- lib/ergane/dsl/macros.rb
|
|
163
164
|
- lib/ergane/errors.rb
|
|
164
165
|
- lib/ergane/formatter.rb
|
|
165
166
|
- lib/ergane/help_formatter.rb
|
|
@@ -167,7 +168,6 @@ files:
|
|
|
167
168
|
- lib/ergane/path_registry.rb
|
|
168
169
|
- lib/ergane/runner.rb
|
|
169
170
|
- lib/ergane/tool.rb
|
|
170
|
-
- lib/ergane/util/debug.rb
|
|
171
171
|
- lib/ergane/util/formatting.rb
|
|
172
172
|
- lib/ergane/version.rb
|
|
173
173
|
homepage: https://github.com/TwilightCoders/ergane
|
|
@@ -175,7 +175,6 @@ licenses:
|
|
|
175
175
|
- MIT
|
|
176
176
|
metadata:
|
|
177
177
|
rubygems_mfa_required: 'true'
|
|
178
|
-
homepage_uri: https://github.com/TwilightCoders/ergane
|
|
179
178
|
source_code_uri: https://github.com/TwilightCoders/ergane
|
|
180
179
|
changelog_uri: https://github.com/TwilightCoders/ergane/blob/main/CHANGELOG.md
|
|
181
180
|
rdoc_options: []
|
data/lib/ergane/util/debug.rb
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ergane
|
|
4
|
-
module Util
|
|
5
|
-
module Debug
|
|
6
|
-
def self.enabled?
|
|
7
|
-
!!$ergane_debug
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def self.enable!
|
|
11
|
-
$ergane_debug = true
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def self.disable!
|
|
15
|
-
$ergane_debug = false
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def self.log(message)
|
|
19
|
-
return unless enabled?
|
|
20
|
-
$stderr.puts "[Ergane DEBUG] #{message}"
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|