File: cmdparse.rb

package info (click to toggle)
ruby-cmdparse 3.0.7-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 312 kB
  • sloc: ruby: 1,736; makefile: 11
file content (911 lines) | stat: -rw-r--r-- 31,814 bytes parent folder | download | duplicates (2)
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