cli_class_tool 0.1.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.
@@ -0,0 +1,261 @@
1
+ module CLIClassTool
2
+ # Generic utilities for CLI class-based actions
3
+ module Utils
4
+
5
+ # Convert a string to an action symbol, validating it against available actions
6
+ #
7
+ # @param str [String] Action name
8
+ # @return [Symbol] Action symbol
9
+ # @raise [RuntimeError] If action is invalid
10
+ def stringToAction(str)
11
+ action = str.to_sym()
12
+ raise("Invalid action '#{str}'") if self.getActionAttr("ACTION_LIST").index(action) == nil
13
+ return action
14
+ end
15
+
16
+ # Convert an action symbol to a string
17
+ #
18
+ # @param sym [Symbol] Action symbol
19
+ # @return [String] Action name
20
+ def actionToString(sym)
21
+ return sym.to_s()
22
+ end
23
+
24
+ # Get attributes from all action classes
25
+ #
26
+ # @param attr [Symbol] Attribute name (e.g., "ACTION_LIST")
27
+ # @return [Hash, Array] Aggregated attributes
28
+ def getActionAttr(attr)
29
+ action_classes = self::ACTION_CLASS
30
+ common_class = self::Common
31
+
32
+ # Resolve overridden/extended class (addon) if getExtendedClass is defined
33
+ resolved_classes = action_classes.map do |x|
34
+ self.respond_to?(:getExtendedClass) ? self.getExtendedClass(x) : x
35
+ end
36
+
37
+ if common_class.const_get(attr).class == Hash
38
+ return resolved_classes.inject({}){|h, x| h.merge(x.const_get(attr))}
39
+ else
40
+ return resolved_classes.map(){|x| x.const_get(attr)}.flatten()
41
+ end
42
+ end
43
+
44
+ # Run a block on the class responsible for a specific action
45
+ #
46
+ # @param action [Symbol] The action
47
+ # @param sym [Symbol, nil] Optional method to check for existence
48
+ # @yield [Class] The class handling the action
49
+ # @return [Object] Result of the block or error code
50
+ def _runOnClass(action, sym, &block)
51
+ self::ACTION_CLASS.each(){|x|
52
+ next if x::ACTION_LIST.index(action) == nil
53
+
54
+ # Resolve overridden/extended class (addon)
55
+ class_to_use = self.respond_to?(:getExtendedClass) ? self.getExtendedClass(x) : x
56
+
57
+ if sym != nil
58
+ has_base = x.singleton_methods().index(sym) != nil
59
+ has_addon = class_to_use != x && class_to_use.singleton_methods().index(sym) != nil
60
+
61
+ if has_base || has_addon
62
+ yield(x) if has_base
63
+ yield(class_to_use) if has_addon
64
+ return 0
65
+ end
66
+ else
67
+ return yield(class_to_use)
68
+ end
69
+ return 0
70
+ }
71
+ return -1
72
+ end
73
+
74
+ # Set options for an action
75
+ #
76
+ # @param action [Symbol] The action
77
+ # @param optsParser [OptionParser] The option parser
78
+ # @param opts [Hash] The options hash
79
+ def setOpts(action, optsParser, opts)
80
+ self._runOnClass(action, :set_opts) {|kClass|
81
+ kClass.set_opts(action, optsParser, opts)
82
+ }
83
+ end
84
+
85
+ # Check options for validity
86
+ #
87
+ # @param opts [Hash] The options hash
88
+ def checkOpts(opts)
89
+ self._runOnClass(opts[:action], :check_opts) {|kClass|
90
+ kClass.check_opts(opts)
91
+ }
92
+ end
93
+
94
+ # Execute an action
95
+ #
96
+ # @param opts [Hash] The options hash
97
+ # @param action [Symbol] The action to execute
98
+ # @param error_class [Class, nil] Optional base error class to rescue
99
+ # @return [Object] Result of the action (often an Integer exit code)
100
+ def execAction(opts, action, error_class = nil)
101
+ caught_error_class = error_class || StandardError
102
+
103
+ self._runOnClass(action, nil) {|kClass|
104
+ begin
105
+ # Some class have their own execAction, because object creation might be tricky.
106
+ if kClass.respond_to?(:execAction)
107
+ ret = kClass.execAction(opts, action)
108
+ else
109
+ # Use load factory method if defined, else fall back to .new
110
+ obj = kClass.respond_to?(:load) ? kClass.load() : kClass.new()
111
+ ret = obj.public_send(action, opts)
112
+ end
113
+ return ret.is_a?(Integer) ? ret : 0
114
+ rescue caught_error_class => e
115
+ puts("# " + "ERROR".red().to_s() + ": Action '#{action}' failed: #{e.message}")
116
+ e.backtrace.each(){|l|
117
+ puts("# " + "ERROR".red().to_s() + ": \t" + l)
118
+ } if self.verbose_log
119
+
120
+ begin
121
+ return e.err_code
122
+ rescue
123
+ return 1
124
+ end
125
+ end
126
+ }
127
+ end
128
+
129
+ # Set verbose logging
130
+ #
131
+ # @param val [Boolean] True to enable verbose logging
132
+ def verbose_log=(val)
133
+ @verbose_log = val
134
+ end
135
+
136
+ # Get verbose logging status
137
+ #
138
+ # @return [Boolean] Verbose logging status
139
+ def verbose_log()
140
+ @verbose_log
141
+ end
142
+
143
+ # Load all custom addon classes/files from a directory
144
+ #
145
+ # @param path [String] Absolute or relative directory path containing .rb files
146
+ def loadAddons(path)
147
+ return unless Dir.exist?(path)
148
+
149
+ $LOAD_PATH.push(path)
150
+ Dir.entries(path).each() do |entry|
151
+ next if !File.file?(File.join(path, entry)) || entry !~ /\.rb$/
152
+ require entry.sub(/\.rb$/, "")
153
+ end
154
+ $LOAD_PATH.pop()
155
+ end
156
+
157
+ # Safely load an overridden/extended class instance using a generic addon_key
158
+ def loadClass(default_class, addon_key, *more)
159
+ @load_class ||= []
160
+ @load_class.push(default_class)
161
+
162
+ # Resolve overridden class using getExtendedClass if available
163
+ extended_class = self.respond_to?(:getExtendedClass) ? self.getExtendedClass(default_class, addon_key) : default_class
164
+ obj = extended_class.new(*more)
165
+ @load_class.pop()
166
+ return obj
167
+ end
168
+
169
+ # Validate that the constructor was only called through loadClass
170
+ def checkDirectConstructor(theClass)
171
+ @load_class ||= []
172
+ curLoad = @load_class.last()
173
+ cl = theClass
174
+ while cl != Object
175
+ return if cl == curLoad
176
+ cl = cl.superclass
177
+ end
178
+ raise("Use #{self.name}::loadClass to construct a #{theClass} class")
179
+ end
180
+
181
+ # Generic CLI runner and argument parser for class-based applications.
182
+ #
183
+ # @param opts [Hash] Initial options hash
184
+ # @param argv [Array<String>] Command line arguments (defaults to ARGV)
185
+ # @yield [parser, phase, action_opts] Custom options setup callback block
186
+ def run_cli(opts = {}, argv = ARGV)
187
+ # Fetch actions and action helps
188
+ action_helps = self.getActionAttr("ACTION_HELP")
189
+ action_list = self.getActionAttr("ACTION_LIST")
190
+
191
+ # 1. Action Parser Setup
192
+ action_parser = OptionParser.new(nil, 60)
193
+ action_parser.banner = "Usage: #{$0} <action> [action options]"
194
+ action_parser.separator ""
195
+ action_parser.separator "Options:"
196
+ action_parser.on("-h", "--help", "Display usage.") { puts action_parser.to_s; exit 0 }
197
+ action_parser.on("--verbose", "Displays more informations.") { self.verbose_log = true }
198
+
199
+ # Yield parser to allow caller to customize the global options
200
+ yield(action_parser, :global, opts) if block_given?
201
+
202
+ action_parser.separator "Possible actions:"
203
+
204
+ # Format actions nicely with padding
205
+ max_len = action_helps.keys.map { |k| self.actionToString(k).length }.max || 0
206
+ col_width = max_len + 4
207
+ action_helps.each do |k, x|
208
+ indent = col_width - self.actionToString(k).length
209
+ action_parser.separator "\t * " + self.actionToString(k) + (" " * indent) + x
210
+ end
211
+
212
+ # Include any registered custom addon class listings if defined
213
+ if self.respond_to?(:getCustomClasses) && self.getCustomClasses.length > 0
214
+ action_parser.separator "Custom repo addons available:"
215
+ self.getCustomClasses.each do |k, x|
216
+ action_parser.separator "\t * #{k}"
217
+ end
218
+ end
219
+
220
+ rest = action_parser.order!(argv)
221
+ if rest.length <= 0
222
+ STDERR.puts("Error: No action provided")
223
+ puts action_parser.to_s
224
+ exit 1
225
+ end
226
+
227
+ action_s = argv[0]
228
+ action = opts[:action] = self.stringToAction(action_s)
229
+ argv.shift()
230
+
231
+ # 2. Options Parser Setup
232
+ opts_parser = OptionParser.new(nil, 60)
233
+ opts_parser.banner = "Usage: #{$0} #{action_s} "
234
+ opts_parser.separator "# " + action_helps[action].to_s()
235
+ opts_parser.separator ""
236
+ opts_parser.separator "Options:"
237
+ opts_parser.on("-h", "--help", "Display usage.") { puts opts_parser.to_s; exit 0 }
238
+ opts_parser.on("-n", "--no", "Assume no to all questions.") { opts[:yn_default] = :no }
239
+ opts_parser.on("-y", "--yes", "Assume yes to all questions.") { opts[:yn_default] = :yes }
240
+ opts_parser.on("--verbose", "Displays more informations.") { self.verbose_log = true }
241
+
242
+ # Provide custom block hook for option parser customization
243
+ yield(opts_parser, :action, opts) if block_given?
244
+
245
+ # Set options on classes
246
+ self.setOpts(action, opts_parser, opts)
247
+
248
+ # Order remaining arguments
249
+ if opts[:ignore_opts] != true
250
+ rest = opts_parser.order!(argv)
251
+ raise("Extra Unexpected extra arguments provided: " + rest.map(){|x|"'" + x + "'"}.join(", ")) if rest.length != 0
252
+ else
253
+ opts[:extra_args] = argv
254
+ end
255
+
256
+ # Validate options and execute action
257
+ self.checkOpts(opts)
258
+ exit self.execAction(opts, action)
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,3 @@
1
+ require_relative 'cli_class_tool/string'
2
+ require_relative 'cli_class_tool/common'
3
+ require_relative 'cli_class_tool/utils'
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cli_class_tool
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nicolas Morey-Chaisemartin
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-06-01 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '12.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '12.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: minitest
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '5.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: yard
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0.8'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0.8'
54
+ description: CLIClassTool decouples the generic execution, logging, and action routing
55
+ engine from project-specific business logic.
56
+ email: nmoreychaisemartin@suse.de
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - LICENSE
62
+ - README.md
63
+ - lib/cli_class_tool.rb
64
+ - lib/cli_class_tool/common.rb
65
+ - lib/cli_class_tool/string.rb
66
+ - lib/cli_class_tool/utils.rb
67
+ homepage: https://github.com/nmorey/cli_class_tool
68
+ licenses:
69
+ - GPL-3.0-or-later
70
+ metadata: {}
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '2.7'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubygems_version: 4.0.10
86
+ specification_version: 4
87
+ summary: A lightweight object-oriented framework for class-based command-line interface
88
+ (CLI) applications.
89
+ test_files: []