Class: Console::Command

Inherits:
Object show all
Defined in:
lib/quality_extensions/console/command.rb,
lib/quality_extensions/console/command.facets.1.8.51.rb,
lib/quality_extensions/console/command.facets.1.8.54.rb

Overview

Console Command

Console::Command provides a clean and easy way to create a command line interface for your program. The unique technique utlizes a Commandline to Object Mapping (COM) to make it quick and easy.

Synopsis

Let’s make an executable called ‘mycmd’.

#!/usr/bin/env ruby

require 'facets'
require 'command'

MyCmd << Console::Command

  def _v
    $VERBOSE = true
  end

  def jump
    if $VERBOSE
      puts "JUMP! JUMP! JUMP!"
    else
      puts "Jump"
    end
  end

end

MyCmd.execute

Then on the command line:

% mycmd jump
Jump

% mycmd -v jump
JUMP! JUMP! JUMP!

Subcommands

Commands can take subcommand and suboptions. To do this simply add a module to your class with the same name as the subcommand, in which the suboption methods are defined.

MyCmd << Console::Command

  def initialize
    @height = 1
  end

  def _v
    $VERBOSE = true
  end

  def jump
    if $VERBOSE
      puts "JUMP!" * @height
    else
      puts "Jump" * @height
    end
  end

  module Jump
    def __height(h)
      @height = h.to_i
    end
  end

end

MyCmd.start

Then on the command line:

% mycmd jump -h 2
Jump Jump

% mycmd -v jump -h 3
JUMP! JUMP! JUMP!

Another thing to notice about this example is that #start is an alias for #execute.

Missing Subcommands

You can use #method_missing to catch missing subcommand calls.

Main and Default

If your command does not take subcommands then simply define a #main method to dispatch action. All options will be treated globablly in this case and any remaining comman-line arguments will be passed to #main.

If on the other hand your command does take subcommands but none is given, the #default method will be called, if defined. If not defined an error will be raised (but only reported if $DEBUG is true).

Global Options

You can define global options which are options that will be processed no matter where they occur in the command line. In the above examples only the options occuring before the subcommand are processed globally. Anything occuring after the subcommand belonds strictly to the subcommand. For instance, if we had added the following to the above example:

global_option :_v

Then -v could appear anywhere in the command line, even on the end, and still work as expected.

% mycmd jump -h 3 -v

Missing Options

You can use #option_missing to catch any options that are not explicility defined.

The method signature should look like:

option_missing(option_name, args)

Example:

def option_missing(option_name, args)
  p args if $debug
  case option_name
    when 'p'
      @a = args[0].to_i
      @b = args[1].to_i
      2
    else
      raise InvalidOptionError(option_name, args)
  end
end

Its return value should be the effective “arity” of that options – that is, how many arguments it consumed (“-p a b”, for example, would consume 2 args: “a” and “b”). An arity of 1 is assumed if nil or false is returned.

Be aware that when using subcommand modules, the same option_missing method will catch missing options for global options and subcommand options too unless an option_missing method is also defined in the subcommand module.

Help Documentation

You can also add help information quite easily. If the following code is saved as ‘foo’ for instance.

MyCmd << Console::Command

  help "Dispays the word JUMP!"

  def jump
    if $VERBOSE
      puts "JUMP! JUMP! JUMP!"
    else
      puts "Jump"
    end
  end

end

MyCmd.execute

then by running ‘foo help’ on the command line, standard help information will be displayed.

foo

  jump  Displays the word JUMP!

++

Defined Under Namespace

Classes: UnknownOptionError

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(global_options = []) ⇒ Command

Do not let this pass through to any included module.



329
330
331
# File 'lib/quality_extensions/console/command.rb', line 329

def initialize(global_options=[])
  @global_options = global_options
end

Class Method Details

.alias_subcommand(hash) ⇒ Object



316
317
318
# File 'lib/quality_extensions/console/command.rb', line 316

def alias_subcommand(hash)
  (@subcommand_aliases ||= {}).merge! hash
end

.execute(*args) ⇒ Object

Starts the command execution.



249
250
251
252
253
254
# File 'lib/quality_extensions/console/command.rb', line 249

def execute( *args )
  cmd = new()
  cmd.instance_variable_set("@global_options", global_options)
  cmd.instance_variable_set("@subcommand_aliases", @subcommand_aliases || {})
  cmd.execute( *args )
end

.global_option(*names) ⇒ Object

Change the option mode.



260
261
262
# File 'lib/quality_extensions/console/command.rb', line 260

def global_option( *names )
  names.each{ |name| global_options << name.to_sym }
end

.global_optionsObject



264
265
266
# File 'lib/quality_extensions/console/command.rb', line 264

def global_options
  @global_options ||= []
end

.pass_through(options, mod) ⇒ Object

This is to be called from your subcommand module to specify which options should simply be “passed on” to some wrapped command that you will later call. Options that are collected by the option methods that this generates will be stored in @passthrough_options (so remember to append that array to your wrapped command!).

module Status
  Console::Command.pass_through({
    [:_q, :__quiet] => 0,
    [:_N, :__non_recursive] => 0,
    [:__no_ignore] => 0,
  }, self)
end

Development notes:

  • Currently requires you to pass the subcommand module’s “self” to this method. I didn’t know of a better way to cause it to create the instance methods in that module rather than here in Console::Command.

  • Possible alternatives:

    • Binding.of_caller() (facets.rubyforge.org/src/doc/rdoc/classes/Binding.html) – wary of using it if it depends on Continuations, which I understand are deprecated

    • copy the pass_through class method to each subcommand module so that calls will be in the module’s context…



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/quality_extensions/console/command.rb', line 284

def pass_through(options, mod)
  options.each do |method_names, arity|
    method_names.each do |method_name|
      if method_name == :_u
        #puts "Defining method #{method_name}(with arity #{arity}) in #{mod.name}"
        #puts "#{mod.name} has #{(mod.methods - Object.methods).inspect}"
      end
      option_name = method_name.to_s.option_demethodize
      mod.send(:define_method, method_name.to_sym) do |*args|
        @passthrough_options << option_name
        args_for_current_option = Escape.shell_command(args.slice(0, arity))
        @passthrough_options << args_for_current_option unless args_for_current_option == ''
        #p args_for_current_option
        #puts "in #{method_name}: Passing through #{arity} options: #{@passthrough_options.inspect}"  #(why does @passthrough_options show up as nil? even when later on it's *not* nil...)
        arity
      end

  #        mod.instance_eval %Q{
  #          def #{method_name}(*args)
  #            @passthrough_options << '#{option_name}'
  #            args_for_current_option = Escape.shell_command(args.slice(0, #{arity}))
  #            @passthrough_options << args_for_current_option unless args_for_current_option == ''
  #            #p args_for_current_option
  #            #puts "in #{method_name}: Passing through #{arity} options: #{@passthrough_options.inspect}"  #(why does @passthrough_options show up as nil? even when later on it's *not* nil...)
  #            #{arity}
  #          end
  #        }
      
    end
  end
end

.startObject

Starts the command execution.



257
258
259
260
261
262
# File 'lib/quality_extensions/console/command.rb', line 257

def execute( *args )
  cmd = new()
  cmd.instance_variable_set("@global_options", global_options)
  cmd.instance_variable_set("@subcommand_aliases", @subcommand_aliases || {})
  cmd.execute( *args )
end

Instance Method Details

#execute(line = nil) ⇒ Object

Execute the command.



335
336
337
338
339
340
341
342
343
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
# File 'lib/quality_extensions/console/command.rb', line 335

def execute( line=nil )
begin
  case line
  when String
    arguments = Shellwords.shellwords(line)
  when Array
    arguments = line
  else
    arguments = ARGV
  end

  # Duplicate arguments to work on them in-place.
  argv = arguments.dup

  # Split single letter option groupings into separate options.
  # ie. -xyz => -x -y -z
  argv = argv.collect { |arg|
    if md = /^-(\w{2,})/.match( arg )
      md[1].split(//).collect { |c| "-#{c}" }
    else
      arg
    end
  }.flatten

  # Process global options
  global_options.each do |name|
    o = name.to_s.option_demethodize
    m = method(name)
    c = m.arity
    while i = argv.index(o)
      args = argv.slice!(i,c+1)
      args.shift
      m.call(*args)
    end
  end

  # Does this command take subcommands?
  takes_subcommands = !respond_to?(:main)

  # Process primary options
  argv = execute_options( argv, takes_subcommands )

  # If this command doesn't take subcommands, then the remaining arguments are arguments for main().
  return send(:main, *argv) unless takes_subcommands

  # What to do if there is nothing else?
  if argv.empty?
    if respond_to?(:default)
      return __send__(:default)
    else
      $stderr << "Nothing to do."
      puts '' # :fix: This seems to be necessary or else I don't see the $stderr output at all! --Tyler
      return
    end
  end

  # Remaining arguments are subcommand and suboptions.

  @subcommand = argv.shift.gsub('-','_')
  @subcommand = (subcommand_aliases[@subcommand.to_sym] || @subcommand).to_s
  puts "@subcommand = #{@subcommand}" if $debug

  # Extend subcommand option module
  #subconst = subcommand.gsub(/\W/,'_').capitalize
  subconst = @subcommand.modulize
  #p self.class.constants if $debug
  if self.class.const_defined?(subconst)
    puts "Extending self (#{self.class}) with subcommand module #{subconst}" if $debug
    submod = self.class.const_get(subconst)
    #puts "... which has these **module** methods (should be instance methods): #{(submod.methods - submod.instance_methods - Object.methods).sort.inspect}"
    self.extend submod
    #puts "... and now self has: #{(self.methods - Object.methods).sort.inspect}"
  end

  # Is the subcommand defined?
  # This is a little tricky. The method has to be defined by a *subclass*.
  @subcommand_is_defined = self.respond_to?( @subcommand ) and
                           !Console::Command.public_instance_methods.include?( @subcommand.to_s )

  # The rest of the args will be interpreted as options for this particular subcommand options.
  argv = execute_options( argv, false )

  # Actually call the subcommand (or method_missing if the subcommand method isn't defined)
  if @subcommand_is_defined
    puts "Calling #{@subcommand}(#{argv.inspect})" if $debug
    __send__(@subcommand, *argv)
  else
    #begin
      puts "Calling method_missing with #{@subcommand}, #{argv.inspect}" if $debug
      method_missing(@subcommand, *argv)
    #rescue NoMethodError => e
      #if self.private_methods.include?( "no_command_error" )
      #  no_command_error( *args )
      #else
    #    $stderr << "Non-applicable command -- #{argv.join(' ')}\n"
    #    exit -1
      #end
    #end
  end

rescue UnknownOptionError => exception
  $stderr << exception.message << "\n"
  exit -1
end

#   rescue => err
#     if $DEBUG
#       raise err
#     else
#       msg = err.message.chomp('.') + '.'
#       msg[0,1] = msg[0,1].capitalize
#       msg << " (#{err.class})" if $VERBOSE
#       $stderr << msg
#     end
end