Module: CLIClassTool::Utils

Defined in:
lib/cli_class_tool/utils.rb

Overview

Generic utilities for CLI class-based actions

Class Method Summary collapse

Instance Method Summary collapse

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

Parameters:

  • action (Symbol)

    The action

  • sym (Symbol, nil)

    Optional method to check for existence

Yields:

  • (Class)

    The class handling the action

Returns:

  • (Object)

    Result of the block or error code



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

Parameters:

Returns:



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

Parameters:

  • sym (Symbol)

    Action symbol

Returns:



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

Parameters:

  • opts (Hash)

    The options hash



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_actionsHash<String, Module>

Dynamically discover and return a map of sub-CLI tools

Returns:

  • (Hash<String, Module>)

    Map of subcommand string to CLI modules



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

Parameters:

  • opts (Hash)

    The options hash

  • action (Symbol)

    The action to execute

  • error_class (Class, nil) (defaults to: nil)

    Optional base error class to rescue

Returns:

  • (Object)

    Result of the action (often an Integer exit code)



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.message}")
            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

Parameters:

  • attr (Symbol)

    Attribute name (e.g., "ACTION_LIST")

Returns:

  • (Hash, Array)

    Aggregated attributes



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|
            expand_val = sub_cli.const_defined?(:CLI_HELP_EXPAND) ? sub_cli::CLI_HELP_EXPAND : nil
            if expand_val
                if expand_val.is_a?(String)
                    sub_helps[expand_val] = ""
                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

Parameters:

  • path (String)

    Absolute or relative directory path containing .rb files



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.expand_path(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.

Parameters:

  • opts (Hash) (defaults to: {})

    Initial options hash

  • argv (Array<String>) (defaults to: ARGV)

    Command line arguments (defaults to ARGV)

Yields:

  • (parser, phase, action_opts)

    Custom options setup callback block



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.banner = "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.banner = "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

Parameters:

  • action (Symbol)

    The action

  • optsParser (OptionParser)

    The option parser

  • opts (Hash)

    The options hash



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

Parameters:

  • str (String)

    Action name

Returns:

  • (Symbol)

    Action symbol

Raises:

  • (RuntimeError)

    If action is invalid



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_logBoolean

Get verbose logging status

Returns:

  • (Boolean)

    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

Parameters:

  • val (Boolean)

    True to enable verbose logging



290
291
292
# File 'lib/cli_class_tool/utils.rb', line 290

def verbose_log=(val)
    @verbose_log = val
end