class Cri::Command
Cri::Command
represents a command that can be executed on the command line. It is also used for the command-line tool itself.
Attributes
@return [Array<String>] A list of aliases for this command that can be
used to invoke this command
@return [Boolean] true if the command should skip option parsing and treat all options as arguments.
@return [Boolean] true if the command should skip option parsing and treat all options as arguments.
@return [Proc] The block that should be executed when invoking this
command (ignored for commands with subcommands)
@return [Set<Cri::Command>] This command’s subcommands
@return [Symbol] The name of the default subcommand
@return [String] The long description (“description”)
@return [Boolean] Whether or not this command has parameters
@return [Boolean] Whether or not this command has parameters
@return [String] The name
@return [Array<Cri::OptionDefinition>] The list of option definitions
@return [Array<Hash>] The list of parameter definitions
@return [Set<Cri::Command>] This command’s subcommands
@return [String] The short description (“summary”)
@return [Cri::Command, nil] This command’s supercommand, or nil if the
command has no supercommand
@return [String] The usage, without the “usage:” prefix and without the
supercommands’ names.
Public Class Methods
Creates a new command using the DSL. If a string is given, the command will be defined using the string; if a block is given, the block will be used instead.
If the block has one parameter, the block will be executed in the same context with the command DSL as its parameter. If the block has no parameters, the block will be executed in the context of the DSL.
@param [String, nil] string The command definition as a string
@param [String, nil] filename The filename corresponding to the string parameter (only useful if a string is given)
@return [Cri::Command] The newly defined command
# File lib/cri/command.rb, line 122 def self.define(string = nil, filename = nil, &block) dsl = Cri::CommandDSL.new if string args = filename ? [string, filename] : [string] dsl.instance_eval(*args) elsif [-1, 0].include? block.arity dsl.instance_eval(&block) else block.call(dsl) end dsl.command end
Creates a new command using a DSL, from code defined in the given filename.
@param [String] filename The filename that contains the command definition
as a string
@return [Cri::Command] The newly defined command
# File lib/cri/command.rb, line 141 def self.load_file(filename, infer_name: false) code = File.read(filename, encoding: 'UTF-8') define(code, filename).tap do |cmd| if infer_name command_name = File.basename(filename, '.rb') cmd.modify { name command_name } end end end
# File lib/cri/command.rb, line 169 def initialize @aliases = Set.new @commands = Set.new @option_definitions = Set.new @parameter_definitions = [] @explicitly_no_params = false @default_subcommand_name = nil end
Returns a new command that implements showing help.
@return [Cri::Command] A basic help command
# File lib/cri/command.rb, line 164 def self.new_basic_help filename = File.dirname(__FILE__) + '/commands/basic_help.rb' define(File.read(filename)) end
Returns a new command that has support for the `-h`/`–help` option and also has a `help` subcommand. It is intended to be modified (adding name, summary, description, other subcommands, …)
@return [Cri::Command] A basic root command
# File lib/cri/command.rb, line 156 def self.new_basic_root filename = File.dirname(__FILE__) + '/commands/basic_root.rb' define(File.read(filename)) end
Public Instance Methods
Compares this command's name to the other given command's name.
@param [Cri::Command] other The command to compare with
@return [-1, 0, 1] The result of the comparison between names
@see Object<=>
# File lib/cri/command.rb, line 389 def <=>(other) name <=> other.name end
Adds the given command as a subcommand to the current command.
@param [Cri::Command] command The command to add as a subcommand
@return [void]
# File lib/cri/command.rb, line 209 def add_command(command) @commands << command command.supercommand = self end
# File lib/cri/command.rb, line 363 def all_opt_defns if supercommand supercommand.all_opt_defns | option_definitions else option_definitions end end
Returns the command with the given name. This method will display error messages and exit in case of an error (unknown or ambiguous command).
The name can be a full command name, a partial command name (e.g. “com” for “commit”) or an aliased command name (e.g. “ci” for “commit”).
@param [String] name The full, partial or aliases name of the command
@return [Cri::Command] The command with the given name
# File lib/cri/command.rb, line 264 def command_named(name, hard_exit: true) commands = commands_named(name) if commands.empty? warn "#{self.name}: unknown command '#{name}'\n" raise CriExitException.new(is_error: true) elsif commands.size > 1 warn "#{self.name}: '#{name}' is ambiguous:" warn " #{commands.map(&:name).sort.join(' ')}" raise CriExitException.new(is_error: true) else commands[0] end rescue CriExitException => e exit(e.error? ? 1 : 0) if hard_exit end
Returns the commands that could be referred to with the given name. If the result contains more than one command, the name is ambiguous.
@param [String] name The full, partial or aliases name of the command
@return [Array<Cri::Command>] A list of commands matching the given name
# File lib/cri/command.rb, line 242 def commands_named(name) # Find by exact name or alias @commands.each do |cmd| found = cmd.name == name || cmd.aliases.include?(name) return [cmd] if found end # Find by approximation @commands.select do |cmd| cmd.name[0, name.length] == name end end
Defines a new subcommand for the current command using the DSL.
@param [String, nil] name The name of the subcommand, or nil if no name
should be set (yet)
@return [Cri::Command] The subcommand
# File lib/cri/command.rb, line 220 def define_command(name = nil, &block) # Execute DSL dsl = Cri::CommandDSL.new dsl.name name unless name.nil? if [-1, 0].include? block.arity dsl.instance_eval(&block) else yield(dsl) end # Create command cmd = dsl.command add_command(cmd) cmd end
@return [Enumerable<Cri::OptionDefinition>] The option definitions for the
command itself and all its ancestors
# File lib/cri/command.rb, line 197 def global_option_definitions res = Set.new res.merge(option_definitions) res.merge(supercommand.global_option_definitions) if supercommand res end
@return [String] The help text for this command
@option params [Boolean] :verbose true if the help output should be
verbose, false otherwise.
@option params [IO] :io ($stdout) the IO the help text is intended for.
This influences the decision to enable/disable colored output.
# File lib/cri/command.rb, line 378 def help(params = {}) HelpRenderer.new(self, params).render end
Modifies the command using the DSL.
If the block has one parameter, the block will be executed in the same context with the command DSL as its parameter. If the block has no parameters, the block will be executed in the context of the DSL.
@return [Cri::Command] The command itself
# File lib/cri/command.rb, line 185 def modify(&block) dsl = Cri::CommandDSL.new(self) if [-1, 0].include? block.arity dsl.instance_eval(&block) else yield(dsl) end self end
Runs the command with the given command-line arguments, possibly invoking subcommands and passing on the options and arguments.
@param [Array<String>] opts_and_args A list of unparsed arguments
@param [Hash] parent_opts A hash of options already handled by the
supercommand
@return [void]
# File lib/cri/command.rb, line 290 def run(opts_and_args, parent_opts = {}, hard_exit: true) # Parse up to command name stuff = partition(opts_and_args) opts_before_subcmd, subcmd_name, opts_and_args_after_subcmd = *stuff if subcommands.empty? || (subcmd_name.nil? && !block.nil?) run_this(opts_and_args, parent_opts) else # Handle options handle_options(opts_before_subcmd) # Get command if subcmd_name.nil? if default_subcommand_name subcmd_name = default_subcommand_name else warn "#{name}: no command given" raise CriExitException.new(is_error: true) end end subcommand = command_named(subcmd_name, hard_exit: hard_exit) return if subcommand.nil? # Run subcommand.run(opts_and_args_after_subcmd, parent_opts.merge(opts_before_subcmd), hard_exit: hard_exit) end rescue CriExitException => e exit(e.error? ? 1 : 0) if hard_exit end
Runs the actual command with the given command-line arguments, not invoking any subcommands. If the command does not have an execution block, an error ir raised.
@param [Array<String>] opts_and_args A list of unparsed arguments
@param [Hash] parent_opts A hash of options already handled by the
supercommand
@raise [NotImplementedError] if the command does not have an execution
block
@return [void]
# File lib/cri/command.rb, line 333 def run_this(opts_and_args, parent_opts = {}) if all_opts_as_args? args = opts_and_args global_opts = parent_opts else # Parse parser = Cri::Parser.new( opts_and_args, global_option_definitions, parameter_definitions, explicitly_no_params?, ) handle_errors_while { parser.run } local_opts = parser.options global_opts = parent_opts.merge(parser.options) global_opts = add_defaults(global_opts) # Handle options handle_options(local_opts) args = handle_errors_while { parser.gen_argument_list } end # Execute if block.nil? raise NotImplementedError, "No implementation available for '#{name}'" end block.call(global_opts, args, self) end
Private Instance Methods
# File lib/cri/command.rb, line 441 def add_defaults(options) all_opt_defns_by_key = all_opt_defns.each_with_object({}) do |opt_defn, hash| key = (opt_defn.long || opt_defn.short).to_sym hash[key] = opt_defn end new_options = Hash.new do |hash, key| hash.fetch(key) { all_opt_defns_by_key[key]&.default } end options.each do |key, value| new_options[key] = value end new_options end
# File lib/cri/command.rb, line 425 def handle_errors_while yield rescue Cri::Parser::IllegalOptionError => e warn "#{name}: unrecognised option -- #{e}" raise CriExitException.new(is_error: true) rescue Cri::Parser::OptionRequiresAnArgumentError => e warn "#{name}: option requires an argument -- #{e}" raise CriExitException.new(is_error: true) rescue Cri::Parser::IllegalOptionValueError => e warn "#{name}: #{e.message}" raise CriExitException.new(is_error: true) rescue Cri::ArgumentList::ArgumentCountMismatchError => e warn "#{name}: #{e.message}" raise CriExitException.new(is_error: true) end
# File lib/cri/command.rb, line 395 def handle_options(opts) opts.each_pair do |key, value| opt_defn = global_option_definitions.find { |o| (o.long || o.short) == key.to_s } block = opt_defn.block block&.call(value, self) end end
# File lib/cri/command.rb, line 403 def partition(opts_and_args) return [{}, opts_and_args.first, opts_and_args] if all_opts_as_args? # Parse delegate = Cri::Command::ParserPartitioningDelegate.new parser = Cri::Parser.new( opts_and_args, global_option_definitions, parameter_definitions, explicitly_no_params?, ) parser.delegate = delegate handle_errors_while { parser.run } # Extract [ parser.options, delegate.last_argument, parser.unprocessed_arguments_and_options, ] end