eac_cli 0.8.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b936a1bc75006a7969bd549ed89750bf7b09ccebf5d77e35a7b2443ad7175b5
4
- data.tar.gz: 812fb927a263d8d6aa74600631db16eb07e626217ec1e81614bdf5f1031d7ef7
3
+ metadata.gz: 22f7790dfca37c3414a3552155806d47808bdee4b6eb6630a07b2eb8b928db59
4
+ data.tar.gz: 8b6826e7f1f7d4980516ad8d53d2b681890b326f513df49f6394c3f2115f3b64
5
5
  SHA512:
6
- metadata.gz: 4b2331aa69dd79cf797ab64305f3b088ee012cfe53e71ef0d0f00ec1f84ed2f5bc849e70a9745e17e43d591fc7e02ae9b061870b4d2b27cb93c51385c12780f8
7
- data.tar.gz: a62b07e41f454aa85015be5b39a3e427ab41b2afffdacfbf998ce1a261a49bf82c0711e95f7bb42b449fb3b41178dee666124a76eeaeee993c9fe6fdc7a6baf5
6
+ metadata.gz: 61d6bf83f3e147b7fd9761f6369347c91c208599eacf7ca0a5762e671e86a7eea30758b6101c639a4775acb376988e0ecd3d8dd31d1950fcebeaabc1b8275360
7
+ data.tar.gz: 66777c73453633a5af6fe8048818d7e07428dd958676b2d9a1cdc800c286910a73140e3a0526cb0ef5e87102a227be0873fcd14146405c33b73e644351e0070f
@@ -8,57 +8,66 @@ require 'eac_ruby_utils/core_ext'
8
8
  module EacCli
9
9
  class Definition
10
10
  require_sub __FILE__
11
+
12
+ MAIN_ALTERNATIVE_KEY = :main
13
+ SUBCOMMAND_NAME_ARG = 'subcommand'
14
+ SUBCOMMAND_ARGS_ARG = 'subcommand_args'
15
+
11
16
  attr_accessor :description
12
- attr_accessor :options_argument
13
17
 
14
18
  def initialize
15
19
  self.description = '-- NO DESCRIPTION SET --'
16
- self.options_argument = true
20
+ alternatives_set[MAIN_ALTERNATIVE_KEY] = main_alternative
17
21
  end
18
22
 
19
23
  def alt(&block)
20
- r = ::EacCli::Definition.new
24
+ r = ::EacCli::Definition::Alternative.new
21
25
  r.instance_eval(&block)
22
- alternatives << r
26
+ alternatives_set[new_alternative_key] = r
23
27
  r
24
28
  end
25
29
 
26
30
  def alternatives
27
- @alternatives ||= []
28
- end
29
-
30
- def arg_opt(short, long, description, option_options = {})
31
- options << ::EacCli::Definition::ArgumentOption.new(
32
- short, long, description, option_options
33
- )
31
+ alternatives_set.values
34
32
  end
35
33
 
36
- def bool_opt(short, long, description, option_options = {})
37
- options << ::EacCli::Definition::BooleanOption.new(short, long, description, option_options)
34
+ def alternative(key)
35
+ alternatives_set.fetch(key)
38
36
  end
39
37
 
40
38
  def desc(description)
41
39
  self.description = description
42
40
  end
43
41
 
42
+ def help_formatter
43
+ @help_formatter ||= ::EacCli::Definition::HelpFormatter.new(self)
44
+ end
45
+
46
+ def main_alternative
47
+ @main_alternative ||= begin
48
+ r = ::EacCli::Definition::Alternative.new
49
+ r.options_argument(true)
50
+ r
51
+ end
52
+ end
53
+
44
54
  def options_arg(options_argument)
45
55
  self.options_argument = options_argument
46
56
  end
47
57
 
48
- def options
49
- @options ||= []
58
+ def options_argument
59
+ main_alternative.options_argument?
50
60
  end
51
61
 
52
- def pos_arg(name, arg_options = {})
53
- positional << ::EacCli::Definition::PositionalArgument.new(name, arg_options)
62
+ def options_argument=(enable)
63
+ main_alternative.options_argument(enable)
54
64
  end
55
65
 
56
- def positional
57
- @positional ||= []
58
- end
66
+ delegate :arg_opt, :bool_opt, :options, :pos_arg,
67
+ :positional, :subcommands, to: :main_alternative
59
68
 
60
- def subcommands
61
- positional << ::EacCli::Definition::PositionalArgument.new('subcommand', subcommand: true)
69
+ def subcommands?
70
+ alternatives.any?(&:subcommands?)
62
71
  end
63
72
 
64
73
  def options_first(enable = true)
@@ -68,5 +77,23 @@ module EacCli
68
77
  def options_first?
69
78
  @options_first ? true : false
70
79
  end
80
+
81
+ private
82
+
83
+ def alternatives_set
84
+ @alternatives_set ||= ::ActiveSupport::HashWithIndifferentAccess.new
85
+ end
86
+
87
+ def new_alternative_key
88
+ @last_key ||= 0
89
+ loop do
90
+ @last_key += 1
91
+ break @last_key unless alternatives_set.key?(@last_key)
92
+ end
93
+ end
94
+
95
+ def pos_set
96
+ @pos_set ||= []
97
+ end
71
98
  end
72
99
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eac_cli/definition/argument_option'
4
+ require 'eac_cli/definition/boolean_option'
5
+ require 'eac_cli/definition/positional_argument'
6
+
7
+ module EacCli
8
+ class Definition
9
+ class Alternative
10
+ SUBCOMMAND_NAME_ARG = :subcommand
11
+ SUBCOMMAND_ARGS_ARG = :subcommand_args
12
+
13
+ def arg_opt(short, long, description, option_options = {})
14
+ options_set << ::EacCli::Definition::ArgumentOption.new(
15
+ short, long, description, option_options
16
+ )
17
+ end
18
+
19
+ def bool_opt(short, long, description, option_options = {})
20
+ options_set << ::EacCli::Definition::BooleanOption.new(short, long, description,
21
+ option_options)
22
+ end
23
+
24
+ def options
25
+ options_set.to_a
26
+ end
27
+
28
+ def options_argument?
29
+ @options_argument ? true : false
30
+ end
31
+
32
+ def options_argument(enable)
33
+ @options_argument = enable
34
+
35
+ self
36
+ end
37
+
38
+ def pos_arg(name, arg_options = {})
39
+ new_pos_arg = ::EacCli::Definition::PositionalArgument.new(name, arg_options)
40
+ check_positional_blocked(new_pos_arg)
41
+ pos_set << new_pos_arg
42
+ end
43
+
44
+ def positional
45
+ pos_set.to_a
46
+ end
47
+
48
+ def positional_arguments_blocked?(new_pos_arg)
49
+ last = pos_set.last
50
+ return false unless last
51
+ return true if subcommands?
52
+ return true if last.repeat?
53
+ return true if last.optional? && new_pos_arg.if_present(&:required?)
54
+
55
+ false
56
+ end
57
+
58
+ def subcommands
59
+ pos_arg(SUBCOMMAND_NAME_ARG, subcommand: true)
60
+ pos_set << ::EacCli::Definition::PositionalArgument.new(SUBCOMMAND_ARGS_ARG,
61
+ optional: true, repeat: true)
62
+ end
63
+
64
+ def subcommands?
65
+ pos_set.any?(&:subcommand?)
66
+ end
67
+
68
+ private
69
+
70
+ def check_positional_blocked(new_pos_arg)
71
+ raise 'Positional arguments are blocked' if positional_arguments_blocked?(new_pos_arg)
72
+ end
73
+
74
+ def pos_set
75
+ @pos_set ||= []
76
+ end
77
+
78
+ def options_set
79
+ @options_set ||= []
80
+ end
81
+ end
82
+ end
83
+ end
@@ -5,19 +5,35 @@ require 'eac_ruby_utils/core_ext'
5
5
  module EacCli
6
6
  class Definition
7
7
  class BaseOption
8
+ DEFAULT_REQUIRED = false
9
+
10
+ enable_listable
11
+ lists.add_symbol :option, :optional, :usage, :required
8
12
  attr_reader :short, :long, :description, :options
9
13
 
10
14
  def initialize(short, long, description, options = {})
11
15
  @short = short
12
16
  @long = long
13
17
  @description = description
14
- @options = options.with_indifferent_access
18
+ @options = options.symbolize_keys
19
+ @options.assert_valid_keys(::EacCli::Definition::BaseOption.lists.option.values)
15
20
  end
16
21
 
17
22
  def identifier
18
23
  long.to_s.variableize.to_sym
19
24
  end
20
25
 
26
+ def required?
27
+ return true if options.key?(:required) && options.fetch(:required)
28
+ return false if options.key?(:optional) && options.fetch(:optional)
29
+
30
+ DEFAULT_REQUIRED
31
+ end
32
+
33
+ def to_s
34
+ "#{self.class.name.demodulize}[#{identifier}]"
35
+ end
36
+
21
37
  def show_on_usage?
22
38
  options[:usage]
23
39
  end
@@ -1,49 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'eac_ruby_utils/core_ext'
4
- require 'optparse'
5
4
 
6
5
  module EacCli
7
- class Parser
8
- class OptionsCollection
6
+ class Definition
7
+ class HelpFormatter
9
8
  SEP = ' '
10
9
  IDENT = SEP * 2
11
10
  OPTION_DESC_SEP = IDENT * 2
12
11
 
13
- enable_simple_cache
14
- common_constructor(:definition, :argv, :collector) { collect }
15
- attr_reader :arguments
16
-
17
- private
18
-
19
- def collect
20
- build_banner
21
- build_options
22
- @arguments = option_parser.parse(argv)
23
- end
24
-
25
- def option_parser_uncached
26
- ::OptionParser.new
27
- end
28
-
29
- def build_banner
30
- option_parser.banner = "#{definition.description}\n\n#{section('usage')}"
31
- end
32
-
33
- def build_options
34
- definition.options.each do |option|
35
- build_option(option)
12
+ class << self
13
+ def option_long(option)
14
+ b = option.long
15
+ b += '=VALUE' if option.argument?
16
+ b
36
17
  end
37
- end
38
18
 
39
- def build_option(option)
40
- option_parser.on(
41
- *[option_short(option), option_long(option), option.description].reject(&:blank?)
42
- ) do |value|
43
- collector.collect(option, value)
19
+ def option_short(option)
20
+ b = option.short
21
+ b += 'VALUE' if option.argument?
22
+ b
44
23
  end
45
24
  end
46
25
 
26
+ common_constructor :definition
27
+
47
28
  def positional_argument(positional)
48
29
  if positional.subcommand?
49
30
  ::EacRubyUtils::Console::DocoptRunner::SUBCOMMANDS_MACRO
@@ -55,22 +36,6 @@ module EacCli
55
36
  end
56
37
  end
57
38
 
58
- def option_argument(option)
59
- option_long(option)
60
- end
61
-
62
- def option_long(option)
63
- b = option.long
64
- b += '=VALUE' if option.argument?
65
- b
66
- end
67
-
68
- def option_short(option)
69
- b = option.short
70
- b += 'VALUE' if option.argument?
71
- b
72
- end
73
-
74
39
  def section(header, include_header = true)
75
40
  b = include_header ? "#{header.humanize}:\n" : ''
76
41
  b += send("self_#{header}") + "\n"
@@ -94,12 +59,18 @@ module EacCli
94
59
  end
95
60
 
96
61
  def self_usage_arguments_options
97
- definition.options.select(&:show_on_usage?).map { |option| option_argument(option) }
62
+ definition.options.select(&:show_on_usage?).map do |option|
63
+ self.class.option_long(option)
64
+ end
98
65
  end
99
66
 
100
67
  def self_usage_arguments_positional
101
68
  definition.positional.map { |p| positional_argument(p) }
102
69
  end
70
+
71
+ def to_banner
72
+ "#{definition.description}\n\n#{section('usage')}"
73
+ end
103
74
  end
104
75
  end
105
76
  end
@@ -5,22 +5,39 @@ require 'eac_ruby_utils/core_ext'
5
5
  module EacCli
6
6
  class Definition
7
7
  class PositionalArgument
8
- common_constructor :name, :options, default: [{}]
8
+ DEFAULT_REQUIRED = true
9
+
10
+ enable_listable
11
+ lists.add_symbol :option, :optional, :repeat, :required, :subcommand
12
+ common_constructor :name, :options, default: [{}] do
13
+ options.assert_valid_keys(self.class.lists.option.values)
14
+ end
9
15
 
10
16
  def identifier
11
17
  name.to_s.variableize.to_sym
12
18
  end
13
19
 
14
20
  def optional?
15
- options[:optional]
21
+ !required?
16
22
  end
17
23
 
18
24
  def repeat?
19
- options[:repeat]
25
+ options[OPTION_REPEAT]
26
+ end
27
+
28
+ def required?
29
+ return true if options.key?(OPTION_REQUIRED) && options.fetch(OPTION_REQUIRED)
30
+ return false if options.key?(OPTION_OPTIONAL) && options.fetch(OPTION_OPTIONAL)
31
+
32
+ DEFAULT_REQUIRED
20
33
  end
21
34
 
22
35
  def subcommand?
23
- options[:subcommand]
36
+ options[OPTION_SUBCOMMAND]
37
+ end
38
+
39
+ def to_s
40
+ "#{self.class.name.demodulize}[#{identifier}]"
24
41
  end
25
42
  end
26
43
  end
@@ -6,71 +6,49 @@ require 'eac_ruby_utils/console/docopt_runner'
6
6
  module EacCli
7
7
  module Docopt
8
8
  class DocBuilder
9
+ require_sub __FILE__
9
10
  common_constructor :definition
10
11
 
11
12
  SEP = ' '
12
13
  IDENT = SEP * 2
13
14
  OPTION_DESC_SEP = IDENT * 2
14
15
 
15
- def positional_argument(positional)
16
- if positional.subcommand?
17
- ::EacRubyUtils::Console::DocoptRunner::SUBCOMMANDS_MACRO
18
- else
19
- r = "<#{positional.name}>"
20
- r += '...' if positional.repeat?
21
- r = "[#{r}]" if positional.optional?
22
- r
16
+ class << self
17
+ def option_long(option)
18
+ b = option.long
19
+ b += '=<value>' if option.argument?
20
+ b
23
21
  end
24
22
  end
25
23
 
26
- def option_argument(option)
27
- option_long(option)
28
- end
29
-
30
24
  def option_definition(option)
31
- option.short + SEP + option_long(option) + OPTION_DESC_SEP + option.description
32
- end
33
-
34
- def option_long(option)
35
- b = option.long
36
- b += '=<value>' if option.argument?
37
- b
25
+ option.short + SEP + self.class.option_long(option) + OPTION_DESC_SEP + option.description
38
26
  end
39
27
 
40
28
  def section(header, include_header = true)
41
29
  b = include_header ? "#{header.humanize}:\n" : ''
42
30
  b += send("self_#{header}") + "\n"
43
31
  definition.alternatives.each do |alternative|
44
- b += self.class.new(alternative).section(header, false)
32
+ b += IDENT + ::EacCli::Docopt::DocBuilder::Alternative.new(alternative).to_s + "\n"
45
33
  end
46
34
  b
47
35
  end
48
36
 
49
- def self_options
50
- definition.options.map { |option| IDENT + option_definition(option) }.join("\n")
51
- end
52
-
53
- def self_usage
54
- IDENT + self_usage_arguments.join(SEP)
55
- end
56
-
57
- def self_usage_arguments
58
- [::EacRubyUtils::Console::DocoptRunner::PROGRAM_MACRO] +
59
- definition.options_argument.if_present([]) { |_v| ['[options]'] } +
60
- self_usage_arguments_options +
61
- self_usage_arguments_positional
62
- end
63
-
64
- def self_usage_arguments_options
65
- definition.options.select(&:show_on_usage?).map { |option| option_argument(option) }
37
+ def options_section
38
+ "Options:\n" +
39
+ definition.alternatives.flat_map(&:options)
40
+ .map { |option| IDENT + option_definition(option) + "\n" }.join
66
41
  end
67
42
 
68
- def self_usage_arguments_positional
69
- definition.positional.map { |p| positional_argument(p) }
43
+ def usage_section
44
+ "Usage:\n" +
45
+ definition.alternatives.map do |alternative|
46
+ IDENT + ::EacCli::Docopt::DocBuilder::Alternative.new(alternative).to_s + "\n"
47
+ end.join
70
48
  end
71
49
 
72
50
  def to_s
73
- "#{definition.description}\n\n#{section('usage')}\n#{section('options')}\n"
51
+ "#{definition.description}\n\n#{usage_section}\n#{options_section}\n"
74
52
  end
75
53
  end
76
54
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eac_ruby_utils/core_ext'
4
+ require 'eac_ruby_utils/console/docopt_runner'
5
+
6
+ module EacCli
7
+ module Docopt
8
+ class DocBuilder
9
+ class Alternative
10
+ common_constructor :alternative
11
+
12
+ def to_s
13
+ (
14
+ [::EacRubyUtils::Console::DocoptRunner::PROGRAM_MACRO] +
15
+ alternative.options_argument?.if_present([]) { |_v| ['[options]'] } +
16
+ options +
17
+ positionals
18
+ ).join(::EacCli::Docopt::DocBuilder::SEP)
19
+ end
20
+
21
+ def options
22
+ alternative.options.select(&:show_on_usage?).map do |option|
23
+ ::EacCli::Docopt::DocBuilder.option_long(option)
24
+ end
25
+ end
26
+
27
+ def option_argument(option)
28
+ b = option.long
29
+ b += '=<value>' if option.argument?
30
+ b
31
+ end
32
+
33
+ def positionals
34
+ alternative.positional.map { |p| positional(p) }
35
+ end
36
+
37
+ def positional(positional)
38
+ if positional.subcommand?
39
+ ::EacRubyUtils::Console::DocoptRunner::SUBCOMMANDS_MACRO
40
+ else
41
+ r = "<#{positional.name}>"
42
+ r += '...' if positional.repeat?
43
+ r = "[#{r}]" if positional.optional?
44
+ r
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'eac_cli/docopt/doc_builder'
4
+ require 'eac_cli/runner'
4
5
  require 'eac_ruby_utils/console/docopt_runner'
5
6
 
6
7
  module EacCli
@@ -5,10 +5,28 @@ require 'eac_ruby_utils/core_ext'
5
5
  module EacCli
6
6
  class Parser
7
7
  require_sub __FILE__
8
- common_constructor :definition
8
+ enable_simple_cache
9
+ common_constructor :definition, :argv
9
10
 
10
- def parse(argv)
11
- ::EacCli::Parser::ParseResult.new(definition, argv).result
11
+ private
12
+
13
+ def parsed_uncached
14
+ raise 'Definition has no alternatives' if alternatives.empty?
15
+
16
+ alternatives.each do |alt_parser|
17
+ return alt_parser.parsed unless alt_parser.error?
18
+ end
19
+
20
+ raise first_error
21
+ end
22
+
23
+ def alternatives_uncached
24
+ definition.alternatives
25
+ .map { |alternative| ::EacCli::Parser::Alternative.new(alternative, argv) }
26
+ end
27
+
28
+ def first_error_uncached
29
+ alternatives.lazy.select(&:error?).map(&:error).first
12
30
  end
13
31
  end
14
32
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eac_cli/parser/collector'
4
+ require 'eac_cli/parser/error'
5
+ require 'eac_ruby_utils/core_ext'
6
+
7
+ module EacCli
8
+ class Parser
9
+ class Alternative
10
+ require_sub __FILE__, include_modules: true
11
+ enable_listable
12
+ lists.add_symbol :phase, :any, :option_argument, :positional
13
+ attr_reader :error
14
+
15
+ common_constructor :alternative, :argv do
16
+ alternative.assert_argument(::EacCli::Definition::Alternative, :alternative)
17
+ self.phase = PHASE_ANY
18
+ collect
19
+ end
20
+
21
+ def error?
22
+ error.present?
23
+ end
24
+
25
+ def parsed
26
+ @parsed ||= collector.to_data.freeze
27
+ end
28
+
29
+ private
30
+
31
+ attr_accessor :phase
32
+
33
+ def any_collect_argv_value
34
+ if argv_current_option?
35
+ option_collect_argv_value
36
+ else
37
+ positional_collect_argv_value
38
+ end
39
+ end
40
+
41
+ def collector
42
+ @collector ||= ::EacCli::Parser::Collector.new(alternative)
43
+ end
44
+
45
+ def collect
46
+ loop do
47
+ break unless argv_pending?
48
+
49
+ collect_argv_value
50
+ end
51
+ validate
52
+ rescue ::EacCli::Parser::Error => e
53
+ @error = e
54
+ end
55
+
56
+ def collect_argv_value
57
+ send("#{phase}_collect_argv_value")
58
+ argv_enum.next
59
+ end
60
+
61
+ def collect_option_argv_value
62
+ alternative.options.each do |option|
63
+ end
64
+
65
+ raise ::EacCli::Parser::Error.new(
66
+ alternative, argv, "Invalid option: #{argv_enum.current}"
67
+ )
68
+ end
69
+
70
+ def raise_error(message)
71
+ raise ::EacCli::Parser::Error.new(alternative, argv, message)
72
+ end
73
+
74
+ def validate
75
+ (alternative.options + alternative.positional).each do |option|
76
+ validate_option(option)
77
+ end
78
+ end
79
+
80
+ def validate_option(option)
81
+ return unless option.required?
82
+ return if collector.supplied?(option)
83
+
84
+ raise_error("Required option/positional #{option} not supplied")
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EacCli
4
+ class Parser
5
+ class Alternative
6
+ module Argv
7
+ def argv_enum
8
+ @argv_enum ||= argv.each
9
+ end
10
+
11
+ def argv_pending?
12
+ argv_enum.ongoing?
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EacCli
4
+ class Parser
5
+ class Alternative
6
+ module DoubleDash
7
+ DOUBLE_DASH = '--'
8
+
9
+ private
10
+
11
+ attr_accessor :double_dash
12
+
13
+ def argv_current_double_dash?
14
+ argv_enum.peek == DOUBLE_DASH && !double_dash
15
+ end
16
+
17
+ def double_dash_collect_argv_value
18
+ self.phase = PHASE_POSITIONAL
19
+ self.double_dash = true
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EacCli
4
+ class Parser
5
+ class Alternative
6
+ module Options
7
+ DOUBLE_DASH = '--'
8
+
9
+ private
10
+
11
+ attr_accessor :argument_option, :double_dash
12
+
13
+ def argument_option_collect_argv(option)
14
+ self.argument_option = option
15
+ self.phase = PHASE_OPTION_ARGUMENT
16
+ end
17
+
18
+ def argv_current_option?
19
+ phase == PHASE_ANY && argv_enum.peek.start_with?('-')
20
+ end
21
+
22
+ def argv_current_double_dash?
23
+ argv_enum.peek == DOUBLE_DASH && !double_dash
24
+ end
25
+
26
+ def boolean_option_collect_argv(option)
27
+ collector.collect(option, true)
28
+ end
29
+
30
+ def option_argument_collect_argv_value
31
+ collector.collect(argument_option, argv_enum.peek)
32
+ self.argument_option = nil
33
+ self.phase = PHASE_ANY
34
+ end
35
+
36
+ def option_collect_argv_value
37
+ return double_dash_collect_argv_value if argv_current_double_dash?
38
+
39
+ alternative.options.any? do |option|
40
+ next false unless [option.short, option.long].include?(argv_enum.peek)
41
+
42
+ if option.argument?
43
+ argument_option_collect_argv(option)
44
+ else
45
+ boolean_option_collect_argv(option)
46
+ end
47
+
48
+ true
49
+ end || raise_argv_current_invalid_option
50
+ end
51
+
52
+ def raise_argv_current_invalid_option
53
+ raise_error "Invalid option: \"#{argv_enum.peek}\""
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EacCli
4
+ class Parser
5
+ class Alternative
6
+ module Positionals
7
+ private
8
+
9
+ def positional_collect_argv_value
10
+ positional_check
11
+ collector.collect(positional_enum.peek, argv_enum.peek)
12
+ positional_next
13
+ end
14
+
15
+ def positional_enum
16
+ @positional_enum ||= alternative.positional.each
17
+ end
18
+
19
+ def positional_check
20
+ raise_error("Invalid positional: #{argv_enum.peek}") if positional_enum.stopped?
21
+ end
22
+
23
+ def positional_next
24
+ self.phase = PHASE_POSITIONAL if positional_enum.peek.subcommand?
25
+ positional_enum.next unless positional_enum.peek.repeat?
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -31,6 +31,10 @@ module EacCli
31
31
  end
32
32
  end
33
33
 
34
+ def supplied?(option)
35
+ data[option].present?
36
+ end
37
+
34
38
  private
35
39
 
36
40
  def data
@@ -5,13 +5,14 @@ require 'eac_cli/runner'
5
5
  require 'eac_cli/runner_with'
6
6
 
7
7
  class Object
8
- def runner_with(*runners)
8
+ def runner_with(*runners, &block)
9
9
  include ::EacCli::Runner
10
10
  enable_simple_cache
11
11
  enable_console_speaker
12
12
  runners.each do |runner|
13
13
  include runner_with_to_module(runner)
14
14
  end
15
+ runner_definition(&block) if block
15
16
  end
16
17
 
17
18
  private
@@ -18,6 +18,10 @@ module EacCli
18
18
  end
19
19
  end
20
20
 
21
+ def runner?(object)
22
+ object.is_a?(::Class) && object.included_modules.include?(::EacCli::Runner)
23
+ end
24
+
21
25
  private
22
26
 
23
27
  def alias_class_method(klass, from, to)
@@ -45,7 +49,7 @@ module EacCli
45
49
  module AfterClassMethods
46
50
  def create(*runner_context_args)
47
51
  r = new
48
- r.runner_context = ::EacCli::Runner::Context.new(*runner_context_args)
52
+ r.runner_context = ::EacCli::Runner::Context.new(r, *runner_context_args)
49
53
  r
50
54
  end
51
55
 
@@ -80,7 +84,7 @@ module EacCli
80
84
  end
81
85
 
82
86
  def parsed
83
- @parsed ||= ::EacCli::Parser.new(self.class.runner_definition).parse(runner_context.argv)
87
+ @parsed ||= ::EacCli::Parser.new(self.class.runner_definition, runner_context.argv).parsed
84
88
  end
85
89
  end
86
90
  end
@@ -5,13 +5,30 @@ require 'eac_ruby_utils/core_ext'
5
5
  module EacCli
6
6
  module Runner
7
7
  class Context
8
- attr_reader :argv, :parent, :program_name
8
+ attr_reader :argv, :parent, :program_name, :runner
9
9
 
10
- def initialize(*context_args)
10
+ def initialize(runner, *context_args)
11
11
  options = context_args.extract_options!
12
12
  @argv = (context_args[0] || options.delete(:argv) || ARGV).dup.freeze
13
13
  @parent = context_args[1] || options.delete(:parent)
14
14
  @program_name = options.delete(:program_name)
15
+ @runner = runner
16
+ end
17
+
18
+ # Call a method in the runner or in one of it ancestors.
19
+ def call(method_name, *args)
20
+ return runner.send(method_name, *args) if runner.respond_to?(method_name)
21
+ return parent_call(method_name, *args) if parent.present?
22
+
23
+ raise ::NameError, "No method \"#{method_name}\" found in #{runner} or in its ancestors"
24
+ end
25
+
26
+ protected
27
+
28
+ def parent_call(method_name, *args)
29
+ return parent.context(method_name, *args) if parent.respond_to?(:context)
30
+
31
+ parent.runner_context.call(method_name, *args)
15
32
  end
16
33
  end
17
34
  end
@@ -10,7 +10,7 @@ module EacCli
10
10
  include ::EacCli::Runner
11
11
 
12
12
  runner_definition.alt do
13
- options_arg false
13
+ options_argument false
14
14
  bool_opt '-h', '--help', 'Show help.', usage: true
15
15
  end
16
16
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'eac_cli/runner'
4
+ require 'eac_ruby_utils/core_ext'
5
+
6
+ module EacCli
7
+ module RunnerWith
8
+ module Subcommands
9
+ common_concern do
10
+ include ::EacCli::Runner
11
+ end
12
+
13
+ EXTRA_AVAILABLE_SUBCOMMANDS_METHOD_NAME = :extra_available_subcommands
14
+
15
+ def available_subcommands
16
+ @available_subcommands ||= available_subcommands_auto.merge(available_subcommands_extra)
17
+ end
18
+
19
+ def available_subcommands_auto
20
+ self.class.constants
21
+ .map { |name| [name.to_s.underscore.gsub('_', '-'), self.class.const_get(name)] }
22
+ .select { |c| ::EacCli::Runner.runner?(c[1]) }
23
+ .to_h.with_indifferent_access
24
+ end
25
+
26
+ def available_subcommands_extra
27
+ if respond_to?(EXTRA_AVAILABLE_SUBCOMMANDS_METHOD_NAME, true)
28
+ send(EXTRA_AVAILABLE_SUBCOMMANDS_METHOD_NAME)
29
+ else
30
+ {}
31
+ end
32
+ end
33
+
34
+ def method_missing(method_name, *arguments, &block)
35
+ return run_with_subcommand(*arguments, &block) if
36
+ run_with_subcommand_alias_run?(method_name)
37
+
38
+ super
39
+ end
40
+
41
+ def respond_to_missing?(method_name, include_private = false)
42
+ run_with_subcommand_alias_run?(method_name) || super
43
+ end
44
+
45
+ def run_with_subcommand
46
+ if subcommand_name
47
+ subcommand_runner.run
48
+ else
49
+ run_without_subcommand
50
+ end
51
+ end
52
+
53
+ def run_with_subcommand_alias_run?(method_name)
54
+ subcommands? && method_name.to_sym == :run
55
+ end
56
+
57
+ def run_without_subcommand
58
+ "Method #{__method__} should be overrided in #{self.class.name}"
59
+ end
60
+
61
+ def subcommands?
62
+ self.class.runner_definition.subcommands?
63
+ end
64
+
65
+ def subcommand_args
66
+ parsed.fetch(::EacCli::Definition::SUBCOMMAND_ARGS_ARG)
67
+ end
68
+
69
+ def subcommand_class
70
+ available_subcommands[subcommand_name].if_present { |v| return v }
71
+
72
+ raise(::EacCli::Parser::Error.new(
73
+ self.class.runner_definition, runner_context.argv,
74
+ "Subcommand \"#{subcommand_name}\" not found " \
75
+ "(Available: #{available_subcommands.keys}"
76
+ ))
77
+ end
78
+
79
+ def subcommand_name
80
+ parsed.fetch(::EacCli::Definition::SUBCOMMAND_NAME_ARG)
81
+ end
82
+
83
+ def subcommand_program
84
+ subcommand_name
85
+ end
86
+
87
+ def subcommand_runner
88
+ @subcommand_runner ||= subcommand_class.create(
89
+ argv: subcommand_args,
90
+ program_name: subcommand_program,
91
+ parent: self
92
+ )
93
+ end
94
+ end
95
+ end
96
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EacCli
4
- VERSION = '0.8.0'
4
+ VERSION = '0.12.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eac_cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esquilo Azul Company
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-27 00:00:00.000000000 Z
11
+ date: 2020-11-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: eac_ruby_utils
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.45'
19
+ version: '0.50'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0.45'
26
+ version: '0.50'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: eac_ruby_gem_support
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -55,18 +55,23 @@ files:
55
55
  - lib/eac_cli/core_ext.rb
56
56
  - lib/eac_cli/default_runner.rb
57
57
  - lib/eac_cli/definition.rb
58
+ - lib/eac_cli/definition/alternative.rb
58
59
  - lib/eac_cli/definition/argument_option.rb
59
60
  - lib/eac_cli/definition/base_option.rb
60
61
  - lib/eac_cli/definition/boolean_option.rb
62
+ - lib/eac_cli/definition/help_formatter.rb
61
63
  - lib/eac_cli/definition/positional_argument.rb
62
64
  - lib/eac_cli/docopt/doc_builder.rb
65
+ - lib/eac_cli/docopt/doc_builder/alternative.rb
63
66
  - lib/eac_cli/docopt/runner_extension.rb
64
67
  - lib/eac_cli/parser.rb
68
+ - lib/eac_cli/parser/alternative.rb
69
+ - lib/eac_cli/parser/alternative/argv.rb
70
+ - lib/eac_cli/parser/alternative/double_dash.rb
71
+ - lib/eac_cli/parser/alternative/options.rb
72
+ - lib/eac_cli/parser/alternative/positionals.rb
65
73
  - lib/eac_cli/parser/collector.rb
66
74
  - lib/eac_cli/parser/error.rb
67
- - lib/eac_cli/parser/options_collection.rb
68
- - lib/eac_cli/parser/parse_result.rb
69
- - lib/eac_cli/parser/positional_collection.rb
70
75
  - lib/eac_cli/patches.rb
71
76
  - lib/eac_cli/patches/object.rb
72
77
  - lib/eac_cli/patches/object/runner_with.rb
@@ -75,6 +80,7 @@ files:
75
80
  - lib/eac_cli/runner_with.rb
76
81
  - lib/eac_cli/runner_with/help.rb
77
82
  - lib/eac_cli/runner_with/output_file.rb
83
+ - lib/eac_cli/runner_with/subcommands.rb
78
84
  - lib/eac_cli/version.rb
79
85
  homepage:
80
86
  licenses: []
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'eac_ruby_utils/core_ext'
4
-
5
- module EacCli
6
- class Parser
7
- class ParseResult
8
- common_constructor :definition, :argv
9
-
10
- def result
11
- ::EacCli::Parser::Collector.to_data(definition) do |collector|
12
- ::EacCli::Parser::PositionalCollection.new(
13
- definition,
14
- ::EacCli::Parser::OptionsCollection.new(definition, argv, collector).arguments,
15
- collector
16
- )
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'eac_ruby_utils/core_ext'
4
- require 'eac_cli/parser/error'
5
-
6
- module EacCli
7
- class Parser
8
- class PositionalCollection
9
- common_constructor(:definition, :argv, :collector) { collect }
10
-
11
- private
12
-
13
- def collected
14
- @collected ||= ::Set.new
15
- end
16
-
17
- def collect
18
- argv.each { |argv_value| colect_argv_value(argv_value) }
19
- return unless pending_required_positional?
20
-
21
- raise ::EacCli::Parser::Error.new(
22
- definition, argv, 'No value for required positional ' \
23
- "\"#{current_positional.identifier}\""
24
- )
25
- end
26
-
27
- def colect_argv_value(argv_value)
28
- collector.collect(current_positional, argv_value)
29
- collected << current_positional
30
- positional_enumerator.next unless current_positional.repeat?
31
- end
32
-
33
- def pending_required_positional?
34
- !(current_positional.blank? || current_positional.optional? ||
35
- collected.include?(current_positional))
36
- end
37
-
38
- def positional_enumerator
39
- @positional_enumerator ||= definition.positional.each
40
- end
41
-
42
- def current_positional
43
- positional_enumerator.peek
44
- rescue ::StopIteration
45
- nil
46
- end
47
- end
48
- end
49
- end