Class: CmdParse::CommandParser

Inherits:
Object
  • Object
show all
Defined in:
lib/cmdparse.rb

Overview

Main Class for Creating a Command Based CLI Program

This class can directly be used (or sub-classed, if need be) to create a command based CLI program.

The CLI program itself is represented by the #main_command, a Command instance (as are all commands and sub-commands). This main command can either hold sub-commands (the normal use case) which represent the programs top level commands or take no commands in which case it acts similar to a simple OptionParser based program (albeit with better help functionality).

Parsing the command line for commands is done by this class, option parsing is delegated to the battle tested OptionParser of the Ruby standard library.

Usage

After initialization some optional information is expected to be set on the Command#options of the #main_command:

banner

A banner that appears in the help output before anything else.

program_name

The name of the program. If not set, this value is computed from $0.

version

The version string of the program.

In addition to the main command’s options instance (which represents the top level options that need to be specified before any command name), there is also a #global_options instance which represents options that can be specified anywhere on the command line.

Top level commands can be added to the main command by using the #add_command method.

Once everything is set up, the #parse method is used for parsing the command line.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(handle_exceptions: false, takes_commands: true) ⇒ CommandParser

Creates a new CommandParser object.

Options:

handle_exceptions

Set to true if exceptions should be handled gracefully by showing the error and a help message, or to false if exception should not be handled at all. If this options is set to :no_help, the exception is handled but no help message is shown.

takes_commands

Specifies whether the main program takes any commands.



807
808
809
810
811
812
813
814
815
816
817
# File 'lib/cmdparse.rb', line 807

def initialize(handle_exceptions: false, takes_commands: true)
  @global_options = OptionParser.new
  @main_command = Command.new('main', takes_commands: takes_commands)
  @main_command.super_command = self
  @main_command.options.stack[0] = MultiList.new(@global_options.stack)
  @handle_exceptions = handle_exceptions
  @help_line_width = 80
  @help_indent = 4
  @help_desc_indent = 18
  @data = {}
end

Instance Attribute Details

#current_commandObject (readonly)

The command that is being executed. Only available during parsing of the command line arguments.



777
778
779
# File 'lib/cmdparse.rb', line 777

def current_command
  @current_command
end

#dataObject

A data store (initially an empty Hash) that can be used for storing anything. For example, it can be used to store global option values. cmdparse itself doesn’t do anything with it.



781
782
783
# File 'lib/cmdparse.rb', line 781

def data
  @data
end

#handle_exceptionsObject (readonly)

Should exceptions be handled gracefully? I.e. by printing error message and the help screen?

See ::new for possible values.



786
787
788
# File 'lib/cmdparse.rb', line 786

def handle_exceptions
  @handle_exceptions
end

#help_desc_indentObject

The indentation used for, among other things, command descriptions.



795
796
797
# File 'lib/cmdparse.rb', line 795

def help_desc_indent
  @help_desc_indent
end

#help_indentObject

The amount of spaces to indent the content of help sections.



792
793
794
# File 'lib/cmdparse.rb', line 792

def help_indent
  @help_indent
end

#help_line_widthObject

The maximum width of the help lines.



789
790
791
# File 'lib/cmdparse.rb', line 789

def help_line_width
  @help_line_width
end

#main_commandObject (readonly)

The top level command representing the program itself.



773
774
775
# File 'lib/cmdparse.rb', line 773

def main_command
  @main_command
end

Instance Method Details

#add_command(*args, **kws, &block) ⇒ Object

Adds a top level command.

See Command#add_command for detailed invocation information.



848
849
850
# File 'lib/cmdparse.rb', line 848

def add_command(*args, **kws, &block)
  @main_command.add_command(*args, **kws, &block)
end

#global_options {|@global_options| ... } ⇒ Object

:call-seq:

cmdparse.global_options              -> OptionParser instance
cmdparse.gloabl_options {|opts| ...} -> opts (OptionParser instance)

Yields the global options if a block is given and returns them.

The global options are those options that can be used on the top level and with any command.

Yields:



840
841
842
843
# File 'lib/cmdparse.rb', line 840

def global_options
  yield(@global_options) if block_given?
  @global_options
end

#main_options {|@main_command.options| ... } ⇒ Object

:call-seq:

cmdparse.main_options              -> OptionParser instance
cmdparse.main_options {|opts| ...} -> opts (OptionParser instance)

Yields the main options (that are only available directly after the program name) if a block is given and returns them.

The main options are also used for setting the program name, version and banner.

Yields:



827
828
829
830
# File 'lib/cmdparse.rb', line 827

def main_options
  yield(@main_command.options) if block_given?
  @main_command.options
end

#parse(argv = ARGV) ⇒ Object

Parses the command line arguments.

If a block is given, the current hierarchy level and the name of the current command is yielded after the option parsing is done but before a command is executed.



856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
# File 'lib/cmdparse.rb', line 856

def parse(argv = ARGV) # :yields: level, command_name
  level = 0
  @current_command = @main_command

  while true
    argv = if @current_command.takes_commands? || ENV.include?('POSIXLY_CORRECT')
             @current_command.options.order(argv)
           else
             @current_command.options.permute(argv)
           end
    yield(level, @current_command.name) if block_given?

    if @current_command.takes_commands?
      cmd_name = argv.shift || @current_command.default_command

      if cmd_name.nil?
        raise NoCommandGivenError.new
      elsif !@current_command.commands.key?(cmd_name)
        raise InvalidCommandError.new(cmd_name)
      end

      @current_command = @current_command.commands[cmd_name]
      level += 1
    else
      original_n = @current_command.arity
      n = (original_n < 0 ? -original_n - 1 : original_n)
      if argv.size < n
        raise NotEnoughArgumentsError.new("#{n} - #{@current_command.usage_arguments}")
      elsif argv.size > n && original_n > 0
        raise TooManyArgumentsError.new("#{n} - #{@current_command.usage_arguments}")
      end

      argv.slice!(n..-1) unless original_n < 0
      @current_command.execute(*argv)
      break
    end
  end
rescue ParseError, OptionParser::ParseError => e
  raise unless @handle_exceptions
  puts "Error while parsing command line:\n    " + e.message
  if @handle_exceptions != :no_help && @main_command.commands.key?('help')
    puts
    @main_command.commands['help'].execute(*@current_command.command_chain.map(&:name))
  end
  exit(64) # FreeBSD standard exit error for "command was used incorrectly"
rescue Interrupt
  exit(128 + 2)
rescue Errno::EPIPE
  # Behave well when used in a pipe
ensure
  @current_command = nil
end