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.
- checksums.yaml +7 -0
- data/LICENSE +674 -0
- data/README.md +245 -0
- data/lib/cli_class_tool/common.rb +170 -0
- data/lib/cli_class_tool/string.rb +48 -0
- data/lib/cli_class_tool/utils.rb +261 -0
- data/lib/cli_class_tool.rb +3 -0
- metadata +89 -0
|
@@ -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
|
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: []
|