Module: CLIClassTool::Utils
- Defined in:
- lib/cli_class_tool/utils.rb
Overview
Generic utilities for CLI class-based actions
Class Method Summary collapse
-
.extended(base) ⇒ Object
Hook called when a module extends CLIClassTool::Utils.
Instance Method Summary collapse
-
#_runOnClass(action, sym) {|Class| ... } ⇒ Object
Run a block on the class responsible for a specific action.
-
#_to_snake_case(str) ⇒ String
Convert CamelCase to snake_case.
-
#actionToString(sym) ⇒ String
Convert an action symbol to a string.
-
#checkDirectConstructor(theClass) ⇒ Object
Validate that the constructor was only called through loadClass.
-
#checkOpts(opts) ⇒ Object
Check options for validity.
-
#cli_sub_actions ⇒ Hash<String, Module>
Dynamically discover and return a map of sub-CLI tools.
-
#execAction(opts, action, error_class = nil) ⇒ Object
Execute an action.
-
#getActionAttr(attr) ⇒ Hash, Array
Get attributes from all action classes.
-
#loadAddons(path) ⇒ Object
Load all custom addon classes/files from a directory.
-
#loadClass(default_class, addon_key, *more) ⇒ Object
Safely load an overridden/extended class instance using a generic addon_key.
-
#run_cli(opts = {}, argv = ARGV) {|parser, phase, action_opts| ... } ⇒ Object
Generic CLI runner and argument parser for class-based applications.
-
#setOpts(action, optsParser, opts) ⇒ Object
Set options for an action.
-
#stringToAction(str) ⇒ Symbol
Convert a string to an action symbol, validating it against available actions.
-
#verbose_log ⇒ Boolean
Get verbose logging status.
-
#verbose_log=(val) ⇒ Object
Set verbose logging.
Class Method Details
.extended(base) ⇒ Object
Hook called when a module extends CLIClassTool::Utils
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/cli_class_tool/utils.rb', line 8 def self.extended(base) return if base == nil lower_mod = base.name.split('::')[-1] superclass_name = "#{base.name}::#{lower_mod}Error" if ! Object.const_defined?(superclass_name) raise("Could not find a base error class named #{superclass_name}") end error_class = Object.const_get(superclass_name) # Override === on the error class to match nested subcommand errors recursively error_class.singleton_class.class_eval do define_method(:_cli_host_module) { base } def ===(other) return true if super if other.is_a?(StandardError) && other.respond_to?(:err_code) other_class_name = other.class.name if other_class_name parts = other_class_name.split('::') if parts.size > 1 parent_mod_name = parts[0...-1].join('::') host = _cli_host_module if host.respond_to?(:cli_sub_actions) all_sub_names = _all_sub_module_names(host) return all_sub_names.include?(parent_mod_name) end end end end false end define_method(:_all_sub_module_names) do |host_mod| names = [] if host_mod.respond_to?(:cli_sub_actions) host_mod.cli_sub_actions.values.each do |sub_cli| if sub_cli.name names << sub_cli.name names.concat(_all_sub_module_names(sub_cli)) end end end names.uniq end end CLIClassTool.define_run_error(base, error_class) end |
Instance Method Details
#_runOnClass(action, sym) {|Class| ... } ⇒ Object
Run a block on the class responsible for a specific action
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/cli_class_tool/utils.rb', line 202 def _runOnClass(action, sym, &block) return -1 unless self.const_defined?(:ACTION_CLASS) self::ACTION_CLASS.each(){|x| next if !x.const_defined?(:ACTION_LIST) || x::ACTION_LIST.index(action) == nil # Resolve overridden/extended class (addon) class_to_use = self.respond_to?(:getExtendedClass) ? self.getExtendedClass(x) : x if sym != nil has_base = x.singleton_methods().index(sym) != nil has_addon = class_to_use != x && class_to_use.singleton_methods().index(sym) != nil if has_base || has_addon yield(x) if has_base yield(class_to_use) if has_addon return 0 end else return yield(class_to_use) end return 0 } return -1 end |
#_to_snake_case(str) ⇒ String
Convert CamelCase to snake_case
65 66 67 68 69 70 |
# File 'lib/cli_class_tool/utils.rb', line 65 def _to_snake_case(str) str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') .gsub(/([a-z\d])([A-Z])/, '\1_\2') .tr('-', '_') .downcase end |
#actionToString(sym) ⇒ String
Convert an action symbol to a string
124 125 126 |
# File 'lib/cli_class_tool/utils.rb', line 124 def actionToString(sym) return sym.to_s() end |
#checkDirectConstructor(theClass) ⇒ Object
Validate that the constructor was only called through loadClass
328 329 330 331 332 333 334 335 336 337 |
# File 'lib/cli_class_tool/utils.rb', line 328 def checkDirectConstructor(theClass) @load_class ||= [] curLoad = @load_class.last() cl = theClass while cl != Object return if cl == curLoad cl = cl.superclass end raise("Use #{self.name}::loadClass to construct a #{theClass} class") end |
#checkOpts(opts) ⇒ Object
Check options for validity
241 242 243 244 245 |
# File 'lib/cli_class_tool/utils.rb', line 241 def checkOpts(opts) self._runOnClass(opts[:action], :check_opts) {|kClass| kClass.check_opts(opts) } end |
#cli_sub_actions ⇒ Hash<String, Module>
Dynamically discover and return a map of sub-CLI tools
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/cli_class_tool/utils.rb', line 75 def cli_sub_actions sub_actions = {} # First, check if manual CLI_SUB_ACTIONS mapping exists if self.const_defined?(:CLI_SUB_ACTIONS) manual_actions = self::CLI_SUB_ACTIONS if manual_actions.is_a?(Hash) manual_actions.each do |k, v| sub_actions[k.to_s] = v end end end # Then, dynamically discover any inner modules/classes extending CLIClassTool::Utils self.constants(false).each do |const_sym| begin const_val = self.const_get(const_sym) if const_val.is_a?(Module) && const_val.is_a?(CLIClassTool::Utils) cmd_name = if const_val.const_defined?(:CLI_COMMAND_NAME) const_val::CLI_COMMAND_NAME.to_s else _to_snake_case(const_sym.to_s) end # Only add if not already manually specified sub_actions[cmd_name] ||= const_val end rescue NameError, LoadError # ignore uninitialized autoloads end end return sub_actions end |
#execAction(opts, action, error_class = nil) ⇒ Object
Execute an action
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 |
# File 'lib/cli_class_tool/utils.rb', line 253 def execAction(opts, action, error_class = nil) caught_error_class = error_class || StandardError ret_code = self._runOnClass(action, nil) {|kClass| begin # Use load factory method if defined, else fall back to .new obj = kClass.respond_to?(:load) ? kClass.load() : kClass.new() ret = obj.public_send(action, opts) return ret.is_a?(Integer) ? ret : 0 rescue caught_error_class => e puts("# " + "ERROR".red().to_s() + ": Action '#{action}' failed: #{e.}") e.backtrace.each(){|l| puts("# " + "ERROR".red().to_s() + ": \t" + l) } if self.verbose_log begin return e.err_code rescue return 1 end end } if ret_code == -1 && action == :list_actions actions = self.getActionAttr("ACTION_LIST").map(){|x| self.actionToString(x)} actions.reject! { |x| x == "list_actions" } puts actions.join("\n") return 0 end return ret_code end |
#getActionAttr(attr) ⇒ Hash, Array
Get attributes from all action classes
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
# File 'lib/cli_class_tool/utils.rb', line 132 def getActionAttr(attr) common_class = self.const_defined?(:Common) ? self::Common : CLIClassTool::Common is_hash = if common_class.const_defined?(attr) common_class.const_get(attr).is_a?(Hash) else attr.to_s.include?("HELP") end action_classes = self.const_defined?(:ACTION_CLASS) ? self::ACTION_CLASS : [] # Resolve overridden/extended class (addon) if getExtendedClass is defined resolved_classes = action_classes.map do |x| self.respond_to?(:getExtendedClass) ? self.getExtendedClass(x) : x end res = if is_hash resolved_classes.inject({}) do |h, x| x.const_defined?(attr) ? h.merge(x.const_get(attr)) : h end else resolved_classes.map do |x| x.const_defined?(attr) ? x.const_get(attr) : [] end.flatten() end # If it's ACTION_LIST, append discovered subcommand names if attr.to_s == "ACTION_LIST" sub_actions = self.cli_sub_actions res += sub_actions.keys.map(&:to_sym) res << :list_actions unless res.include?(:list_actions) # If it's ACTION_HELP, merge subcommand helps elsif attr.to_s == "ACTION_HELP" sub_helps = {} self.cli_sub_actions.each do |cmd_name, sub_cli| = sub_cli.const_defined?(:CLI_HELP_EXPAND) ? sub_cli::CLI_HELP_EXPAND : nil if if .is_a?(String) sub_helps[] = "" end sub_helps_rec = sub_cli.getActionAttr("ACTION_HELP") sub_helps_rec.each do |k, desc| sub_helps["#{cmd_name} #{k}".to_sym] = desc end else desc = "" if sub_cli.const_defined?(:CLI_DESCRIPTION) desc = sub_cli::CLI_DESCRIPTION elsif sub_cli.const_defined?(:HELP) desc = sub_cli::HELP end sub_helps[cmd_name.to_sym] = desc end end if self.const_defined?(:CLI_SUB_ACTIONS_HELP) self::CLI_SUB_ACTIONS_HELP.each do |k, desc| sub_helps[k.to_sym] = desc end end res = res.merge(sub_helps) end return res end |
#loadAddons(path) ⇒ Object
Load all custom addon classes/files from a directory
304 305 306 307 308 309 310 311 312 313 |
# File 'lib/cli_class_tool/utils.rb', line 304 def loadAddons(path) return unless Dir.exist?(path) absolute_dir = File.(path) Dir.entries(absolute_dir).each() do |entry| absolute_file = File.join(absolute_dir, entry) next if !File.file?(absolute_file) || entry !~ /\.rb$/ require absolute_file end end |
#loadClass(default_class, addon_key, *more) ⇒ Object
Safely load an overridden/extended class instance using a generic addon_key
316 317 318 319 320 321 322 323 324 325 |
# File 'lib/cli_class_tool/utils.rb', line 316 def loadClass(default_class, addon_key, *more) @load_class ||= [] @load_class.push(default_class) # Resolve overridden class using getExtendedClass if available extended_class = self.respond_to?(:getExtendedClass) ? self.getExtendedClass(default_class, addon_key) : default_class obj = extended_class.new(*more) @load_class.pop() return obj end |
#run_cli(opts = {}, argv = ARGV) {|parser, phase, action_opts| ... } ⇒ Object
Generic CLI runner and argument parser for class-based applications.
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 |
# File 'lib/cli_class_tool/utils.rb', line 344 def run_cli(opts = {}, argv = ARGV, &block) # Fetch actions and action helps action_helps = self.getActionAttr("ACTION_HELP") # 1. Action Parser Setup action_parser = OptionParser.new(nil, 60) action_parser. = "Usage: #{$0} <action> [action options]" action_parser.separator "" action_parser.separator "Options:" action_parser.on("-h", "--help", "Display usage.") { puts action_parser.to_s; exit 0 } action_parser.on("--verbose", "Displays more informations.") { self.verbose_log = true } # Yield parser to allow caller to customize the global options yield(action_parser, :global, opts) if block_given? action_parser.separator "Possible actions:" # Format actions nicely with padding max_len = action_helps.keys.map { |k| self.actionToString(k).length }.max || 0 col_width = max_len + 4 action_helps.each do |k, x| indent = col_width - self.actionToString(k).length action_parser.separator "\t * " + self.actionToString(k) + (" " * indent) + x.to_s end # Include any registered custom addon class listings if defined if self.respond_to?(:getCustomClasses) && self.getCustomClasses.length > 0 action_parser.separator "Custom repo addons available:" self.getCustomClasses.each do |k, x| action_parser.separator "\t * #{k}" end end # Include command aliases if defined if self.const_defined?(:CLI_COMMAND_ALIASES) aliases = self::CLI_COMMAND_ALIASES if aliases.is_a?(Hash) && aliases.length > 0 action_parser.separator "" action_parser.separator "Command aliases:" aliases.each do |k, v| exp_str = v.is_a?(Array) ? v.join(" ") : v.to_s action_parser.separator "\t * #{k} -> #{exp_str}" end end end rest = action_parser.order!(argv) if rest.length <= 0 STDERR.puts("Error: No action provided") puts action_parser.to_s exit 1 end # Expand command aliases if self.const_defined?(:CLI_COMMAND_ALIASES) aliases = self::CLI_COMMAND_ALIASES if aliases.is_a?(Hash) action_s = argv[0] action_sym = action_s ? action_s.to_sym : nil if aliases.key?(action_s) || (action_sym && aliases.key?(action_sym)) expansion = aliases[action_s] || aliases[action_sym] expansion = expansion.split(' ') if expansion.is_a?(String) argv.shift() argv.unshift(*expansion) end end end action_s = argv[0] # Intercept subcommands here! sub_actions = self.cli_sub_actions if sub_actions.key?(action_s) sub_cli = sub_actions[action_s] if sub_cli.respond_to?(:verbose_log=) sub_cli.verbose_log = self.verbose_log end argv.shift() if block exit(sub_cli.run_cli(opts, argv, &block)) else exit(sub_cli.run_cli(opts, argv)) end end action = opts[:action] = self.stringToAction(action_s) argv.shift() # 2. Options Parser Setup opts_parser = OptionParser.new(nil, 60) opts_parser. = "Usage: #{$0} #{action_s} " opts_parser.separator "# " + action_helps[action].to_s() opts_parser.separator "" opts_parser.separator "Options:" opts_parser.on("-h", "--help", "Display usage.") { puts opts_parser.to_s; exit 0 } opts_parser.on("-n", "--no", "Assume no to all questions.") { opts[:yn_default] = :no } opts_parser.on("-y", "--yes", "Assume yes to all questions.") { opts[:yn_default] = :yes } opts_parser.on("--verbose", "Displays more informations.") { self.verbose_log = true } # Provide custom block hook for option parser customization yield(opts_parser, :action, opts) if block_given? # Set options on classes self.setOpts(action, opts_parser, opts) # Order remaining arguments rest = opts_parser.order!(argv) if opts[:ignore_opts] != true raise("Extra Unexpected extra arguments provided: " + rest.map(){|x|"'" + x + "'"}.join(", ")) if rest.length != 0 else opts[:extra_args] = rest end # Validate options and execute action self.checkOpts(opts) exit self.execAction(opts, action) end |
#setOpts(action, optsParser, opts) ⇒ Object
Set options for an action
232 233 234 235 236 |
# File 'lib/cli_class_tool/utils.rb', line 232 def setOpts(action, optsParser, opts) self._runOnClass(action, :set_opts) {|kClass| kClass.set_opts(action, optsParser, opts) } end |
#stringToAction(str) ⇒ Symbol
Convert a string to an action symbol, validating it against available actions
114 115 116 117 118 |
# File 'lib/cli_class_tool/utils.rb', line 114 def stringToAction(str) action = str.to_sym() raise("Invalid action '#{str}'") if self.getActionAttr("ACTION_LIST").index(action) == nil return action end |
#verbose_log ⇒ Boolean
Get verbose logging status
297 298 299 |
# File 'lib/cli_class_tool/utils.rb', line 297 def verbose_log() @verbose_log end |
#verbose_log=(val) ⇒ Object
Set verbose logging
290 291 292 |
# File 'lib/cli_class_tool/utils.rb', line 290 def verbose_log=(val) @verbose_log = val end |