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.
data/README.md ADDED
@@ -0,0 +1,245 @@
1
+ # CLIClassTool
2
+
3
+ `CLIClassTool` is a lightweight, object-oriented framework for building class-based command-line interface (CLI) applications. It decouples the generic execution, logging, and action routing engine from project-specific business logic, making it extremely easy to synchronize across multiple projects or package as a shared gem.
4
+
5
+ ---
6
+
7
+ ## Directory Structure
8
+
9
+ To use `CLIClassTool` in your project, copy or subtree the `cli_class_tool/` directory under your library path (typically `lib/`):
10
+
11
+ ```
12
+ lib/
13
+ └── cli_class_tool/
14
+ ├── README.md
15
+ ├── common.rb # Generic common helper class (logging, shell exec, prompt)
16
+ ├── string.rb # Generic String class extension for ANSI colors
17
+ └── utils.rb # Generic action routing and runner engine
18
+ ```
19
+
20
+ ---
21
+
22
+ ## How to Use inside a Project
23
+
24
+ ### 1. Define Your Custom Action Classes
25
+ Define classes representing categories of your CLI commands. These classes should inherit from your project's subclassed `Common` (which inherits from `CLIClassTool::Common`):
26
+
27
+ ```ruby
28
+ module MyProject
29
+ # Inherit from CLIClassTool::Common
30
+ class Common < CLIClassTool::Common
31
+ # Add any project-specific helpers here
32
+ end
33
+ end
34
+
35
+ module MyProject
36
+ class Suse < Common
37
+ # 1. Declare the list of available actions (methods)
38
+ ACTION_LIST = [ :source_rebase, :checkpatch ]
39
+
40
+ # 2. Define action method help/descriptions
41
+ ACTION_HELP = {
42
+ :source_rebase => "Rebase SUSE kernel sources to the latest tip",
43
+ :checkpatch => "Run checkpatch on pending patches"
44
+ }
45
+
46
+ # 3. Implement action methods
47
+ def source_rebase(opts)
48
+ log(:INFO, "Rebasing...")
49
+ run("git pull --rebase")
50
+ return 0 # Return 0 for success (or an Integer exit code)
51
+ end
52
+
53
+ def checkpatch(opts)
54
+ # Uses inherited `run` and logging methods
55
+ ret = run("git diff HEAD~1")
56
+ log(:VERBOSE, ret)
57
+ return 0
58
+ end
59
+ end
60
+ end
61
+ ```
62
+
63
+ ### 2. Initialize and Load CLIClassTool
64
+ In your library entrypoint (e.g. `lib/my-project.rb`), require the `cli_class_tool` components, set up your project-specific namespace, and extend `CLIClassTool::Utils`:
65
+
66
+ ```ruby
67
+ # Add lib to load path if necessary
68
+ $LOAD_PATH.push(File.dirname(__FILE__))
69
+
70
+ require 'cli_class_tool'
71
+
72
+ module MyProject
73
+ # Define project-specific base Common class
74
+ class Common < CLIClassTool::Common
75
+ # Project-specific methods can be defined here
76
+ end
77
+ end
78
+
79
+ # Require all your custom action classes
80
+ require 'my_project/suse'
81
+ require 'my_project/other_actions'
82
+
83
+ module MyProject
84
+ # List of classes that implement various CLI actions
85
+ ACTION_CLASS = [ Suse, OtherActions ]
86
+
87
+ # Extend CLIClassTool::Utils to map methods onto MyProject module
88
+ extend CLIClassTool::Utils
89
+ end
90
+ ```
91
+
92
+ ### 3. Create the Executable Runner
93
+ In your executable bin (e.g. `bin/mytool`), parse options and call the `execAction` utility to invoke the target action class:
94
+
95
+ ```ruby
96
+ #!/usr/bin/ruby
97
+ require 'optparse'
98
+ require 'my-project'
99
+
100
+ opts = { action: :source_rebase } # Typically parsed from command-line arguments
101
+
102
+ # 1. Optionally parse options
103
+ optsParser = OptionParser.new
104
+ MyProject.setOpts(opts[:action], optsParser, opts)
105
+ optsParser.parse!(ARGV)
106
+
107
+ # 2. Check options validity
108
+ MyProject.checkOpts(opts)
109
+
110
+ # 3. Execute action dynamically
111
+ # Uses the class `load` factory method if defined, falling back to `.new`.
112
+ # If an exception is thrown, it will be caught and logged cleanly, returning the correct exit status.
113
+ exit MyProject.execAction(opts, opts[:action])
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Logging Levels
119
+ By inheriting from `CLIClassTool::Common`, your action classes have access to a rich `log` helper supporting several standard output and color levels:
120
+
121
+ - `log(:DEBUG, "msg")`: Prints to STDOUT when `ENV["DEBUG"]` is active (Magenta).
122
+ - `log(:VERBOSE, "msg")`: Prints to STDOUT when `MyProject.verbose_log == true` (Blue).
123
+ - `log(:INFO, "msg")`: General informative logs (Green).
124
+ - `log(:PROGRESS, "msg")`: In-place update logs (Green with `\r` carriage return).
125
+ - `log(:WARNING, "msg")`: Warning logs (Brown).
126
+ - `log(:ERROR, "msg")`: Prints to STDERR (Red).
127
+
128
+ ---
129
+
130
+ ## Dynamic Class Overrides (Addons)
131
+
132
+ `CLIClassTool` natively supports dynamic class overrides (addons). This allows projects to load repository-specific or custom subclasses that extend or override base action behaviors without modifying the core codebase.
133
+
134
+ ### 1. Set Up `getExtendedClass`
135
+
136
+ To enable class overrides, define a `getExtendedClass` class/module method on your parent module:
137
+
138
+ ```ruby
139
+ module MyProject
140
+ # Map of repository names to overridden classes
141
+ @@custom_classes = {}
142
+
143
+ def self.registerCustom(repo_name, classes)
144
+ @@custom_classes[repo_name] = classes
145
+ end
146
+
147
+ # Resolve the customized/overridden subclass (addon) if registered
148
+ def self.getExtendedClass(default_class, repo_name = File.basename(Dir.pwd))
149
+ custom = @@custom_classes[repo_name]
150
+ if custom != nil && custom[default_class] != nil
151
+ return custom[default_class]
152
+ else
153
+ return default_class
154
+ end
155
+ end
156
+ end
157
+ ```
158
+
159
+ If defined, `CLIClassTool` will automatically:
160
+ - Execute actions using the extended class instead of the base class.
161
+ - For options setup (`setOpts`) and validation checks (`checkOpts`), it will sequentially call BOTH the base class hooks and the extended class hooks to ensure clean options merging.
162
+
163
+ ### 2. Dynamically Loading Addons
164
+
165
+ `CLIClassTool` provides a `loadAddons(path)` helper to dynamically scan a folder and load all custom Ruby classes/addons present in it.
166
+
167
+ ```ruby
168
+ module MyProject
169
+ extend CLIClassTool::Utils
170
+
171
+ # Load all core addons
172
+ loadAddons(File.expand_path('addons', __dir__))
173
+
174
+ # Load optional user addons from an environment variable path
175
+ if ENV["MY_PROJECT_ADDON_DIR"]
176
+ loadAddons(ENV["MY_PROJECT_ADDON_DIR"])
177
+ end
178
+ end
179
+ ```
180
+
181
+ ### 3. Factory Class Loading & Validation
182
+
183
+ `CLIClassTool` provides helper methods to implement safe, validated factory class loading. This ensures that subclasses are only instantiated through the authorized factory methods rather than being directly instantiated:
184
+
185
+ - `loadClass(default_class, addon_key, *args)`: Safely loads and instantiates an overridden/extended class instance.
186
+ - `checkDirectConstructor(class)`: Raises an error if the class is directly instantiated instead of going through `loadClass`.
187
+
188
+ #### Example Setup:
189
+
190
+ ```ruby
191
+ module MyProject
192
+ class Suse < Common
193
+ def initialize(path)
194
+ # Validate that constructor was only called through loadClass factory
195
+ MyProject.checkDirectConstructor(self.class)
196
+ @path = path
197
+ end
198
+
199
+ # Factory loading method
200
+ def self.load(path=".")
201
+ # Safely instantiate via CLIClassTool loadClass
202
+ return MyProject.loadClass(Suse, "suse-addon-key", path)
203
+ end
204
+ end
205
+ end
206
+ ```
207
+
208
+ ### 4. Fully Automated CLI Runner (`run_cli`)
209
+
210
+ `CLIClassTool` provides a highly configurable, automated CLI runner (`run_cli`) that eliminates repetitive parsing and formatting boilerplate from your executable binaries.
211
+
212
+ - `run_cli(opts={}, argv=ARGV) { |parser, phase, action_opts| ... }`
213
+
214
+ #### Built-in Default Options:
215
+ To simplify applications, `run_cli` natively defines and handles standard options by default:
216
+ - `--verbose`: Handled both globally and action-specifically, setting `MyProject.verbose_log = true`.
217
+ - `-y`, `--yes` and `-n`, `--no`: Handled action-specifically, setting `opts[:yn_default] = :yes` or `:no` respectively (which is natively recognized by the inherited `confirm` method).
218
+
219
+ #### Customization Phases:
220
+ - `:global`: Customize options parsed globally before the action is matched.
221
+ - `:action`: Customize options parsed specifically for the targeted action.
222
+
223
+ #### Addon Help Integration (`getCustomClasses`):
224
+ If your project uses dynamic class overrides (addons) and defines a `getCustomClasses` method on the parent module returning a hash/list of registered addon names, `run_cli` will automatically append a list of these custom repository addons to the `--help` output of the CLI for seamless help integration.
225
+
226
+ #### Example Executable (`bin/mytool`):
227
+
228
+ ```ruby
229
+ #!/usr/bin/ruby
230
+ require 'my-project'
231
+
232
+ opts = {
233
+ :default_setting => "value"
234
+ }
235
+
236
+ # The entire CLI parsing, formatting, listing, option checking, and action routing is fully automated!
237
+ # Built-in options like --verbose, -y/--yes, -n/--no are parsed and processed automatically.
238
+ MyProject.run_cli(opts) do |parser, phase, action_opts|
239
+ case phase
240
+ when :global
241
+ parser.on("-c", "--config FILE") { |val| MyProject::Config.path = val }
242
+ end
243
+ end
244
+ ```
245
+ ```
@@ -0,0 +1,170 @@
1
+ # Main module for generic CLI class-based tools and utilities
2
+ module CLIClassTool
3
+
4
+ # Common utility class providing logging, configuration, and shell execution methods
5
+ class Common
6
+ # List of available actions for this class
7
+ ACTION_LIST = [ :list_actions ]
8
+ # Help text for actions
9
+ ACTION_HELP = {}
10
+
11
+ private
12
+ # Get the parent module of this class (e.g. KernelWork or XXX)
13
+ def parent_module
14
+ @parent_module ||= begin
15
+ parts = self.class.name.split('::')
16
+ parts.size > 1 ? Object.const_get(parts[0...-1].join('::')) : Object
17
+ end
18
+ end
19
+
20
+ # Internal log method
21
+ # @param lvl [String] Log level string (colored)
22
+ # @param str [String] Message
23
+ # @param out [IO] Output stream (default $stdout)
24
+ def _log(lvl, str, out=$stdout)
25
+ out.puts("# " + lvl.to_s() + ": " + str)
26
+ end
27
+
28
+ # Internal relog method (update current line)
29
+ # @param lvl [String] Log level string (colored)
30
+ # @param str [String] Message
31
+ # @param out [IO] Output stream (default $stdout)
32
+ def _relog(lvl, str, out=$stdout)
33
+ out.print("# " + lvl.to_s() + ": " + str + "\r")
34
+ end
35
+
36
+ # Raise error if system command failed
37
+ # @param check_err [Boolean] Whether to check for errors
38
+ # @param sysret [Process::Status] System return status
39
+ # @param ret [String, nil] Optional return message
40
+ # @raise [StandardError] If command failed
41
+ def abort_if_err(check_err, sysret, ret = nil)
42
+ if sysret.exitstatus != 0 && check_err == true
43
+ run_error_class = parent_module.const_defined?(:RunError) ? parent_module::RunError : RuntimeError
44
+ raise(run_error_class.new(sysret.exitstatus, ret))
45
+ end
46
+ end
47
+
48
+ # Debug command execution
49
+ # @param cmd_type [String] Type of command (e.g., 'git')
50
+ # @param cmd [String] The command string
51
+ def cmd_debug(cmd_type, cmd)
52
+ log(:DEBUG, "Called from #{caller[1]}")
53
+ log(:DEBUG, "Running #{cmd_type} command '#{cmd}'")
54
+ end
55
+ protected
56
+ # Log a message with a specific level
57
+ #
58
+ # @param lvl [Symbol] Log level (:DEBUG, :INFO, :WARNING, :ERROR, etc.)
59
+ # @param str [String] Message to log
60
+ def log(lvl, str)
61
+ case lvl
62
+ when :DEBUG
63
+ _log("DEBUG".magenta(), str) if ENV["DEBUG"].to_s() != ""
64
+ when :DEBUG_CI
65
+ _log("DEBUG_CI".magenta(), str) if ENV["DEBUG_CI"].to_s() != ""
66
+ when :VERBOSE
67
+ _log("INFO".blue(), str) if parent_module.verbose_log == true
68
+ when :INFO
69
+ _log("INFO".green(), str)
70
+ when :PROGRESS
71
+ _relog("INFO".green(), str)
72
+ when :WARNING
73
+ _log("WARNING".brown(), str)
74
+ when :ERROR
75
+ _log("ERROR".red(), str, $stderr)
76
+ else
77
+ _log(lvl, str)
78
+ end
79
+ end
80
+
81
+ # Prompt the user for confirmation
82
+ #
83
+ # @param opts [Hash] Options hash
84
+ # @param msg [String] Confirmation message
85
+ # @param ignore_default [Boolean] Ignore default yes/no options
86
+ # @param allowed_reps [Array<String>] Allowed responses
87
+ # @return [String] User response
88
+ def confirm(opts, msg, ignore_default=false, allowed_reps=[ "y", "n" ])
89
+ rep = 't'
90
+ while allowed_reps.index(rep) == nil && rep != '' do
91
+ puts "Do you wish to #{msg} ? (#{allowed_reps.join("/")}): "
92
+ case (ignore_default == true ? nil : opts[:yn_default])
93
+ when :no
94
+ puts "Auto-replying no due to --no option"
95
+ rep = 'n'
96
+ when :yes
97
+ puts "Auto-replying yes due to --yes option"
98
+ rep = 'y'
99
+ else
100
+ rep = STDIN.gets.chomp()
101
+ end
102
+ end
103
+ return rep
104
+ end
105
+
106
+ public
107
+ # Run a shell command
108
+ #
109
+ # @param cmd [String] Command to run
110
+ # @param check_err [Boolean] Raise error on failure
111
+ # @return [String] Command output
112
+ # @raise [StandardError] If command fails and check_err is true
113
+ def run(cmd, check_err = true)
114
+ cmd_debug('', cmd)
115
+ ret = `cd #{@path} && #{cmd}`.chomp()
116
+ abort_if_err(check_err, $?, ret)
117
+ return ret
118
+ end
119
+
120
+ # Run a shell command using system() (interactive)
121
+ #
122
+ # @param cmd [String] Command to run
123
+ # @param check_err [Boolean] Raise error on failure
124
+ # @return [Boolean] Command success status
125
+ # @raise [StandardError] If command fails and check_err is true
126
+ def runSystem(cmd, check_err = true)
127
+ cmd_debug('interactive', cmd)
128
+ ret = system("cd #{@path} && #{cmd}")
129
+ abort_if_err(check_err, $?)
130
+ return ret
131
+ end
132
+
133
+ # Run a git command
134
+ #
135
+ # @param cmd [String] Git command arguments
136
+ # @param opts [Hash] Options (e.g., :env)
137
+ # @param check_err [Boolean] Raise error on failure
138
+ # @return [String] Command output
139
+ # @raise [StandardError] If command fails and check_err is true
140
+ def runGit(cmd, opts={}, check_err = true)
141
+ cmd_debug('git', cmd)
142
+ ret = `cd #{@path} && #{opts[:env]} git #{cmd}`.chomp()
143
+ abort_if_err(check_err, $?, ret)
144
+ return ret
145
+ end
146
+
147
+ # Run a git command interactively
148
+ #
149
+ # @param cmd [String] Git command arguments
150
+ # @param opts [Hash] Options (e.g., :env)
151
+ # @param check_err [Boolean] Raise error on failure
152
+ # @return [Boolean] Command success status
153
+ # @raise [StandardError] If command fails and check_err is true
154
+ def runGitInteractive(cmd, opts={}, check_err = true)
155
+ cmd_debug('git interactive', cmd)
156
+ ret = system("cd #{@path} && #{opts[:env]} git #{cmd}")
157
+ abort_if_err(check_err, $?)
158
+ return ret
159
+ end
160
+
161
+ # List available actions
162
+ #
163
+ # @param opts [Hash] Options hash
164
+ # @return [Integer] 0
165
+ def list_actions(opts)
166
+ puts parent_module.getActionAttr("ACTION_LIST").map(){|x| parent_module.actionToString(x)}.join("\n")
167
+ return 0
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,48 @@
1
+ # Extension to the core String class to add colorization support
2
+ class String
3
+ # colorization
4
+ @@is_a_tty = nil
5
+
6
+ # Colorize the string using ANSI escape codes
7
+ #
8
+ # @param color_code [Integer] ANSI color code
9
+ # @return [String] Colorized string if TTY, else original string
10
+ def colorize(color_code)
11
+ @@is_a_tty = $stdout.isatty() if @@is_a_tty == nil
12
+ if @@is_a_tty then
13
+ return "\e[#{color_code}m#{self}\e[0m"
14
+ else
15
+ return self
16
+ end
17
+ end
18
+
19
+ # Make the string red
20
+ # @return [String] Red string
21
+ def red
22
+ colorize(31)
23
+ end
24
+
25
+ # Make the string green
26
+ # @return [String] Green string
27
+ def green
28
+ colorize(32)
29
+ end
30
+
31
+ # Make the string brown (yellow)
32
+ # @return [String] Brown string
33
+ def brown
34
+ colorize(33)
35
+ end
36
+
37
+ # Make the string blue
38
+ # @return [String] Blue string
39
+ def blue
40
+ colorize(34)
41
+ end
42
+
43
+ # Make the string magenta
44
+ # @return [String] Magenta string
45
+ def magenta
46
+ colorize(35)
47
+ end
48
+ end