cuprum-cli 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.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +34 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE +21 -0
  5. data/README.md +163 -0
  6. data/lib/cuprum/cli/argument.rb +172 -0
  7. data/lib/cuprum/cli/arguments/class_methods.rb +283 -0
  8. data/lib/cuprum/cli/arguments.rb +16 -0
  9. data/lib/cuprum/cli/coercion.rb +131 -0
  10. data/lib/cuprum/cli/command.rb +102 -0
  11. data/lib/cuprum/cli/commands/ci/report.rb +121 -0
  12. data/lib/cuprum/cli/commands/ci/rspec_command.rb +108 -0
  13. data/lib/cuprum/cli/commands/ci/rspec_each_command.rb +185 -0
  14. data/lib/cuprum/cli/commands/ci.rb +12 -0
  15. data/lib/cuprum/cli/commands/echo_command.rb +76 -0
  16. data/lib/cuprum/cli/commands/file/generate_file.rb +141 -0
  17. data/lib/cuprum/cli/commands/file/new_command.rb +86 -0
  18. data/lib/cuprum/cli/commands/file/render_erb.rb +88 -0
  19. data/lib/cuprum/cli/commands/file/resolve_template.rb +136 -0
  20. data/lib/cuprum/cli/commands/file/templates/rspec.rb.erb +14 -0
  21. data/lib/cuprum/cli/commands/file/templates/ruby.rb.erb +29 -0
  22. data/lib/cuprum/cli/commands/file/templates.rb +71 -0
  23. data/lib/cuprum/cli/commands/file.rb +14 -0
  24. data/lib/cuprum/cli/commands.rb +12 -0
  25. data/lib/cuprum/cli/dependencies/file_system/mock.rb +297 -0
  26. data/lib/cuprum/cli/dependencies/file_system.rb +247 -0
  27. data/lib/cuprum/cli/dependencies/standard_io/helpers.rb +138 -0
  28. data/lib/cuprum/cli/dependencies/standard_io/mock.rb +85 -0
  29. data/lib/cuprum/cli/dependencies/standard_io.rb +110 -0
  30. data/lib/cuprum/cli/dependencies/system_command/mock.rb +57 -0
  31. data/lib/cuprum/cli/dependencies/system_command.rb +147 -0
  32. data/lib/cuprum/cli/dependencies.rb +25 -0
  33. data/lib/cuprum/cli/errors/files/file_not_writeable.rb +42 -0
  34. data/lib/cuprum/cli/errors/files/missing_parameter.rb +71 -0
  35. data/lib/cuprum/cli/errors/files/missing_template.rb +36 -0
  36. data/lib/cuprum/cli/errors/files/template_error.rb +37 -0
  37. data/lib/cuprum/cli/errors/files/template_not_resolved.rb +54 -0
  38. data/lib/cuprum/cli/errors/files.rb +19 -0
  39. data/lib/cuprum/cli/errors/system_command_failure.rb +44 -0
  40. data/lib/cuprum/cli/errors.rb +11 -0
  41. data/lib/cuprum/cli/integrations/thor/arguments_parser.rb +99 -0
  42. data/lib/cuprum/cli/integrations/thor/registry.rb +42 -0
  43. data/lib/cuprum/cli/integrations/thor/task.rb +211 -0
  44. data/lib/cuprum/cli/integrations/thor.rb +14 -0
  45. data/lib/cuprum/cli/integrations.rb +8 -0
  46. data/lib/cuprum/cli/metadata.rb +215 -0
  47. data/lib/cuprum/cli/option.rb +165 -0
  48. data/lib/cuprum/cli/options/class_methods.rb +232 -0
  49. data/lib/cuprum/cli/options/quiet.rb +32 -0
  50. data/lib/cuprum/cli/options/verbose.rb +32 -0
  51. data/lib/cuprum/cli/options.rb +18 -0
  52. data/lib/cuprum/cli/registry.rb +141 -0
  53. data/lib/cuprum/cli/rspec/deferred/arguments_examples.rb +203 -0
  54. data/lib/cuprum/cli/rspec/deferred/ci/report_examples.rb +450 -0
  55. data/lib/cuprum/cli/rspec/deferred/ci.rb +8 -0
  56. data/lib/cuprum/cli/rspec/deferred/dependencies/file_system_examples.rb +1469 -0
  57. data/lib/cuprum/cli/rspec/deferred/dependencies.rb +8 -0
  58. data/lib/cuprum/cli/rspec/deferred/metadata_examples.rb +856 -0
  59. data/lib/cuprum/cli/rspec/deferred/options_examples.rb +234 -0
  60. data/lib/cuprum/cli/rspec/deferred/registry_examples.rb +451 -0
  61. data/lib/cuprum/cli/rspec/deferred.rb +8 -0
  62. data/lib/cuprum/cli/rspec.rb +8 -0
  63. data/lib/cuprum/cli/version.rb +59 -0
  64. data/lib/cuprum/cli.rb +47 -0
  65. metadata +173 -0
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/error'
4
+
5
+ require 'cuprum/cli/errors'
6
+
7
+ module Cuprum::Cli::Errors
8
+ # Error returned when a system command returns a non-success status.
9
+ class SystemCommandFailure < Cuprum::Error
10
+ # Short string used to identify the type of error.
11
+ TYPE = 'cuprum.cli.errors.system_command_failure'
12
+
13
+ # @param command [String] the failed command.
14
+ # @param details [String] the error output from the process, if any.
15
+ # @param exit_status [Integer] the exit code returned by the process.
16
+ def initialize(command:, details: nil, exit_status: nil)
17
+ @command = command
18
+ @details = details
19
+ @exit_status = exit_status
20
+
21
+ super(message: default_message)
22
+ end
23
+
24
+ attr_reader :command
25
+
26
+ attr_reader :details
27
+
28
+ attr_reader :exit_status
29
+
30
+ private
31
+
32
+ def as_json_data
33
+ {
34
+ 'command' => command,
35
+ 'details' => details,
36
+ 'exit_status' => exit_status
37
+ }
38
+ end
39
+
40
+ def default_message
41
+ %(system command failed with exit status #{exit_status} - "#{command}")
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/cli'
4
+
5
+ module Cuprum::Cli
6
+ # Namespace for errors, which represent failure states of commands.
7
+ module Errors
8
+ autoload :Files, 'cuprum/cli/errors/files'
9
+ autoload :SystemCommandFailure, 'cuprum/cli/errors/system_command_failure'
10
+ end
11
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/cli/integrations/thor'
4
+
5
+ module Cuprum::Cli::Integrations::Thor
6
+ # Utility for parsing command-line arguments captured by Thor tasks.
7
+ #
8
+ # Any unrecognized command line flags or options are appended as-is to the
9
+ # arguments array by Thor. Therefore, to handle cases such as variadic options
10
+ # where flags or options cannot be pre-parsed by Thor, we need an additional
11
+ # parsing step to pull any remaining flags or options out of the arguments.
12
+ #
13
+ # This parser supports the following formats:
14
+ #
15
+ # - `-a`, `--all`: Sets the `a` or `all` flag to `true`.
16
+ # - `-abc`: Sets the `a`, `b`, and `c` flags to `true`.
17
+ # - `--skip-all`, `--no-all`: Sets the `a` flag to `false`.
18
+ # - `-a=value`, `--a=value`: Sets the `a` option to `"value"`.
19
+ #
20
+ # The following formats are specifically *not* supported:
21
+ #
22
+ # - `--foo bar`: `--foo` is assumed to be a flag, `bar` is assumed to be a
23
+ # positional argument.
24
+ # - `--str[]=foo --str[]=bar`: Array arguments are not supported.
25
+ # - `--str[foo]=foo --str[bar]=bar`: Hash arguments are not supported.
26
+ #
27
+ # In addition, parsed option values are coerced into their most likely
28
+ # intended types.
29
+ class ArgumentsParser
30
+ # Parses the given argument inputs into arguments and options.
31
+ #
32
+ # @param inputs [Array<String>] the arguments captured by Thor.
33
+ #
34
+ # @return [Array<Array<String>, Hash{Symbol=>Object}>] the parsed arguments
35
+ # and options.
36
+ def call(*inputs)
37
+ raw_options, arguments = inputs.partition { |str| str.start_with?('-') }
38
+
39
+ [arguments, parse_options(raw_options)]
40
+ end
41
+
42
+ private
43
+
44
+ def coerce_value(raw_value)
45
+ Cuprum::Cli::Coercion.coerce(raw_value)
46
+ end
47
+
48
+ def grouped_flags?(raw_key)
49
+ return false if raw_key.start_with?('--')
50
+
51
+ raw_key.length > 2
52
+ end
53
+
54
+ def normalize_flag_key(raw_key)
55
+ return raw_key[5..] if raw_key.start_with?('--no-')
56
+ return raw_key[7..] if raw_key.start_with?('--skip-')
57
+ return raw_key[2..] if raw_key.start_with?('--')
58
+
59
+ raw_key[1..]
60
+ end
61
+
62
+ def normalize_option_key(raw_key)
63
+ return raw_key[2..] if raw_key.start_with?('--')
64
+
65
+ raw_key[1..]
66
+ end
67
+
68
+ def parse_flag_value(raw_key) # rubocop:disable Naming/PredicateMethod
69
+ return false if raw_key.start_with?('--no-')
70
+ return false if raw_key.start_with?('--skip-')
71
+
72
+ true
73
+ end
74
+
75
+ def parse_option(raw_key, raw_value) # rubocop:disable Metrics/MethodLength
76
+ if raw_value.nil? && grouped_flags?(raw_key)
77
+ raw_key[1..].chars.to_h { |char| [char.to_sym, true] }
78
+ elsif raw_value.nil?
79
+ key = normalize_flag_key(raw_key)
80
+ value = parse_flag_value(raw_key)
81
+
82
+ { key.to_sym => value }
83
+ else
84
+ key = normalize_option_key(raw_key)
85
+ value = coerce_value(raw_value)
86
+
87
+ { key.to_sym => value }
88
+ end
89
+ end
90
+
91
+ def parse_options(raw_options)
92
+ raw_options.reduce({}) do |options, input|
93
+ raw_key, raw_value = input.split('=')
94
+
95
+ options.merge(parse_option(raw_key, raw_value))
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/cli/integrations/thor'
4
+ require 'cuprum/cli/integrations/thor/task'
5
+ require 'cuprum/cli/registry'
6
+
7
+ module Cuprum::Cli::Integrations::Thor
8
+ # Registers CLI commands by name and adds as Thor tasks.
9
+ class Registry < Cuprum::Cli::Registry
10
+ # Registers the command with the registry.
11
+ #
12
+ # Also registers a Thor task with compatible parameters and metadata.
13
+ #
14
+ # @param command [Class] the command class to register.
15
+ # @param config [Hash] options for configuring the command.
16
+ #
17
+ # @option config arguments [Array] arguments to pass to the command on
18
+ # initialization.
19
+ # @option config description [String] the description for the command.
20
+ # @option config full_description [String] the full description for the
21
+ # command.
22
+ # @option config full_name [String] the name under which to register the
23
+ # command. Defaults to the value of command.full_name.
24
+ # @option config options [Hash] options to pass to the command on
25
+ # initialization.
26
+ #
27
+ # @raise [NameError] if a command is already registered with that name.
28
+ #
29
+ # @return [self]
30
+ def register(command, **config)
31
+ super.tap do
32
+ name = config.fetch(:full_name, command.full_name)
33
+ command = commands[name]
34
+
35
+ Cuprum::Cli::Integrations::Thor::Task::Builder
36
+ .new(command)
37
+ .build(full_name: name)
38
+ end
39
+ end
40
+ alias add register
41
+ end
42
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ require 'sleeping_king_studios/tools/toolbelt'
6
+ require 'sleeping_king_studios/tools/toolbox/subclass'
7
+ require 'thor'
8
+
9
+ require 'cuprum/cli/integrations/thor'
10
+ require 'cuprum/cli/integrations/thor/arguments_parser'
11
+
12
+ module Cuprum::Cli::Integrations::Thor
13
+ # Thor task wrapping a Cuprum::Cli command.
14
+ class Task < ::Thor
15
+ extend SleepingKingStudios::Tools::Toolbox::Subclass
16
+
17
+ # Generates a Thor::Task wrapping a Cuprum::Cli command class.
18
+ class Builder
19
+ extend Forwardable
20
+
21
+ NUMERIC_TYPES = Set.new(%w[big_decimal integer float]).freeze
22
+ private_constant :NUMERIC_TYPES
23
+
24
+ # @param command_class [Class] the command to execute.
25
+ def initialize(command_class)
26
+ validate_command_class(command_class)
27
+
28
+ @command_class = command_class
29
+ @full_name = nil
30
+ end
31
+
32
+ # @return [Class] the command to execute.
33
+ attr_reader :command_class
34
+
35
+ def_delegators :@command_class,
36
+ :arguments,
37
+ :description,
38
+ :full_description,
39
+ :full_description?
40
+
41
+ # Generates a Thor::Task wrapping the command class.
42
+ #
43
+ # The generated task will be assigned Thor metadata automatically, based
44
+ # on the configuration of the command class.
45
+ #
46
+ # @return [Class] the generated Task class.
47
+ def build(full_name: nil)
48
+ @full_name = full_name || command_class.full_name
49
+
50
+ tools.assertions.validate_name(@full_name, as: 'full_name')
51
+
52
+ Cuprum::Cli::Integrations::Thor::Task
53
+ .subclass(command_class)
54
+ .tap do |task|
55
+ apply_metadata(task)
56
+
57
+ task.alias_method short_name, :call_command
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ attr_reader :full_name
64
+
65
+ def apply_options(task)
66
+ command_class.options.each_value do |option|
67
+ params = {
68
+ aliases: option.aliases,
69
+ banner: option.parameter_name || option.name.to_s.upcase,
70
+ desc: option.description,
71
+ required: option.required?,
72
+ type: parameter_type(option)
73
+ }
74
+
75
+ task.option(option.name, **params)
76
+ end
77
+ end
78
+
79
+ def apply_metadata(task)
80
+ task.namespace(namespace)
81
+ task.desc(signature, description)
82
+
83
+ task.long_desc(full_description) if full_description?
84
+
85
+ apply_options(task)
86
+
87
+ task
88
+ end
89
+
90
+ def argument_signature(argument)
91
+ signature = argument.parameter_name || argument.name.to_s.upcase
92
+
93
+ argument.variadic ? " ...#{signature}" : " #{signature}"
94
+ end
95
+
96
+ def arguments_signature
97
+ arguments.map { |argument| argument_signature(argument) }.join
98
+ end
99
+
100
+ def namespace
101
+ return 'default' if full_name.nil? || !full_name.include?(':')
102
+
103
+ full_name&.sub(/:[\w_]+\z/, '')
104
+ end
105
+
106
+ def parameter_type(parameter)
107
+ type = parameter.type
108
+ type = type.name if type.is_a?(Class)
109
+ type = tools.string_tools.underscore(type)
110
+
111
+ return :numeric if NUMERIC_TYPES.include?(type)
112
+
113
+ type.to_sym
114
+ end
115
+
116
+ def short_name
117
+ full_name&.split(':')&.last
118
+ end
119
+
120
+ def signature
121
+ "#{short_name}#{arguments_signature}"
122
+ end
123
+
124
+ def tools
125
+ SleepingKingStudios::Tools::Toolbelt.instance
126
+ end
127
+
128
+ def validate_command_class(command_class, as: 'command_class')
129
+ tools.assertions.validate_class(command_class, as:)
130
+ tools.assertions.validate_inherits_from(
131
+ command_class,
132
+ as:,
133
+ expected: Cuprum::Cli::Command
134
+ )
135
+
136
+ validate_command_description(command_class, as:)
137
+ end
138
+
139
+ def validate_command_description(command_class, as:)
140
+ description = command_class.description
141
+
142
+ return unless description.nil? || description.empty?
143
+
144
+ raise ArgumentError, "#{as} does not have a description"
145
+ end
146
+ end
147
+
148
+ # Ensures that the task exists with a non-zero status code on a failure.
149
+ #
150
+ # @return [true]
151
+ def self.exit_on_failure? = true
152
+
153
+ # @overload initialize(command_class, arguments = [], options = {}, config = {})
154
+ # @param command_class [Class] the command to execute.
155
+ # @param arguments [Array] the arguments passed by the Thor runtime.
156
+ # @param options [Hash] the options passed by the Thor runtime.
157
+ # @param config [Hash] additional configuration passed by the Thor
158
+ # runtime.
159
+ def initialize(
160
+ command_class,
161
+ arguments = [],
162
+ options = {},
163
+ config = {},
164
+ command_dependencies: {}
165
+ )
166
+ super(arguments, options, config)
167
+
168
+ @command_class = command_class
169
+ @command_dependencies = command_dependencies
170
+ end
171
+
172
+ # @return [Class] the command to execute.
173
+ attr_reader :command_class
174
+
175
+ no_commands do
176
+ # Calls the wrapped Cuprum::Cli command with the parsed parameters.
177
+ def call_command(*args)
178
+ args, opts =
179
+ Cuprum::Cli::Integrations::Thor::ArgumentsParser.new.call(*args)
180
+ opts = opts.merge(options)
181
+ opts = tools.hash_tools.convert_keys_to_symbols(opts)
182
+ result =
183
+ command_class.new(**command_dependencies).call(*args, **opts)
184
+
185
+ handle_failure(result) if result.failure?
186
+ rescue Cuprum::Cli::Options::UnknownOptionError,
187
+ Cuprum::Cli::Arguments::ExtraArgumentsError => exception
188
+ abort(exception.message)
189
+ end
190
+ end
191
+
192
+ private
193
+
194
+ attr_reader :command_dependencies
195
+
196
+ def abort(*) = Kernel.abort(*)
197
+
198
+ def handle_failure(result)
199
+ message =
200
+ result
201
+ .error
202
+ &.then { |err| "#{err.class.name}: #{err.message}" } || false
203
+
204
+ abort(message)
205
+ end
206
+
207
+ def tools
208
+ SleepingKingStudios::Tools::Toolbelt.instance
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/cli/integrations'
4
+
5
+ module Cuprum::Cli::Integrations
6
+ # Integration with the Thor CLI toolkit.
7
+ #
8
+ # @see http://whatisthor.com/
9
+ module Thor
10
+ autoload :ArgumentsParser, 'cuprum/cli/integrations/thor/arguments_parser'
11
+ autoload :Registry, 'cuprum/cli/integrations/thor/registry'
12
+ autoload :Task, 'cuprum/cli/integrations/thor/task'
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cuprum/cli'
4
+
5
+ module Cuprum::Cli
6
+ # Namespace for integrations with other command-line tools.
7
+ module Integrations; end
8
+ end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sleeping_king_studios/tools/toolbox/mixin'
4
+
5
+ require 'cuprum/cli'
6
+
7
+ module Cuprum::Cli
8
+ # Class methods for describing commands.
9
+ module Metadata
10
+ extend SleepingKingStudios::Tools::Toolbox::Mixin
11
+
12
+ # Format used to validate command names.
13
+ FULL_NAME_FORMAT = /\A[a-z_]+(:[a-z_]+)*\z/
14
+
15
+ UNDEFINED = SleepingKingStudios::Tools::UNDEFINED
16
+ private_constant :UNDEFINED
17
+
18
+ # Raised when performing a protected operation on an abstract Command.
19
+ class AbstractCommandError < StandardError; end
20
+
21
+ # Class methods to extend when including Metadata.
22
+ module ClassMethods
23
+ # Marks the command as abstract.
24
+ def abstract = @abstract = true
25
+
26
+ # @return [true, false] true if the command is abstract and should not be
27
+ # instantiated directly or assigned metadata; otherwise false.
28
+ def abstract? = @abstract.nil? ? false : @abstract
29
+
30
+ # @overload description
31
+ # @return [String] the description for the command.
32
+ #
33
+ # @overload description(value)
34
+ # Sets the description for the command.
35
+ #
36
+ # @param value [String] the description to set.
37
+ #
38
+ # @return [String] the set description.
39
+ def description(value = UNDEFINED)
40
+ return defined_description if value == UNDEFINED
41
+
42
+ if abstract?
43
+ raise AbstractCommandError,
44
+ abstract_command_message('set description')
45
+ end
46
+
47
+ tools.assertions.validate_name(value, as: 'description')
48
+
49
+ @description = value
50
+ end
51
+
52
+ # @return [true, false] true if the command defines a description;
53
+ # otherwise false.
54
+ def description? = !defined_description.nil?
55
+
56
+ # @overload full_description
57
+ # @return [String] the full description for the command.
58
+ #
59
+ # @overload full_description(value)
60
+ # Sets the full description for the command.
61
+ #
62
+ # @param value [String] the full description to set.
63
+ #
64
+ # @return [String] the set full description.
65
+ def full_description(value = UNDEFINED)
66
+ if value == UNDEFINED
67
+ return defined_full_description || defined_description
68
+ end
69
+
70
+ if abstract?
71
+ raise AbstractCommandError,
72
+ abstract_command_message('set full_description')
73
+ end
74
+
75
+ tools.assertions.validate_name(value, as: 'full_description')
76
+
77
+ @full_description = value
78
+ end
79
+
80
+ # @return [true, false] true if the command defines a full description;
81
+ # otherwise false.
82
+ def full_description? = !defined_full_description.nil?
83
+
84
+ # @overload full_name
85
+ # Returns the name of the command, used when calling from a CLI.
86
+ #
87
+ # Unless another value is set, defaults to the class name of the command
88
+ # with the following format:
89
+ #
90
+ # - Removes the "Commands" namespace and any prior namespace, if any.
91
+ # - Removes a "Command" suffix, if any.
92
+ # - Converts each remaining segment to snake_case and joins with ":".
93
+ #
94
+ # @return [String] the scoped name for the command.
95
+ #
96
+ # @overload full_name(value)
97
+ # Sets the full name for the command.
98
+ #
99
+ # The full name must be in snake_case format joined by ":".
100
+ #
101
+ # @param value [String] the full name to set.
102
+ #
103
+ # @return [String] the set full name.
104
+ def full_name(value = UNDEFINED) # rubocop:disable Metrics/MethodLength
105
+ return defined_full_name if value == UNDEFINED
106
+
107
+ if abstract?
108
+ raise AbstractCommandError, abstract_command_message('set full_name')
109
+ end
110
+
111
+ tools.assertions.validate_name(value, as: 'full_name')
112
+ tools.assertions.validate_matches(
113
+ value,
114
+ as: 'full_name',
115
+ expected: FULL_NAME_FORMAT,
116
+ message: invalid_full_name_format_message
117
+ )
118
+
119
+ @full_name = value
120
+ end
121
+
122
+ # The namespace for the command.
123
+ #
124
+ # A command's namespace is defined as the part of the full name prior to
125
+ # the last segment.
126
+ #
127
+ # - For a command with full name "custom", the namespace will be nil.
128
+ # - For a command with full name "category:sub_category:do_something", the
129
+ # namespace will be "category:sub_category".
130
+ #
131
+ # @return [String, nil] the namespace for the command, or nil if the
132
+ # command's full name is unscoped.
133
+ #
134
+ # @see #short_name.
135
+ def namespace
136
+ return if full_name.nil? || !full_name.include?(':')
137
+
138
+ full_name&.sub(/:[\w_]+\z/, '')
139
+ end
140
+
141
+ # @return [true, false] true if the command defines a namespace; otherwise
142
+ # false.
143
+ def namespace? = !namespace.nil?
144
+
145
+ # The short name for the command.
146
+ #
147
+ # A command's short_name is the last segment of the full name.
148
+ #
149
+ # - For a command with full name "custom", the short_name will be
150
+ # "custom".
151
+ # - For a command with full name "category:sub_category:do_something", the
152
+ # short_name will be "do_something".
153
+ #
154
+ # @return [String] the short name for the command.
155
+ def short_name = full_name&.split(':')&.last
156
+
157
+ protected
158
+
159
+ def abstract_command_message(short_message)
160
+ class_name =
161
+ ancestors
162
+ .find { |ancestor| ancestor.is_a?(Class) && ancestor.name }
163
+ .name
164
+
165
+ "unable to #{short_message} - #{class_name} is an abstract class"
166
+ end
167
+
168
+ def defined_description
169
+ return @description if @description
170
+
171
+ return unless superclass.respond_to?(:defined_description, true)
172
+
173
+ superclass.defined_description
174
+ end
175
+
176
+ def defined_full_description
177
+ return @full_description if @full_description
178
+
179
+ return unless superclass.respond_to?(:defined_full_description, true)
180
+
181
+ superclass.defined_full_description
182
+ end
183
+
184
+ def defined_full_name
185
+ return @full_name if @full_name ||= default_name
186
+
187
+ return unless superclass.respond_to?(:defined_full_name, true)
188
+
189
+ return if superclass.name == 'Cuprum::Cli::Command'
190
+
191
+ superclass.defined_full_name
192
+ end
193
+
194
+ private
195
+
196
+ def default_name
197
+ return if name.nil?
198
+
199
+ name
200
+ .split('Commands::')
201
+ .last
202
+ .sub(/Command\z/, '')
203
+ .split('::')
204
+ .map { |str| tools.string_tools.underscore(str) }
205
+ .join(':')
206
+ end
207
+
208
+ def invalid_full_name_format_message
209
+ 'full_name does not match format category:sub_category:do_something'
210
+ end
211
+
212
+ def tools = SleepingKingStudios::Tools::Toolbelt.instance
213
+ end
214
+ end
215
+ end