cli_class_tool 0.4.0 → 0.5.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 +19 -0
- data/README.md +99 -0
- data/lib/cli_class_tool/common.rb +13 -3
- data/lib/cli_class_tool/utils.rb +214 -27
- data/lib/cli_class_tool.rb +10 -22
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c2833d3bd929aa6e69df3df0cbadbbc3e92030afdbf8172387869a9ad4f95f3
|
|
4
|
+
data.tar.gz: 3bac4466f6a3d4e07bc739e36eb0eaaba2929c5a308a980715e4d0c55ef11fd7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f8ab8e7152ade287dc0a8c50598d2069cbbd99ecbcc5d0bb9cc7fd37a8a9e3fa401a889030c3bcea9a387cb2f5ac5a8ab49ef40b78980a982355ecdcba3bd53
|
|
7
|
+
data.tar.gz: d031b3ca4da59379e5f3079ad091f10fd4934b0d1150df9f846ea2de77e1d439e236ac2070763418083394956cef64ed3f7ec2f7883222c66b14cd24e3ea9854
|
data/CHANGELOG
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
------------------
|
|
2
|
+
0.5.0 (2026-07-22)
|
|
3
|
+
------------------
|
|
4
|
+
|
|
5
|
+
* Support CLI_HELP_EXPAND on sub-CLIClass modules to recursively list sub-actions in parent help outputs, including support for boolean or custom string headers
|
|
6
|
+
* Support recursive subcommand alias and shortcut resolution within deeply nested sub-CLIClasses
|
|
7
|
+
* Add CLI_COMMAND_ALIASES support to define top-level command shortcuts/aliases to deeply-nested subcommand actions
|
|
8
|
+
* Filter list_actions command output to exclude itself, preserving clean autocomplete query behavior
|
|
9
|
+
* Require optparse explicitly inside utils.rb to prevent NameErrors in production environments
|
|
10
|
+
* Safeguard attribute resolution and _runOnClass to gracefully default when ACTION_HELP/ACTION_LIST is missing
|
|
11
|
+
* Add recursive nested subcommand and namespace auto-discovery support
|
|
12
|
+
|
|
13
|
+
------------------
|
|
14
|
+
0.4.1 (2026-06-05)
|
|
15
|
+
------------------
|
|
16
|
+
|
|
17
|
+
* Fix definition on RunError
|
|
18
|
+
* In DEBUG=1 mode, run() call stack depth can be tuned with DEBUG_CALL_STACK=<n>
|
|
19
|
+
|
|
1
20
|
------------------
|
|
2
21
|
0.4.0 (2026-06-05)
|
|
3
22
|
------------------
|
data/README.md
CHANGED
|
@@ -294,4 +294,103 @@ MyProject.run_cli(opts) do |parser, phase, action_opts|
|
|
|
294
294
|
end
|
|
295
295
|
end
|
|
296
296
|
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Nested CLI Subcommands (Subcommands & Recursive Routing)
|
|
301
|
+
|
|
302
|
+
`CLIClassTool` natively supports nested CLI subcommands, enabling hierarchical structures of the form:
|
|
303
|
+
```
|
|
304
|
+
$ mytool <subcommand> <action> [options]
|
|
305
|
+
$ mytool <subcommand> <sub_subcommand> <action> [options]
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
This allows you to decouple complex tools into self-contained sub-CLI components while executing them seamlessly under a single binary entry point.
|
|
309
|
+
|
|
310
|
+
### 1. Auto-Discovery & Setup
|
|
311
|
+
|
|
312
|
+
Any nested module or class defined within your parent CLI module that extends `CLIClassTool::Utils` is **automatically discovered** as a subcommand:
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
module MyProject
|
|
316
|
+
extend CLIClassTool::Utils # Parent CLI
|
|
317
|
+
|
|
318
|
+
# 1. Nesting a subcommand module
|
|
319
|
+
module ConfigCLI
|
|
320
|
+
# Define custom subcommand help description
|
|
321
|
+
CLI_DESCRIPTION = "Manage application configurations"
|
|
322
|
+
|
|
323
|
+
# Optional: Customize the command name (defaults to "config_cli")
|
|
324
|
+
CLI_COMMAND_NAME = "config"
|
|
325
|
+
|
|
326
|
+
class ConfigError < StandardError; end
|
|
327
|
+
|
|
328
|
+
class Common < CLIClassTool::Common
|
|
329
|
+
def parent_module; ConfigCLI; end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
class ConfigAction < Common
|
|
333
|
+
ACTION_LIST = [ :set_val ]
|
|
334
|
+
ACTION_HELP = { :set_val => "Set a config value" }
|
|
335
|
+
|
|
336
|
+
def set_val(opts)
|
|
337
|
+
# business logic...
|
|
338
|
+
return 0
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
ACTION_CLASS = [ ConfigAction ]
|
|
343
|
+
extend CLIClassTool::Utils
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### 2. Help Aggregation & Name Resolution
|
|
349
|
+
|
|
350
|
+
- **Naming:** By default, subcommand trigger words are derived from the inner module/class name converted to `snake_case` (e.g., `ConfigCLI` becomes `config_cli`). Define `CLI_COMMAND_NAME = "custom_name"` on your submodule to override this behavior.
|
|
351
|
+
- **Help Menus:** Nested subcommands and their descriptions (`CLI_DESCRIPTION` or `HELP` constants) are automatically collected and listed in the parent CLI's usage output under `Possible actions:`.
|
|
352
|
+
- **Option Forwarding:** Global customization blocks and verbosity flags are recursively passed down to the active subcommand.
|
|
353
|
+
- **Help Expansion:** By default, `--help` lists the trigger name and description of subcommands. You can declare `CLI_HELP_EXPAND = true` on a sub-CLI module to recursively list all of its individual actions directly in the parent's help menu. If `CLI_HELP_EXPAND` is a `String` (e.g., `"Config options:"`), that string will be printed as a dedicated help header separator inside the parent help menu.
|
|
354
|
+
|
|
355
|
+
### 3. Command Aliases (Shortcuts)
|
|
356
|
+
|
|
357
|
+
You can define command shortcuts/aliases at any CLI level by declaring a `CLI_COMMAND_ALIASES` Hash constant mapping shortcut names (symbols or strings) to subcommand/action pathways:
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
module MyProject
|
|
361
|
+
extend CLIClassTool::Utils
|
|
362
|
+
|
|
363
|
+
# Map shortcuts to deeply-nested actions
|
|
364
|
+
CLI_COMMAND_ALIASES = {
|
|
365
|
+
"myalias" => ["config", "set_val"],
|
|
366
|
+
"string_alias" => "config set_val"
|
|
367
|
+
}
|
|
368
|
+
end
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Running `$ mytool myalias --foo bar` will automatically expand and route arguments exactly as if the user had typed `$ mytool config set_val --foo bar`.
|
|
372
|
+
These shortcuts are also dynamically collected and formatted under a dedicated `Command aliases:` section when printing `--help`.
|
|
373
|
+
|
|
374
|
+
### 4. Seamless Subcommand Exception Handling
|
|
375
|
+
|
|
376
|
+
`CLIClassTool` provides dynamic subclass exception matching to solve a common design challenge: *rescuing any exception originating from nested sub-CLIs under a single, central parent rescue block.*
|
|
377
|
+
|
|
378
|
+
When you extend `CLIClassTool::Utils` on a parent module, `CLIClassTool` overrides the case-equality (`===`) operator on the parent's base error class. This ensures:
|
|
379
|
+
- Any child subcommand exception (such as `MyProject::ConfigCLI::RunError`) is **seamlessly rescued** by the parent base error class.
|
|
380
|
+
- The original exception classes and names remain **completely unchanged**, meaning they can still be rescued by their specific sub-CLI exception class as normal.
|
|
381
|
+
|
|
382
|
+
This enables infinite sub-CLI nesting while preserving modular reuse across different parent projects or as standalone binaries:
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
# In bin/mytool
|
|
386
|
+
begin
|
|
387
|
+
MyProject.run_cli
|
|
388
|
+
rescue MyProjectError => e
|
|
389
|
+
# Seamlessly catches errors from MyProject::RunError,
|
|
390
|
+
# MyProject::ConfigCLI::RunError, or any deeper sublevel!
|
|
391
|
+
STDERR.puts "# ERROR: #{e.message}"
|
|
392
|
+
exit e.respond_to?(:err_code) ? e.err_code : 1
|
|
393
|
+
end
|
|
394
|
+
```
|
|
395
|
+
|
|
297
396
|
```
|
|
@@ -25,7 +25,8 @@ module CLIClassTool
|
|
|
25
25
|
# @param obj [Object,Class] Object or class to get the Module from
|
|
26
26
|
# @raise [StandardError] If command failed
|
|
27
27
|
def obj_to_parent_mod(obj)
|
|
28
|
-
return obj if obj.
|
|
28
|
+
return obj if obj.class == Module
|
|
29
|
+
|
|
29
30
|
theClass = obj.is_a?(Class) ? obj : obj.class
|
|
30
31
|
if theClass.name.nil?
|
|
31
32
|
return Object
|
|
@@ -137,7 +138,14 @@ module CLIClassTool
|
|
|
137
138
|
# @param cmd_type [String] Type of command (e.g., 'git')
|
|
138
139
|
# @param cmd [String] The command string
|
|
139
140
|
def cmd_debug(cmd_type, cmd)
|
|
140
|
-
log(:DEBUG, "Called from
|
|
141
|
+
log(:DEBUG, "Called from:")
|
|
142
|
+
depth=1
|
|
143
|
+
if ENV["DEBUG_CALL_DEPTH"].to_s() != ""
|
|
144
|
+
depth = ENV["DEBUG_CALL_DEPTH"].to_i()
|
|
145
|
+
end
|
|
146
|
+
[ depth, caller.length].min.downto(1){|x|
|
|
147
|
+
log(:DEBUG, " #{caller[x]}")
|
|
148
|
+
}
|
|
141
149
|
log(:DEBUG, "Running #{cmd_type} command '#{cmd}'")
|
|
142
150
|
end
|
|
143
151
|
|
|
@@ -214,7 +222,9 @@ module CLIClassTool
|
|
|
214
222
|
# @param opts [Hash] Options hash
|
|
215
223
|
# @return [Integer] 0
|
|
216
224
|
def list_actions(opts)
|
|
217
|
-
|
|
225
|
+
actions = parent_module.getActionAttr("ACTION_LIST").map(){|x| parent_module.actionToString(x)}
|
|
226
|
+
actions.reject! { |x| x == "list_actions" }
|
|
227
|
+
puts actions.join("\n")
|
|
218
228
|
return 0
|
|
219
229
|
end
|
|
220
230
|
end
|
data/lib/cli_class_tool/utils.rb
CHANGED
|
@@ -1,20 +1,109 @@
|
|
|
1
|
+
require 'optparse'
|
|
2
|
+
|
|
1
3
|
module CLIClassTool
|
|
2
4
|
# Generic utilities for CLI class-based actions
|
|
3
5
|
module Utils
|
|
4
6
|
|
|
5
7
|
# Hook called when a module extends CLIClassTool::Utils
|
|
6
8
|
def self.extended(base)
|
|
7
|
-
|
|
9
|
+
return if base == nil
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
CLIClassTool::RunError
|
|
11
|
+
lower_mod = base.name.split('::')[-1]
|
|
12
|
+
superclass_name = "#{base.name}::#{lower_mod}Error"
|
|
13
|
+
|
|
14
|
+
if ! Object.const_defined?(superclass_name)
|
|
15
|
+
raise("Could not find a base error class named #{superclass_name}")
|
|
15
16
|
end
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
error_class = Object.const_get(superclass_name)
|
|
19
|
+
|
|
20
|
+
# Override === on the error class to match nested subcommand errors recursively
|
|
21
|
+
error_class.singleton_class.class_eval do
|
|
22
|
+
define_method(:_cli_host_module) { base }
|
|
23
|
+
|
|
24
|
+
def ===(other)
|
|
25
|
+
return true if super
|
|
26
|
+
|
|
27
|
+
if other.is_a?(StandardError) && other.respond_to?(:err_code)
|
|
28
|
+
other_class_name = other.class.name
|
|
29
|
+
if other_class_name
|
|
30
|
+
parts = other_class_name.split('::')
|
|
31
|
+
if parts.size > 1
|
|
32
|
+
parent_mod_name = parts[0...-1].join('::')
|
|
33
|
+
host = _cli_host_module
|
|
34
|
+
if host.respond_to?(:cli_sub_actions)
|
|
35
|
+
all_sub_names = _all_sub_module_names(host)
|
|
36
|
+
return all_sub_names.include?(parent_mod_name)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
define_method(:_all_sub_module_names) do |host_mod|
|
|
46
|
+
names = []
|
|
47
|
+
if host_mod.respond_to?(:cli_sub_actions)
|
|
48
|
+
host_mod.cli_sub_actions.values.each do |sub_cli|
|
|
49
|
+
if sub_cli.name
|
|
50
|
+
names << sub_cli.name
|
|
51
|
+
names.concat(_all_sub_module_names(sub_cli))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
names.uniq
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
CLIClassTool.define_run_error(base, error_class)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Convert CamelCase to snake_case
|
|
63
|
+
# @param str [String]
|
|
64
|
+
# @return [String]
|
|
65
|
+
def _to_snake_case(str)
|
|
66
|
+
str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
67
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
68
|
+
.tr('-', '_')
|
|
69
|
+
.downcase
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Dynamically discover and return a map of sub-CLI tools
|
|
73
|
+
#
|
|
74
|
+
# @return [Hash<String, Module>] Map of subcommand string to CLI modules
|
|
75
|
+
def cli_sub_actions
|
|
76
|
+
sub_actions = {}
|
|
77
|
+
|
|
78
|
+
# First, check if manual CLI_SUB_ACTIONS mapping exists
|
|
79
|
+
if self.const_defined?(:CLI_SUB_ACTIONS)
|
|
80
|
+
manual_actions = self::CLI_SUB_ACTIONS
|
|
81
|
+
if manual_actions.is_a?(Hash)
|
|
82
|
+
manual_actions.each do |k, v|
|
|
83
|
+
sub_actions[k.to_s] = v
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Then, dynamically discover any inner modules/classes extending CLIClassTool::Utils
|
|
89
|
+
self.constants(false).each do |const_sym|
|
|
90
|
+
begin
|
|
91
|
+
const_val = self.const_get(const_sym)
|
|
92
|
+
if const_val.is_a?(Module) && const_val.is_a?(CLIClassTool::Utils)
|
|
93
|
+
cmd_name = if const_val.const_defined?(:CLI_COMMAND_NAME)
|
|
94
|
+
const_val::CLI_COMMAND_NAME.to_s
|
|
95
|
+
else
|
|
96
|
+
_to_snake_case(const_sym.to_s)
|
|
97
|
+
end
|
|
98
|
+
# Only add if not already manually specified
|
|
99
|
+
sub_actions[cmd_name] ||= const_val
|
|
100
|
+
end
|
|
101
|
+
rescue NameError, LoadError
|
|
102
|
+
# ignore uninitialized autoloads
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
return sub_actions
|
|
18
107
|
end
|
|
19
108
|
|
|
20
109
|
# Convert a string to an action symbol, validating it against available actions
|
|
@@ -41,19 +130,67 @@ module CLIClassTool
|
|
|
41
130
|
# @param attr [Symbol] Attribute name (e.g., "ACTION_LIST")
|
|
42
131
|
# @return [Hash, Array] Aggregated attributes
|
|
43
132
|
def getActionAttr(attr)
|
|
44
|
-
|
|
45
|
-
|
|
133
|
+
common_class = self.const_defined?(:Common) ? self::Common : CLIClassTool::Common
|
|
134
|
+
is_hash = if common_class.const_defined?(attr)
|
|
135
|
+
common_class.const_get(attr).is_a?(Hash)
|
|
136
|
+
else
|
|
137
|
+
attr.to_s.include?("HELP")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
action_classes = self.const_defined?(:ACTION_CLASS) ? self::ACTION_CLASS : []
|
|
46
141
|
|
|
47
142
|
# Resolve overridden/extended class (addon) if getExtendedClass is defined
|
|
48
143
|
resolved_classes = action_classes.map do |x|
|
|
49
144
|
self.respond_to?(:getExtendedClass) ? self.getExtendedClass(x) : x
|
|
50
145
|
end
|
|
51
146
|
|
|
52
|
-
|
|
53
|
-
|
|
147
|
+
res = if is_hash
|
|
148
|
+
resolved_classes.inject({}) do |h, x|
|
|
149
|
+
x.const_defined?(attr) ? h.merge(x.const_get(attr)) : h
|
|
150
|
+
end
|
|
54
151
|
else
|
|
55
|
-
|
|
152
|
+
resolved_classes.map do |x|
|
|
153
|
+
x.const_defined?(attr) ? x.const_get(attr) : []
|
|
154
|
+
end.flatten()
|
|
56
155
|
end
|
|
156
|
+
|
|
157
|
+
# If it's ACTION_LIST, append discovered subcommand names
|
|
158
|
+
if attr.to_s == "ACTION_LIST"
|
|
159
|
+
sub_actions = self.cli_sub_actions
|
|
160
|
+
res += sub_actions.keys.map(&:to_sym)
|
|
161
|
+
res << :list_actions unless res.include?(:list_actions)
|
|
162
|
+
# If it's ACTION_HELP, merge subcommand helps
|
|
163
|
+
elsif attr.to_s == "ACTION_HELP"
|
|
164
|
+
sub_helps = {}
|
|
165
|
+
self.cli_sub_actions.each do |cmd_name, sub_cli|
|
|
166
|
+
expand_val = sub_cli.const_defined?(:CLI_HELP_EXPAND) ? sub_cli::CLI_HELP_EXPAND : nil
|
|
167
|
+
if expand_val
|
|
168
|
+
if expand_val.is_a?(String)
|
|
169
|
+
sub_helps[expand_val] = ""
|
|
170
|
+
end
|
|
171
|
+
sub_helps_rec = sub_cli.getActionAttr("ACTION_HELP")
|
|
172
|
+
sub_helps_rec.each do |k, desc|
|
|
173
|
+
sub_helps["#{cmd_name} #{k}".to_sym] = desc
|
|
174
|
+
end
|
|
175
|
+
else
|
|
176
|
+
desc = ""
|
|
177
|
+
if sub_cli.const_defined?(:CLI_DESCRIPTION)
|
|
178
|
+
desc = sub_cli::CLI_DESCRIPTION
|
|
179
|
+
elsif sub_cli.const_defined?(:HELP)
|
|
180
|
+
desc = sub_cli::HELP
|
|
181
|
+
end
|
|
182
|
+
sub_helps[cmd_name.to_sym] = desc
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
if self.const_defined?(:CLI_SUB_ACTIONS_HELP)
|
|
186
|
+
self::CLI_SUB_ACTIONS_HELP.each do |k, desc|
|
|
187
|
+
sub_helps[k.to_sym] = desc
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
res = res.merge(sub_helps)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
return res
|
|
57
194
|
end
|
|
58
195
|
|
|
59
196
|
# Run a block on the class responsible for a specific action
|
|
@@ -63,8 +200,9 @@ module CLIClassTool
|
|
|
63
200
|
# @yield [Class] The class handling the action
|
|
64
201
|
# @return [Object] Result of the block or error code
|
|
65
202
|
def _runOnClass(action, sym, &block)
|
|
203
|
+
return -1 unless self.const_defined?(:ACTION_CLASS)
|
|
66
204
|
self::ACTION_CLASS.each(){|x|
|
|
67
|
-
next if x::ACTION_LIST.index(action) == nil
|
|
205
|
+
next if !x.const_defined?(:ACTION_LIST) || x::ACTION_LIST.index(action) == nil
|
|
68
206
|
|
|
69
207
|
# Resolve overridden/extended class (addon)
|
|
70
208
|
class_to_use = self.respond_to?(:getExtendedClass) ? self.getExtendedClass(x) : x
|
|
@@ -115,16 +253,12 @@ module CLIClassTool
|
|
|
115
253
|
def execAction(opts, action, error_class = nil)
|
|
116
254
|
caught_error_class = error_class || StandardError
|
|
117
255
|
|
|
118
|
-
self._runOnClass(action, nil) {|kClass|
|
|
256
|
+
ret_code = self._runOnClass(action, nil) {|kClass|
|
|
119
257
|
begin
|
|
120
|
-
#
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
# Use load factory method if defined, else fall back to .new
|
|
125
|
-
obj = kClass.respond_to?(:load) ? kClass.load() : kClass.new()
|
|
126
|
-
ret = obj.public_send(action, opts)
|
|
127
|
-
end
|
|
258
|
+
# Use load factory method if defined, else fall back to .new
|
|
259
|
+
obj = kClass.respond_to?(:load) ? kClass.load() : kClass.new()
|
|
260
|
+
ret = obj.public_send(action, opts)
|
|
261
|
+
|
|
128
262
|
return ret.is_a?(Integer) ? ret : 0
|
|
129
263
|
rescue caught_error_class => e
|
|
130
264
|
puts("# " + "ERROR".red().to_s() + ": Action '#{action}' failed: #{e.message}")
|
|
@@ -139,6 +273,15 @@ module CLIClassTool
|
|
|
139
273
|
end
|
|
140
274
|
end
|
|
141
275
|
}
|
|
276
|
+
|
|
277
|
+
if ret_code == -1 && action == :list_actions
|
|
278
|
+
actions = self.getActionAttr("ACTION_LIST").map(){|x| self.actionToString(x)}
|
|
279
|
+
actions.reject! { |x| x == "list_actions" }
|
|
280
|
+
puts actions.join("\n")
|
|
281
|
+
return 0
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
return ret_code
|
|
142
285
|
end
|
|
143
286
|
|
|
144
287
|
# Set verbose logging
|
|
@@ -198,7 +341,7 @@ module CLIClassTool
|
|
|
198
341
|
# @param opts [Hash] Initial options hash
|
|
199
342
|
# @param argv [Array<String>] Command line arguments (defaults to ARGV)
|
|
200
343
|
# @yield [parser, phase, action_opts] Custom options setup callback block
|
|
201
|
-
def run_cli(opts = {}, argv = ARGV)
|
|
344
|
+
def run_cli(opts = {}, argv = ARGV, &block)
|
|
202
345
|
# Fetch actions and action helps
|
|
203
346
|
action_helps = self.getActionAttr("ACTION_HELP")
|
|
204
347
|
|
|
@@ -220,7 +363,7 @@ module CLIClassTool
|
|
|
220
363
|
col_width = max_len + 4
|
|
221
364
|
action_helps.each do |k, x|
|
|
222
365
|
indent = col_width - self.actionToString(k).length
|
|
223
|
-
action_parser.separator "\t * " + self.actionToString(k) + (" " * indent) + x
|
|
366
|
+
action_parser.separator "\t * " + self.actionToString(k) + (" " * indent) + x.to_s
|
|
224
367
|
end
|
|
225
368
|
|
|
226
369
|
# Include any registered custom addon class listings if defined
|
|
@@ -231,6 +374,19 @@ module CLIClassTool
|
|
|
231
374
|
end
|
|
232
375
|
end
|
|
233
376
|
|
|
377
|
+
# Include command aliases if defined
|
|
378
|
+
if self.const_defined?(:CLI_COMMAND_ALIASES)
|
|
379
|
+
aliases = self::CLI_COMMAND_ALIASES
|
|
380
|
+
if aliases.is_a?(Hash) && aliases.length > 0
|
|
381
|
+
action_parser.separator ""
|
|
382
|
+
action_parser.separator "Command aliases:"
|
|
383
|
+
aliases.each do |k, v|
|
|
384
|
+
exp_str = v.is_a?(Array) ? v.join(" ") : v.to_s
|
|
385
|
+
action_parser.separator "\t * #{k} -> #{exp_str}"
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
234
390
|
rest = action_parser.order!(argv)
|
|
235
391
|
if rest.length <= 0
|
|
236
392
|
STDERR.puts("Error: No action provided")
|
|
@@ -238,7 +394,38 @@ module CLIClassTool
|
|
|
238
394
|
exit 1
|
|
239
395
|
end
|
|
240
396
|
|
|
397
|
+
# Expand command aliases
|
|
398
|
+
if self.const_defined?(:CLI_COMMAND_ALIASES)
|
|
399
|
+
aliases = self::CLI_COMMAND_ALIASES
|
|
400
|
+
if aliases.is_a?(Hash)
|
|
401
|
+
action_s = argv[0]
|
|
402
|
+
action_sym = action_s ? action_s.to_sym : nil
|
|
403
|
+
if aliases.key?(action_s) || (action_sym && aliases.key?(action_sym))
|
|
404
|
+
expansion = aliases[action_s] || aliases[action_sym]
|
|
405
|
+
expansion = expansion.split(' ') if expansion.is_a?(String)
|
|
406
|
+
argv.shift()
|
|
407
|
+
argv.unshift(*expansion)
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
241
412
|
action_s = argv[0]
|
|
413
|
+
|
|
414
|
+
# Intercept subcommands here!
|
|
415
|
+
sub_actions = self.cli_sub_actions
|
|
416
|
+
if sub_actions.key?(action_s)
|
|
417
|
+
sub_cli = sub_actions[action_s]
|
|
418
|
+
if sub_cli.respond_to?(:verbose_log=)
|
|
419
|
+
sub_cli.verbose_log = self.verbose_log
|
|
420
|
+
end
|
|
421
|
+
argv.shift()
|
|
422
|
+
if block
|
|
423
|
+
exit(sub_cli.run_cli(opts, argv, &block))
|
|
424
|
+
else
|
|
425
|
+
exit(sub_cli.run_cli(opts, argv))
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
242
429
|
action = opts[:action] = self.stringToAction(action_s)
|
|
243
430
|
argv.shift()
|
|
244
431
|
|
|
@@ -260,11 +447,11 @@ module CLIClassTool
|
|
|
260
447
|
self.setOpts(action, opts_parser, opts)
|
|
261
448
|
|
|
262
449
|
# Order remaining arguments
|
|
450
|
+
rest = opts_parser.order!(argv)
|
|
263
451
|
if opts[:ignore_opts] != true
|
|
264
|
-
rest = opts_parser.order!(argv)
|
|
265
452
|
raise("Extra Unexpected extra arguments provided: " + rest.map(){|x|"'" + x + "'"}.join(", ")) if rest.length != 0
|
|
266
453
|
else
|
|
267
|
-
opts[:extra_args] =
|
|
454
|
+
opts[:extra_args] = rest
|
|
268
455
|
end
|
|
269
456
|
|
|
270
457
|
# Validate options and execute action
|
data/lib/cli_class_tool.rb
CHANGED
|
@@ -1,31 +1,19 @@
|
|
|
1
1
|
module CLIClassTool
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
def self.define_run_error(parent_module, superclass)
|
|
3
|
+
klass = Class.new(superclass)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@output = output
|
|
8
|
-
super("Command failed with exit status #{err_code}#{output ? "\n#{output}" : ''}")
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def self.define_run_error(parent_module, superclass = CLIClassTool::RunError)
|
|
13
|
-
klass = Class.new(superclass)
|
|
14
|
-
|
|
15
|
-
if !(superclass <= CLIClassTool::RunError)
|
|
16
|
-
klass.class_eval do
|
|
17
|
-
attr_reader :err_code, :output
|
|
5
|
+
klass.class_eval do
|
|
6
|
+
attr_reader :err_code, :msg
|
|
18
7
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
8
|
+
def initialize(err_code, output = nil)
|
|
9
|
+
@err_code = err_code
|
|
10
|
+
@msg = output
|
|
11
|
+
super("Command failed with exit status #{err_code}")
|
|
12
|
+
end
|
|
23
13
|
end
|
|
24
|
-
|
|
14
|
+
parent_module.const_set(:RunError, klass)
|
|
25
15
|
end
|
|
26
16
|
|
|
27
|
-
parent_module.const_set(:RunError, klass)
|
|
28
|
-
end
|
|
29
17
|
end
|
|
30
18
|
|
|
31
19
|
require_relative 'cli_class_tool/string'
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cli_class_tool
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nicolas Morey
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-07-22 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rake
|
|
@@ -83,7 +83,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
83
83
|
- !ruby/object:Gem::Version
|
|
84
84
|
version: '0'
|
|
85
85
|
requirements: []
|
|
86
|
-
rubygems_version: 4.0.
|
|
86
|
+
rubygems_version: 4.0.16
|
|
87
87
|
specification_version: 4
|
|
88
88
|
summary: A lightweight object-oriented framework for class-based command-line interface
|
|
89
89
|
(CLI) applications.
|