class CLAide::Command
This class is used to build a command-line interface
Each command is represented by a subclass of this class, which may be nested to create more granular commands.
Following is an overview of the types of commands and what they should do.
### Any command type
-
Inherit from the command class under which the command should be nested.
-
Set {Command.summary} to a brief description of the command.
-
Override {Command.options} to return the options it handles and their descriptions and prepending them to the results of calling `super`.
-
Override {Command#initialize} if it handles any parameters.
-
Override {Command#validate!} to check if the required parameters the command handles are valid, or call {Command#help!} in case they’re not.
### Abstract command
The following is needed for an abstract command:
-
Set {Command.abstract_command} to `true`.
-
Subclass the command.
When the optional {Command.description} is specified, it will be shown at the top of the command’s help banner.
### Normal command
The following is needed for a normal command:
-
Set {Command.arguments} to the description of the arguments this command handles.
-
Override {Command#run} to perform the actual work.
When the optional {Command.description} is specified, it will be shown underneath the usage section of the command’s help banner. Otherwise this defaults to {Command.summary}.
Constants
- DEFAULT_OPTIONS
- DEFAULT_ROOT_OPTIONS
Attributes
@return [Boolean] Indicates whether or not this command can actually
perform work of itself, or that it only contains subcommands.
@return [Boolean] Indicates whether or not this command can actually
perform work of itself, or that it only contains subcommands.
@return [String] The subcommand which an abstract command should invoke
by default.
@return [String] A longer description of the command, which is shown
underneath the usage section of the command’s help banner. Any indentation in this value will be ignored.
@return [Boolean] Indicates whether or not this command is used during
command parsing and whether or not it should be shown in the help banner or to show its subcommands instead. Setting this to `true` implies it’s an abstract command.
@return [Boolean] Indicates whether or not this command is used during
command parsing and whether or not it should be shown in the help banner or to show its subcommands instead. Setting this to `true` implies it’s an abstract command.
@return [String] A brief description of the command, which is shown
next to the command in the help banner of a parent command.
@return [String] The version of the command. This value will be printed
by the `--version` flag if used for the root command.
Set to `true` if {Command.ansi_output} returns `true` and the user did not specify the `–no-ansi` option.
@note (see verbose
)
@return [Boolean]
Whether or not to use ANSI codes to prettify output. For instance, by default {InformativeError} exception messages will be colored red and subcommands in help banners green.
Set to `true` if {Command.ansi_output} returns `true` and the user did not specify the `–no-ansi` option.
@note (see verbose
)
@return [Boolean]
Whether or not to use ANSI codes to prettify output. For instance, by default {InformativeError} exception messages will be colored red and subcommands in help banners green.
Set to `true` if initialized with a `–help` flag
@return [Boolean]
Whether the command was initialized with argv containing --help
Set to `true` if initialized with a `–help` flag
@return [Boolean]
Whether the command was initialized with argv containing --help
@return [Bool] Whether the command was invoked by an abstract command by
default.
@return [Bool] Whether the command was invoked by an abstract command by
default.
Set to `true` if the user specifies the `–verbose` option.
@note
If you want to make use of this value for your own configuration, you should check the value _after_ calling the `super` {Command#initialize} implementation.
@return [Boolean]
Wether or not backtraces should be included when presenting the user an exception that includes the {InformativeError} module.
Set to `true` if the user specifies the `–verbose` option.
@note
If you want to make use of this value for your own configuration, you should check the value _after_ calling the `super` {Command#initialize} implementation.
@return [Boolean]
Wether or not backtraces should be included when presenting the user an exception that includes the {InformativeError} module.
Public Class Methods
@return [Boolean] The default value for {Command#ansi_output}. This
defaults to `true` if `STDOUT` is connected to a TTY and `String` has the instance methods `#red`, `#green`, and `#yellow` (which are defined by, for instance, the [colored](https://github.com/defunkt/colored) gem).
# File lib/claide/command.rb, line 127 def ansi_output if @ansi_output.nil? @ansi_output = STDOUT.tty? end @ansi_output end
@return [Array<Argument>]
A list of arguments the command handles. This is shown in the usage section of the command’s help banner. Each Argument in the array represents an argument by its name (or list of alternatives) and whether it's required or optional
# File lib/claide/command.rb, line 98 def arguments @arguments ||= [] end
@param [Array<Argument>] arguments
An array listing the command arguments. Each Argument object describe the argument by its name (or list of alternatives) and whether it's required or optional
@todo Remove deprecation
# File lib/claide/command.rb, line 109 def arguments=(arguments) if arguments.is_a?(Array) if arguments.empty? || arguments[0].is_a?(Argument) @arguments = arguments else self.arguments_array = arguments end else self.arguments_string = arguments end end
@return [String] The name of the command. Defaults to a snake-cased
version of the class’ name.
# File lib/claide/command.rb, line 139 def command @command ||= name.split('::').last.gsub(/[A-Z]+[a-z]*/) do |part| part.downcase << '-' end[0..-2] end
Searches the list of subcommands that should not be ignored for command lookup for a subcommand with the given `name`.
@param [String] name
The name of the subcommand to be found.
@return [CLAide::Command, nil] The subcommand, if found.
# File lib/claide/command.rb, line 210 def self.find_subcommand(name) subcommands_for_command_lookup.find { |sc| sc.command == name } end
@return [String] The full command up-to this command, as it would be
looked up during parsing.
@note (see ignore_in_command_lookup)
@example
BevarageMaker::Tea.full_command # => "beverage-maker tea"
# File lib/claide/command.rb, line 163 def self.full_command if superclass == Command ignore_in_command_lookup? ? '' : command else if ignore_in_command_lookup? superclass.full_command else "#{superclass.full_command} #{command}" end end end
Presents an exception to the user in a short manner in case of an `InformativeError` or in long form in other cases,
@param [Command, nil] command
The command from where the exception originated.
@param [Object] exception
The exception to present.
@return [void]
# File lib/claide/command.rb, line 387 def self.handle_exception(command, exception) if exception.is_a?(InformativeError) puts exception.message if command.nil? || command.verbose? puts puts(*exception.backtrace) end exit exception.exit_status else report_error(exception) end end
@visibility private
@param [String] error_message
The error message to show to the user.
@param [Class] help_class
The class to use to raise a ‘help’ error.
@raise [Help]
Signals CLAide that a help banner for this command should be shown, with an optional error message.
@return [void]
# File lib/claide/command.rb, line 438 def self.help!(error_message = nil, help_class = Help) raise help_class.new(banner, error_message) end
# File lib/claide/command.rb, line 63 def ignore_in_command_lookup=(flag) @ignore_in_command_lookup = self.abstract_command = flag end
@visibility private
Automatically registers a subclass as a subcommand.
# File lib/claide/command.rb, line 218 def self.inherited(subcommand) subcommands << subcommand end
Convenience method. Instantiate the command and run it with the provided arguments at once.
@note This method validate! the command before running it, but contrary to CLAide::Command::run
, it does not load plugins nor exit on failure. It is up to the caller to rescue any possible exception raised.
@param [String…, Array<String>] args
The arguments to initialize the command with
@raise [Help] If validate! fails
# File lib/claide/command.rb, line 541 def self.invoke(*args) command = new(ARGV.new(args.flatten)) command.validate! command.run end
@param [Array, ARGV] argv
A list of (remaining) parameters.
@return [Command] Returns the default subcommand initialized with the
given arguments.
# File lib/claide/command.rb, line 366 def self.load_default_subcommand(argv) unless subcommand = find_subcommand(default_subcommand) raise 'Unable to find the default subcommand ' \ "`#{default_subcommand}` for command `#{self}`." end result = subcommand.parse(argv) result.invoked_as_default = true result end
Subclasses should override this method to remove the arguments/options they support from `argv` before calling `super`.
The `super` implementation sets the {#verbose} attribute based on whether or not the `–verbose` option is specified; and the {#ansi_output} attribute to `false` if {Command.ansi_output} returns `true`, but the user specified the `–no-ansi` option.
@param [ARGV, Array] argv
A list of (user-supplied) params that should be handled.
# File lib/claide/command.rb, line 521 def initialize(argv) argv = ARGV.coerce(argv) @verbose = argv.flag?('verbose') @ansi_output = argv.flag?('ansi', Command.ansi_output?) @argv = argv @help_arg = argv.flag?('help') end
Should be overridden by a subclass if it handles any options.
The subclass has to combine the result of calling `super` and its own list of options. The recommended way of doing this is by concatenating to this classes’ own options.
@return [Array<Array>]
A list of option name and description tuples.
@example
def self.options [ ['--verbose', 'Print more info'], ['--help', 'Print help banner'], ].concat(super) end
# File lib/claide/command.rb, line 251 def self.options if root_command? DEFAULT_ROOT_OPTIONS + DEFAULT_OPTIONS else DEFAULT_OPTIONS end end
@param [Array, ARGV] argv
A list of (remaining) parameters.
@return [Command] An instance of the command class that was matched by
going through the arguments in the parameters and drilling down command classes.
# File lib/claide/command.rb, line 347 def self.parse(argv) argv = ARGV.coerce(argv) cmd = argv.arguments.first if cmd && subcommand = find_subcommand(cmd) argv.shift_argument subcommand.parse(argv) elsif abstract_command? && default_subcommand load_default_subcommand(argv) else new(argv) end end
@return [Array<String>] The prefixes used to search for CLAide
plugins.
Plugins are loaded via their `<plugin_prefix>_plugin.rb` file. Defaults to search for `claide` plugins.
# File lib/claide/command.rb, line 87 def plugin_prefixes @plugin_prefixes ||= ['claide'] end
Allows the application to perform custom error reporting, by overriding this method.
@param [Exception] exception
An exception that occurred while running a command through {Command.run}.
@raise
By default re-raises the specified exception.
@return [void]
# File lib/claide/command.rb, line 414 def self.report_error(exception) plugins = PluginManager.plugins_involved_in_exception(exception) unless plugins.empty? puts '[!] The exception involves the following plugins:' \ "\n - #{plugins.join("\n - ")}\n".ansi.yellow end raise exception end
@return [Bool] Whether this is the root command class
# File lib/claide/command.rb, line 177 def self.root_command? superclass == CLAide::Command end
Instantiates the command class matching the parameters through {Command.parse}, validates it through {Command#validate!}, and runs it through {Command#run}.
@note The ANSI
support is configured before running a command to allow
the same process to run multiple commands with different settings. For example a process with ANSI output enabled might want to programmatically invoke another command with the output enabled.
@param [Array, ARGV] argv
A list of parameters. For instance, the standard `ARGV` constant, which contains the parameters passed to the program.
@return [void]
# File lib/claide/command.rb, line 324 def self.run(argv = []) plugin_prefixes.each do |plugin_prefix| PluginManager.load_plugins(plugin_prefix) end argv = ARGV.coerce(argv) command = parse(argv) ANSI.disabled = !command.ansi_output? unless command.handle_root_options(argv) command.validate! command.run end rescue Object => exception handle_exception(command, exception) end
@return [Array<Class>] A list of all command classes that are nested
under this command.
# File lib/claide/command.rb, line 184 def self.subcommands @subcommands ||= [] end
@return [Array<Class>] A list of command classes that are nested under
this command _or_ the subcommands of those command classes in case the command class should be ignored in command lookup.
# File lib/claide/command.rb, line 192 def self.subcommands_for_command_lookup subcommands.map do |subcommand| if subcommand.ignore_in_command_lookup? subcommand.subcommands_for_command_lookup else subcommand end end.flatten end
Protected Class Methods
Handle deprecated form of self.arguments as an Array<Array<(String
, Symbol)>> like in:
self.arguments = [ ['NAME', :required], ['QUERY', :optional] ]
@todo Remove deprecated format support
# File lib/claide/command.rb, line 631 def self.arguments_array=(arguments) warn '[!] The signature of CLAide#arguments has changed. ' \ "Use CLAide::Argument (#{self}: `#{arguments}`)".ansi.yellow @arguments = arguments.map do |(name_str, type)| names = name_str.split('|') required = (type == :required) Argument.new(names, required) end end
Handle deprecated form of self.arguments as a String
, like in:
self.arguments = 'NAME [QUERY]'
@todo Remove deprecated format support
# File lib/claide/command.rb, line 647 def self.arguments_string=(arguments) warn '[!] The specification of arguments as a string has been' \ " deprecated #{self}: `#{arguments}`".ansi.yellow @arguments = arguments.split(' ').map do |argument| if argument.start_with?('[') Argument.new(argument.sub(/\[(.*)\]/, '\1').split('|'), false) else Argument.new(argument.split('|'), true) end end end
Handle depracted form of assigning a plugin prefix.
@todo Remove deprecated form.
# File lib/claide/command.rb, line 663 def self.plugin_prefix=(prefix) warn '[!] The specification of a singular plugin prefix has been ' \ "deprecated. Use `#{self}::plugin_prefixes` instead." plugin_prefixes << prefix end
Private Class Methods
Adds a new option for the current command.
This method can be used in conjunction with overriding `options`.
@return [void]
@example
option '--help', 'Print help banner '
# File lib/claide/command.rb, line 269 def self.option(name, description) mod = Module.new do define_method(:options) do [ [name, description], ].concat(super()) end end extend(mod) end
Public Instance Methods
Handles root commands options if appropriate.
@param [ARGV] argv
The parameters of the command.
@return [Bool] Whether any root command option was handled.
# File lib/claide/command.rb, line 288 def handle_root_options(argv) return false unless self.class.root_command? if argv.flag?('version') print_version return true end false end
Prints the version of the command optionally including plugins.
# File lib/claide/command.rb, line 299 def print_version puts self.class.version if verbose? PluginManager.specifications.each do |spec| puts "#{spec.name}: #{spec.version}" end end end
This method should be overridden by the command class to perform its work.
@return [void]
# File lib/claide/command.rb, line 579 def run raise 'A subclass should override the `CLAide::Command#run` method to ' \ 'actually perform some work.' end
Raises a Help
exception if the `–help` option is specified, if `argv` still contains remaining arguments/options by the time it reaches this implementation, or when called on an ‘abstract command’.
Subclasses should call `super` before doing their own validation. This way when the user specifies the `–help` flag a help banner is shown, instead of possible actual validation errors.
@raise [Help]
@return [void]
# File lib/claide/command.rb, line 565 def validate! banner! if help? unless @argv.empty? argument = @argv.remainder.first help! ArgumentSuggester.new(argument, self.class).suggestion end help! if self.class.abstract_command? end
Protected Instance Methods
@param [String] error_message
A custom optional error message
@raise [Help]
Signals CLAide that a help banner for this command should be shown, with an optional error message.
@return [void]
# File lib/claide/command.rb, line 608 def help!(error_message = nil) invoked_command_class.help!(error_message) end
Returns the class of the invoked command
@return [Command]
# File lib/claide/command.rb, line 590 def invoked_command_class if invoked_as_default? self.class.superclass else self.class end end