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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bad275062a9f72ff201812eb6cd7871b9321b80b57f379f817cab483de42b738
4
- data.tar.gz: e3cb70b12fe4831a4fdd194eaa4c67118efcfd9ffc0f1903524d74bdaf805e1a
3
+ metadata.gz: 2c2833d3bd929aa6e69df3df0cbadbbc3e92030afdbf8172387869a9ad4f95f3
4
+ data.tar.gz: 3bac4466f6a3d4e07bc739e36eb0eaaba2929c5a308a980715e4d0c55ef11fd7
5
5
  SHA512:
6
- metadata.gz: 43cc9f0156398b92a5d1c7370c6c8c7dcf61d80e4ad9b18bec8a4a5bedceec7aa9e9af40e5d801554c7369aa746bac98c204ad949d2d27b381c4e14cbec93312
7
- data.tar.gz: '0948e6f13057dd174140d84a7845c33a25e758a9ef018e10afb9c75bb734e87dffaf191e81091e01fe8c1dc9ff66437672364343d7ffc054f76cf0d591ebec1d'
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.is_a?(Module)
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 #{caller[1]}")
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
- puts parent_module.getActionAttr("ACTION_LIST").map(){|x| parent_module.actionToString(x)}.join("\n")
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
@@ -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
- possible_name = base.name ? "#{base.name}Error" : nil
9
+ return if base == nil
8
10
 
9
- superclass = if possible_name && Object.const_defined?(possible_name)
10
- Object.const_get(possible_name)
11
- elsif base.const_defined?(:Error)
12
- base.const_get(:Error)
13
- else
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
- CLIClassTool.define_run_error(base, superclass)
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
- action_classes = self::ACTION_CLASS
45
- common_class = self::Common
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
- if common_class.const_get(attr).class == Hash
53
- return resolved_classes.inject({}){|h, x| h.merge(x.const_get(attr))}
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
- return resolved_classes.map(){|x| x.const_get(attr)}.flatten()
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
- # Some class have their own execAction, because object creation might be tricky.
121
- if kClass.respond_to?(:execAction)
122
- ret = kClass.execAction(opts, action)
123
- else
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] = argv
454
+ opts[:extra_args] = rest
268
455
  end
269
456
 
270
457
  # Validate options and execute action
@@ -1,31 +1,19 @@
1
1
  module CLIClassTool
2
- class RunError < StandardError
3
- attr_reader :err_code, :output
2
+ def self.define_run_error(parent_module, superclass)
3
+ klass = Class.new(superclass)
4
4
 
5
- def initialize(err_code, output = nil)
6
- @err_code = err_code
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
- def initialize(err_code, output = nil)
20
- @err_code = err_code
21
- @output = output
22
- super("Command failed with exit status #{err_code}#{output ? "\n#{output}" : ''}")
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
- end
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.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-06-05 00:00:00.000000000 Z
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.10
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.