Class: CLAide::Command

Inherits:
Object
  • Object
show all
Defined in:
lib/claide/command.rb,
lib/claide/command/banner.rb,
lib/claide/command/plugin_manager.rb,
lib/claide/command/argument_suggester.rb

Overview

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 #initialize if it handles any parameters.
  • Override #validate! to check if the required parameters the command handles are valid, or call #help! in case they’re not.

Abstract command

The following is needed for an abstract 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 #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.

Defined Under Namespace

Classes: ArgumentSuggester, Banner, PluginManager

Constant Summary collapse

DEFAULT_ROOT_OPTIONS =
[
  ['--version', 'Show the version of the tool'],
]
DEFAULT_OPTIONS =
[
  ['--verbose', 'Show more debugging information'],
  ['--no-ansi', 'Show output without ANSI codes'],
  ['--help',    'Show help banner of specified command'],
]

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(argv) ⇒ Command

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 ansi_output returns true, but the user specified the --no-ansi option.

Parameters:

  • argv (ARGV, Array)

    A list of (user-supplied) params that should be handled.



521
522
523
524
525
526
527
# 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

Class Attribute Details

.abstract_commandBoolean Also known as: abstract_command?

Returns Indicates whether or not this command can actually perform work of itself, or that it only contains subcommands.

Returns:

  • (Boolean)

    Indicates whether or not this command can actually perform work of itself, or that it only contains subcommands.



52
53
54
# File 'lib/claide/command.rb', line 52

def abstract_command
  @abstract_command
end

.ansi_outputBoolean Also known as: ansi_output?

Returns The default value for #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 gem).

Returns:

  • (Boolean)

    The default value for #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 gem).



127
128
129
130
131
132
# File 'lib/claide/command.rb', line 127

def ansi_output
  if @ansi_output.nil?
    @ansi_output = STDOUT.tty?
  end
  @ansi_output
end

.commandString

Returns The name of the command. Defaults to a snake-cased version of the class’ name.

Returns:

  • (String)

    The name of the command. Defaults to a snake-cased version of the class’ name.



139
140
141
142
143
# 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

.default_subcommandString

Returns The subcommand which an abstract command should invoke by default.

Returns:

  • (String)

    The subcommand which an abstract command should invoke by default.



70
71
72
# File 'lib/claide/command.rb', line 70

def default_subcommand
  @default_subcommand
end

.descriptionString

Returns 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.

Returns:

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



81
82
83
# File 'lib/claide/command.rb', line 81

def description
  @description
end

.ignore_in_command_lookupBoolean Also known as: ignore_in_command_lookup?

Returns 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.

Returns:

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



61
62
63
# File 'lib/claide/command.rb', line 61

def ignore_in_command_lookup
  @ignore_in_command_lookup
end

.plugin_prefixesArray<String>

Returns The prefixes used to search for CLAide plugins. Plugins are loaded via their <plugin_prefix>_plugin.rb file. Defaults to search for claide plugins.

Returns:

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



87
88
89
# File 'lib/claide/command.rb', line 87

def plugin_prefixes
  @plugin_prefixes ||= ['claide']
end

.summaryString

Returns A brief description of the command, which is shown next to the command in the help banner of a parent command.

Returns:

  • (String)

    A brief description of the command, which is shown next to the command in the help banner of a parent command.



75
76
77
# File 'lib/claide/command.rb', line 75

def summary
  @summary
end

.versionString

Returns The version of the command. This value will be printed by the --version flag if used for the root command.

Returns:

  • (String)

    The version of the command. This value will be printed by the --version flag if used for the root command.



149
150
151
# File 'lib/claide/command.rb', line 149

def version
  @version
end

Instance Attribute Details

#ansi_outputBoolean Also known as: ansi_output?

Note:

If you want to make use of this value for your own configuration, you should check the value after calling the super #initialize implementation.

Set to true if ansi_output returns true and the user did not specify the --no-ansi option.

Returns:

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



497
498
499
# File 'lib/claide/command.rb', line 497

def ansi_output
  @ansi_output
end

#help_argBoolean Also known as: help?

Set to true if initialized with a --help flag

Returns:

  • (Boolean)

    Whether the command was initialized with argv containing --help



506
507
508
# File 'lib/claide/command.rb', line 506

def help_arg
  @help_arg
end

#invoked_as_defaultBool Also known as: invoked_as_default?

Returns Whether the command was invoked by an abstract command by default.

Returns:

  • (Bool)

    Whether the command was invoked by an abstract command by default.



550
551
552
# File 'lib/claide/command.rb', line 550

def invoked_as_default
  @invoked_as_default
end

#verboseBoolean Also known as: verbose?

Note:

If you want to make use of this value for your own configuration, you should check the value after calling the super #initialize implementation.

Set to true if the user specifies the --verbose option.

Returns:

  • (Boolean)

    Wether or not backtraces should be included when presenting the user an exception that includes the InformativeError module.



483
484
485
# File 'lib/claide/command.rb', line 483

def verbose
  @verbose
end

Class Method Details

.argumentsArray<Argument>

Returns 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.

Returns:

  • (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



98
99
100
# File 'lib/claide/command.rb', line 98

def arguments
  @arguments ||= []
end

.arguments=(arguments) ⇒ Object

TODO:

Remove deprecation

Parameters:

  • arguments (Array<Argument>)

    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



109
110
111
112
113
114
115
116
117
118
119
# 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

.arguments_array=(arguments) ⇒ Object (protected)

TODO:

Remove deprecated format support

Handle deprecated form of self.arguments as an Array> like in:

self.arguments = [ ['NAME', :required], ['QUERY', :optional] ]



631
632
633
634
635
636
637
638
639
# 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

.arguments_string=(arguments) ⇒ Object (protected)

TODO:

Remove deprecated format support

Handle deprecated form of self.arguments as a String, like in:

self.arguments = 'NAME [QUERY]'



647
648
649
650
651
652
653
654
655
656
657
# 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

.find_subcommand(name) ⇒ CLAide::Command?

Searches the list of subcommands that should not be ignored for command lookup for a subcommand with the given name.

Parameters:

  • name (String)

    The name of the subcommand to be found.

Returns:



210
211
212
# File 'lib/claide/command.rb', line 210

def self.find_subcommand(name)
  subcommands_for_command_lookup.find { |sc| sc.command == name }
end

.full_commandString

Returns The full command up-to this command, as it would be looked up during parsing.

Examples:


BevarageMaker::Tea.full_command # => "beverage-maker tea"

Returns:

  • (String)

    The full command up-to this command, as it would be looked up during parsing.



163
164
165
166
167
168
169
170
171
172
173
# 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

.handle_exception(command, exception) ⇒ void

This method returns an undefined value.

Presents an exception to the user in a short manner in case of an InformativeError or in long form in other cases,

Parameters:

  • command (Command, nil)

    The command from where the exception originated.

  • exception (Object)

    The exception to present.



387
388
389
390
391
392
393
394
395
396
397
398
# 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

.invoke(*args) ⇒ Object

Note:

This method validate! the command before running it, but contrary to

Convenience method. Instantiate the command and run it with the provided arguments at once.

CLAide::Command::run, it does not load plugins nor exit on failure. It is up to the caller to rescue any possible exception raised.

Parameters:

  • args (String..., Array<String>)

    The arguments to initialize the command with

Raises:

  • (Help)

    If validate! fails



541
542
543
544
545
# File 'lib/claide/command.rb', line 541

def self.invoke(*args)
  command = new(ARGV.new(args.flatten))
  command.validate!
  command.run
end

.load_default_subcommand(argv) ⇒ Command

Returns the default subcommand initialized with the given arguments.

Parameters:

  • argv (Array, ARGV)

    A list of (remaining) parameters.

Returns:

  • (Command)

    Returns the default subcommand initialized with the given arguments.



366
367
368
369
370
371
372
373
374
# 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

.optionsArray<Array>

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.

Examples:


def self.options
  [
    ['--verbose', 'Print more info'],
    ['--help',    'Print help banner'],
  ].concat(super)
end

Returns:

  • (Array<Array>)

    A list of option name and description tuples.



251
252
253
254
255
256
257
# File 'lib/claide/command.rb', line 251

def self.options
  if root_command?
    DEFAULT_ROOT_OPTIONS + DEFAULT_OPTIONS
  else
    DEFAULT_OPTIONS
  end
end

.parse(argv) ⇒ Command

Returns An instance of the command class that was matched by going through the arguments in the parameters and drilling down command classes.

Parameters:

  • argv (Array, ARGV)

    A list of (remaining) parameters.

Returns:

  • (Command)

    An instance of the command class that was matched by going through the arguments in the parameters and drilling down command classes.



347
348
349
350
351
352
353
354
355
356
357
358
# 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

.plugin_prefix=(prefix) ⇒ Object (protected)

TODO:

Remove deprecated form.

Handle depracted form of assigning a plugin prefix.



663
664
665
666
667
# 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

.report_error(exception) ⇒ void

This method returns an undefined value.

Allows the application to perform custom error reporting, by overriding this method.

Parameters:

  • exception (Exception)

    An exception that occurred while running a command through run.

Raises:

  • By default re-raises the specified exception.



414
415
416
417
418
419
420
421
# 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

.root_command?Bool

Returns Whether this is the root command class.

Returns:

  • (Bool)

    Whether this is the root command class



177
178
179
# File 'lib/claide/command.rb', line 177

def self.root_command?
  superclass == CLAide::Command
end

.run(argv = []) ⇒ void

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.

This method returns an undefined value.

Instantiates the command class matching the parameters through parse, validates it through #validate!, and runs it through #run.

Parameters:

  • argv (Array, ARGV) (defaults to: [])

    A list of parameters. For instance, the standard ARGV constant, which contains the parameters passed to the program.



324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# 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

.subcommandsArray<Class>

Returns A list of all command classes that are nested under this command.

Returns:

  • (Array<Class>)

    A list of all command classes that are nested under this command.



184
185
186
# File 'lib/claide/command.rb', line 184

def self.subcommands
  @subcommands ||= []
end

.subcommands_for_command_lookupArray<Class>

Returns 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.

Returns:

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



192
193
194
195
196
197
198
199
200
# 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

Instance Method Details

#banner!void (protected)

Note:

Calling this method exits the current process.

This method returns an undefined value.

Print banner and exit



618
619
620
# File 'lib/claide/command.rb', line 618

def banner!
  invoked_command_class.banner!
end

#handle_root_options(argv) ⇒ Bool

Handles root commands options if appropriate.

Parameters:

  • argv (ARGV)

    The parameters of the command.

Returns:

  • (Bool)

    Whether any root command option was handled.



288
289
290
291
292
293
294
295
# 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

#help!(error_message = nil) ⇒ void (protected)

This method returns an undefined value.

Parameters:

  • error_message (String) (defaults to: nil)

    A custom optional error message

Raises:

  • (Help)

    Signals CLAide that a help banner for this command should be shown, with an optional error message.



608
609
610
# File 'lib/claide/command.rb', line 608

def help!(error_message = nil)
  invoked_command_class.help!(error_message)
end

#invoked_command_classCommand (protected)

Returns the class of the invoked command

Returns:



590
591
592
593
594
595
596
# File 'lib/claide/command.rb', line 590

def invoked_command_class
  if invoked_as_default?
    self.class.superclass
  else
    self.class
  end
end

Prints the version of the command optionally including plugins.



299
300
301
302
303
304
305
306
# 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

#runvoid

This method returns an undefined value.

This method should be overridden by the command class to perform its work.



579
580
581
582
# 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

#validate!void

This method returns an undefined value.

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.

Raises:



565
566
567
568
569
570
571
572
# 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