1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 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 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 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 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 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 908 909 910 911
|
#
#--
# cmdparse: advanced command line parser supporting commands
# Copyright (C) 2004-2020 Thomas Leitner
#
# This file is part of cmdparse which is licensed under the MIT.
#++
#
require 'optparse'
OptionParser::Officious.delete('version')
OptionParser::Officious.delete('help')
# Extension for OptionParser objects to allow access to some internals.
class OptionParser #:nodoc:
# Access the option list stack.
attr_reader :stack
# Returns +true+ if at least one local option is defined.
#
# The zeroth stack element is not respected when doing the query because it contains either the
# OptionParser::DefaultList or a CmdParse::MultiList with the global options of the
# CmdParse::CommandParser.
def options_defined?
stack[1..-1].each do |list|
list.each_option do |switch|
return true if switch.kind_of?(OptionParser::Switch) && (switch.short || switch.long)
end
end
false
end
# Returns +true+ if a banner has been set.
def banner?
!@banner.nil?
end
end
# Namespace module for cmdparse.
#
# See CmdParse::CommandParser and CmdParse::Command for the two important classes.
module CmdParse
# The version of this cmdparse implemention
VERSION = '3.0.7'.freeze
# Base class for all cmdparse errors.
class ParseError < StandardError
# Sets the error reason for the subclass.
def self.reason(reason)
@reason = reason
end
# Returns the error reason or 'CmdParse error' if it has not been set.
def self.get_reason
@reason ||= 'CmdParse error'
end
# Returns the reason plus the original message.
def message
str = super
self.class.get_reason + (str.empty? ? "" : ": #{str}")
end
end
# This error is thrown when an invalid command is encountered.
class InvalidCommandError < ParseError
reason 'Invalid command'
end
# This error is thrown when an invalid argument is encountered.
class InvalidArgumentError < ParseError
reason 'Invalid argument'
end
# This error is thrown when an invalid option is encountered.
class InvalidOptionError < ParseError
reason 'Invalid option'
end
# This error is thrown when no command was given and no default command was specified.
class NoCommandGivenError < ParseError
reason 'No command given'
def initialize #:nodoc:
super('')
end
end
# This error is thrown when a command is added to another command which does not support commands.
class TakesNoCommandError < ParseError
reason 'This command takes no other commands'
end
# This error is thrown when not enough arguments are provided for the command.
class NotEnoughArgumentsError < ParseError
reason 'Not enough arguments provided, minimum is'
end
# This error is thrown when too many arguments are provided for the command.
class TooManyArgumentsError < ParseError
reason 'Too many arguments provided, maximum is'
end
# Command Hash - will return partial key matches as well if there is a single non-ambigous
# matching key
class CommandHash < Hash #:nodoc:
def key?(name) #:nodoc:
!self[name].nil?
end
def [](cmd_name) #:nodoc:
super || begin
possible = keys.select {|key| key[0, cmd_name.length] == cmd_name }
fetch(possible[0]) if possible.size == 1
end
end
end
# Container for multiple OptionParser::List objects.
#
# This is needed for providing what's equivalent to stacked OptionParser instances and the global
# options implementation.
class MultiList #:nodoc:
def initialize(list) #:nodoc:
@list = list
end
def summarize(*args, &block) #:nodoc:
# We don't want summary information of the global options to automatically appear.
end
[:accept, :reject, :prepend, :append].each do |mname|
module_eval <<-EOF
def #{mname}(*args, &block)
@list[-1].#{mname}(*args, &block)
end
EOF
end
[:search, :complete, :each_option, :add_banner, :compsys].each do |mname|
module_eval <<-EOF
def #{mname}(*args, &block) #:nodoc:
@list.reverse_each {|list| list.#{mname}(*args, &block)}
end
EOF
end
def get_candidates(id, &b)
@list.reverse_each {|list| list.get_candidates(id, &b)}
end
end
# === Base class for commands
#
# This class implements all needed methods so that it can be used by the CommandParser class.
#
# Commands can either be created by sub-classing or on the fly when using the #add_command method.
# The latter allows for a more terse specification of a command while the sub-class approach
# allows to customize all aspects of a command by overriding methods.
#
# Basic example for sub-classing:
#
# class TestCommand < CmdParse::Command
# def initialize
# super('test', takes_commands: false)
# options.on('-m', '--my-opt', 'My option') { 'Do something' }
# end
# end
#
# parser = CmdParse::CommandParser.new
# parser.add_command(TestCommand.new)
# parser.parse
#
# Basic example for on the fly creation:
#
# parser = CmdParse::CommandParser.new
# parser.add_command('test') do |cmd|
# takes_commands(false)
# options.on('-m', '--my-opt', 'My option') { 'Do something' }
# end
# parser.parse
#
# === Basic Properties
#
# The only thing that is mandatory to set for a Command is its #name. If the command does not take
# any sub-commands, then additionally an #action block needs to be specified or the #execute
# method overridden.
#
# However, there are several other methods that can be used to configure the behavior of a
# command:
#
# #takes_commands:: For specifying whether sub-commands are allowed.
# #options:: For specifying command specific options.
# #add_command:: For specifying sub-commands if the command takes them.
#
# === Help Related Methods
#
# Many of this class' methods are related to providing useful help output. While the most common
# methods can directly be invoked to set or retrieve information, many other methods compute the
# needed information dynamically and therefore need to be overridden to customize their return
# value.
#
# #short_desc::
# For a short description of the command (getter/setter).
# #long_desc::
# For a detailed description of the command (getter/setter).
# #argument_desc::
# For describing command arguments (setter).
# #help, #help_banner, #help_short_desc, #help_long_desc, #help_commands, #help_arguments, #help_options::
# For outputting the general command help or individual sections of the command help (getter).
# #usage, #usage_options, #usage_arguments, #usage_commands::
# For outputting the usage line or individual parts of it (getter).
#
# === Built-in Commands
#
# cmdparse ships with two built-in commands:
# * HelpCommand (for showing help messages) and
# * VersionCommand (for showing version information).
class Command
# The name of the command.
attr_reader :name
# Returns the name of the default sub-command or +nil+ if there isn't any.
attr_reader :default_command
# Sets or returns the super-command of this command. The super-command is either a Command
# instance for normal commands or a CommandParser instance for the main command (ie.
# CommandParser#main_command).
attr_accessor :super_command
# Returns the mapping of command name to command for all sub-commands of this command.
attr_reader :commands
# A data store (initially an empty Hash) that can be used for storing anything. For example, it
# can be used to store option values. cmdparse itself doesn't do anything with it.
attr_accessor :data
# Initializes the command called +name+.
#
# Options:
#
# takes_commands:: Specifies whether this command can take sub-commands.
def initialize(name, takes_commands: true)
@name = name.freeze
@options = OptionParser.new
@commands = CommandHash.new
@default_command = nil
@action = nil
@argument_desc ||= {}
@data = {}
takes_commands(takes_commands)
end
# Sets whether this command can take sub-command.
#
# The argument +val+ needs to be +true+ or +false+.
def takes_commands(val)
if !val && !commands.empty?
raise Error, "Can't change takes_commands to false because there are already sub-commands"
else
@takes_commands = val
end
end
alias takes_commands= takes_commands
# Return +true+ if this command can take sub-commands.
def takes_commands?
@takes_commands
end
# :call-seq:
# command.options {|opts| ...} -> opts
# command.options -> opts
#
# Yields the OptionParser instance that is used for parsing the options of this command (if a
# block is given) and returns it.
def options #:yields: options
yield(@options) if block_given?
@options
end
# :call-seq:
# command.add_command(other_command, default: false) {|cmd| ... } -> command
# command.add_command('other', default: false) {|cmd| ...} -> command
#
# Adds a command to the command list.
#
# The argument +command+ can either be a Command object or a String in which case a new Command
# object is created. In both cases the Command object is yielded.
#
# If the optional argument +default+ is +true+, then the command is used when no other
# sub-command is specified on the command line.
#
# If this command takes no other commands, an error is raised.
def add_command(command, default: false) # :yields: command_object
raise TakesNoCommandError.new(name) unless takes_commands?
command = Command.new(command) if command.kind_of?(String)
command.super_command = self
@commands[command.name] = command
@default_command = command.name if default
command.fire_hook_after_add
yield(command) if block_given?
self
end
# :call-seq:
# command.command_chain -> [top_level_command, super_command, ..., command]
#
# Returns the command chain, i.e. a list containing this command and all of its super-commands,
# starting at the top level command.
def command_chain
cmds = []
cmd = self
while !cmd.nil? && !cmd.super_command.kind_of?(CommandParser)
cmds.unshift(cmd)
cmd = cmd.super_command
end
cmds
end
# Returns the associated CommandParser instance for this command or +nil+ if no command parser
# is associated.
def command_parser
cmd = super_command
cmd = cmd.super_command while !cmd.nil? && !cmd.kind_of?(CommandParser)
cmd
end
# Sets the given +block+ as the action block that is used on when executing this command.
#
# If a sub-class is created for specifying a command, then the #execute method should be
# overridden instead of setting an action block.
#
# See also: #execute
def action(&block)
@action = block
end
# Invokes the action block with the parsed arguments.
#
# This method is called by the CommandParser instance if this command was specified on the
# command line to be executed.
#
# Sub-classes can either specify an action block or directly override this method (the latter is
# preferred).
def execute(*args)
@action.call(*args)
end
# Sets the short description of the command if an argument is given. Always returns the short
# description.
#
# The short description is ideally shorter than 60 characters.
def short_desc(*val)
@short_desc = val[0] unless val.empty?
@short_desc
end
alias short_desc= short_desc
# Sets the detailed description of the command if an argument is given. Always returns the
# detailed description.
#
# This may be a single string or an array of strings for multiline description. Each string
# is ideally shorter than 76 characters.
def long_desc(*val)
@long_desc = val.flatten unless val.empty?
@long_desc
end
alias long_desc= long_desc
# :call-seq:
# cmd.argument_desc(name => desc, ...)
#
# Sets the descriptions for one or more arguments using name-description pairs.
#
# The used names should correspond to the names used in #usage_arguments.
def argument_desc(hash)
@argument_desc.update(hash)
end
# Returns the number of arguments required for the execution of the command, i.e. the number of
# arguments the #action block or the #execute method takes.
#
# If the returned number is negative, it means that the minimum number of arguments is -n-1.
#
# See: Method#arity, Proc#arity
def arity
(@action || method(:execute)).arity
end
# Returns +true+ if the command can take one or more arguments.
def takes_arguments?
arity.abs > 0
end
# Returns a string containing the help message for the command.
def help
output = ''
output << help_banner
output << help_short_desc
output << help_long_desc
output << help_commands
output << help_arguments
output << help_options('Options (take precedence over global options)', options)
output << help_options('Global Options', command_parser.global_options)
end
# Returns the banner (including the usage line) of the command.
#
# The usage line is command specific but the rest is the same for all commands and can be set
# via +command_parser.main_options.banner+.
def help_banner
output = ''
if command_parser.main_options.banner?
output << format(command_parser.main_options.banner, indent: 0) << "\n\n"
end
output << format(usage, indent: 7) << "\n\n"
end
# Returns the usage line for the command.
#
# The usage line is automatically generated from the available information. If this is not
# suitable, override this method to provide a command specific usage line.
#
# Typical usage lines looks like the following:
#
# Usage: program [options] command [options] {sub_command1 | sub_command2}
# Usage: program [options] command [options] ARG1 [ARG2] [REST...]
#
# See: #usage_options, #usage_arguments, #usage_commands
def usage
tmp = "Usage: #{command_parser.main_options.program_name}"
tmp << command_parser.main_command.usage_options
tmp << command_chain.map {|cmd| " #{cmd.name}#{cmd.usage_options}"}.join('')
if takes_commands?
tmp << " #{usage_commands}"
elsif takes_arguments?
tmp << " #{usage_arguments}"
end
tmp
end
# Returns a string describing the options of the command for use in the usage line.
#
# If there are any options, the resulting string also includes a leading space!
#
# A typical return value would look like the following:
#
# [options]
#
# See: #usage
def usage_options
(options.options_defined? ? ' [options]' : '')
end
# Returns a string describing the arguments for the command for use in the usage line.
#
# By default the names of the action block or #execute method arguments are used (done via
# Ruby's reflection API). If this is not wanted, override this method.
#
# A typical return value would look like the following:
#
# ARG1 [ARG2] [REST...]
#
# See: #usage, #argument_desc
def usage_arguments
(@action || method(:execute)).parameters.map do |type, name|
case type
when :req then name.to_s
when :opt then "[#{name}]"
when :rest then "[#{name}...]"
end
end.join(" ").upcase
end
# Returns a string describing the sub-commands of the commands for use in the usage line.
#
# Override this method for providing a command specific specialization.
#
# A typical return value would look like the following:
#
# {command | other_command | another_command }
def usage_commands
(commands.empty? ? '' : "{#{commands.keys.sort.join(" | ")}}")
end
# Returns the formatted short description.
#
# For the output format see #cond_format_help_section
def help_short_desc
cond_format_help_section("Summary", "#{name} - #{short_desc}",
condition: short_desc && !short_desc.empty?)
end
# Returns the formatted detailed description.
#
# For the output format see #cond_format_help_section
def help_long_desc
cond_format_help_section("Description", [long_desc].flatten,
condition: long_desc && !long_desc.empty?)
end
# Returns the formatted sub-commands of this command.
#
# For the output format see #cond_format_help_section
def help_commands
describe_commands = lambda do |command, level = 0|
command.commands.sort.collect do |name, cmd|
str = " " * level << name << (name == command.default_command ? " (*)" : '')
str = str.ljust(command_parser.help_desc_indent) << cmd.short_desc.to_s
str = format(str, width: command_parser.help_line_width - command_parser.help_indent,
indent: command_parser.help_desc_indent)
str << "\n" << (cmd.takes_commands? ? describe_commands.call(cmd, level + 1) : "")
end.join('')
end
cond_format_help_section("Available commands", describe_commands.call(self),
condition: takes_commands?, preformatted: true)
end
# Returns the formatted arguments of this command.
#
# For the output format see #cond_format_help_section
def help_arguments
desc = @argument_desc.map {|k, v| k.to_s.ljust(command_parser.help_desc_indent) << v.to_s}
cond_format_help_section('Arguments', desc, condition: !@argument_desc.empty?)
end
# Returns the formatted option descriptions for the given OptionParser instance.
#
# The section title needs to be specified with the +title+ argument.
#
# For the output format see #cond_format_help_section
def help_options(title, options)
summary = ''
summary_width = command_parser.main_options.summary_width
options.summarize([], summary_width, summary_width - 1, '') do |line|
summary << format(line, width: command_parser.help_line_width - command_parser.help_indent,
indent: summary_width + 1, indent_first_line: false) << "\n"
end
cond_format_help_section(title, summary, condition: !summary.empty?, preformatted: true)
end
# This hook method is called when the command (or one of its super-commands) is added to another
# Command instance that has an associated command parser (#see command_parser).
#
# It can be used, for example, to add global options.
def on_after_add
end
# For sorting commands by name.
def <=>(other)
name <=> other.name
end
protected
# Conditionally formats a help section.
#
# Returns either the formatted help section if the condition is +true+ or an empty string
# otherwise.
#
# The help section starts with a title and the given lines are indented to easily distinguish
# different sections.
#
# A typical help section would look like the following:
#
# Summary:
# help - Provide help for individual commands
#
# Options:
#
# condition:: The formatted help section is only returned if the condition is +true+.
#
# indent:: Whether the lines should be indented with CommandParser#help_indent spaces.
#
# preformatted:: Assume that the given lines are already correctly formatted and don't try to
# reformat them.
def cond_format_help_section(title, *lines, condition: true, indent: true, preformatted: false)
if condition
out = "#{title}:\n"
lines = lines.flatten.join("\n").split(/\n/)
if preformatted
lines.map! {|l| ' ' * command_parser.help_indent << l} if indent
out << lines.join("\n")
else
out << format(lines.join("\n"), indent: (indent ? command_parser.help_indent : 0), indent_first_line: true)
end
out << "\n\n"
else
''
end
end
# Returns the text in +content+ formatted so that no line is longer than +width+ characters.
#
# Options:
#
# width:: The maximum width of a line. If not specified, the CommandParser#help_line_width value
# is used.
#
# indent:: This option specifies the amount of spaces prepended to each line. If not specified,
# the CommandParser#help_indent value is used.
#
# indent_first_line:: If this option is +true+, then the first line is also indented.
def format(content, width: command_parser.help_line_width,
indent: command_parser.help_indent, indent_first_line: false)
content = (content || '').dup
line_length = width - indent
first_line_pattern = other_lines_pattern = /\A.{1,#{line_length}}\z|\A.{1,#{line_length}}[ \n]/m
(first_line_pattern = /\A.{1,#{width}}\z|\A.{1,#{width}}[ \n]/m) unless indent_first_line
pattern = first_line_pattern
content.split(/\n\n/).map do |paragraph|
lines = []
until paragraph.empty?
unless (str = paragraph.slice!(pattern)) and (str = str.sub(/[ \n]\z/, ''))
str = paragraph.slice!(0, line_length)
end
lines << (lines.empty? && !indent_first_line ? '' : ' ' * indent) + str.tr("\n", ' ')
pattern = other_lines_pattern
end
lines.join("\n")
end.join("\n\n")
end
def fire_hook_after_add #:nodoc:
return unless command_parser
@options.stack[0] = MultiList.new(command_parser.global_options.stack)
on_after_add
@commands.each_value {|cmd| cmd.fire_hook_after_add}
end
end
# The default help Command.
#
# It adds the options "-h" and "--help" to the CommandParser#global_options.
#
# When the command is specified on the command line (or one of the above mentioned options), it
# shows the main help or individual command help.
class HelpCommand < Command
def initialize #:nodoc:
super('help', takes_commands: false)
short_desc('Provide help for individual commands')
long_desc('This command prints the program help if no arguments are given. If one or ' \
'more command names are given as arguments, these arguments are interpreted ' \
'as a hierachy of commands and the help for the right most command is show.')
argument_desc(COMMAND: 'The name of a command or sub-command')
end
def on_after_add #:nodoc:
command_parser.global_options.on_tail("-h", "--help", "Show help") do
execute(*command_parser.current_command.command_chain.map(&:name))
exit
end
end
def usage_arguments #:nodoc:
"[COMMAND COMMAND...]"
end
def execute(*args) #:nodoc:
if !args.empty?
cmd = command_parser.main_command
arg = args.shift
while !arg.nil? && cmd.commands.key?(arg)
cmd = cmd.commands[arg]
arg = args.shift
end
if arg.nil?
puts cmd.help
else
raise InvalidArgumentError, args.unshift(arg).join(' ')
end
else
puts command_parser.main_command.help
end
end
end
# The default version command.
#
# It adds the options "-v" and "--version" to the CommandParser#main_options but this can be
# changed in ::new.
#
# When the command is specified on the command line (or one of the above mentioned options), it
# shows the version of the program configured by the settings
#
# * command_parser.main_options.program_name
# * command_parser.main_options.version
class VersionCommand < Command
# Create a new version command.
#
# Options:
#
# add_switches:: Specifies whether the '-v' and '--version' switches should be added to the
# CommandParser#main_options
def initialize(add_switches: true)
super('version', takes_commands: false)
short_desc("Show the version of the program")
@add_switches = add_switches
end
def on_after_add #:nodoc:
command_parser.main_options.on_tail("--version", "-v", "Show the version of the program") do
execute
end if @add_switches
end
def execute #:nodoc:
version = command_parser.main_options.version
version = version.join('.') if version.kind_of?(Array)
puts command_parser.main_options.banner + "\n" if command_parser.main_options.banner?
puts "#{command_parser.main_options.program_name} #{version}"
exit
end
end
# === 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.
class CommandParser
# The top level command representing the program itself.
attr_reader :main_command
# The command that is being executed. Only available during parsing of the command line
# arguments.
attr_reader :current_command
# 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.
attr_accessor :data
# Should exceptions be handled gracefully? I.e. by printing error message and the help screen?
#
# See ::new for possible values.
attr_reader :handle_exceptions
# The maximum width of the help lines.
attr_accessor :help_line_width
# The amount of spaces to indent the content of help sections.
attr_accessor :help_indent
# The indentation used for, among other things, command descriptions.
attr_accessor :help_desc_indent
# 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.
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
# :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.
def main_options
yield(@main_command.options) if block_given?
@main_command.options
end
# :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.
def global_options
yield(@global_options) if block_given?
@global_options
end
# Adds a top level command.
#
# See Command#add_command for detailed invocation information.
def add_command(*args, **kws, &block)
@main_command.add_command(*args, **kws, &block)
end
# 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.
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
end
end
|