options_by_example 3.2.0 → 3.4.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: b5533bd626a90e56af73f7fe32cbd2c0a79630da399242c98abce184ac084dd9
4
- data.tar.gz: 4583a92414c1e10544ffb42b4e45da0d1c0eac1adc1d38748bd31e24e791337e
3
+ metadata.gz: beeaa2ae22a04cfe49102108f616bad5d3fb63b00156bb34647774ba07c8def0
4
+ data.tar.gz: e2af434e1812a6244c276bea7b00b52b97411927ccc207e843f4866e64356d08
5
5
  SHA512:
6
- metadata.gz: cd7df1507d066462d1031d71dc054fa886764dddca9fe17b4d6f7310f9e17f75673a6a6709e6ef1636d863dd35a537918d9e6838a04981cd947174d402569480
7
- data.tar.gz: 9f11d4fdae24faefc1069435683e65c648635568d54d19e3e8197040b721db3630be4aa779781403d26b5c2e04dd23b3b6bcc0e3bef11d405855ac07e89a0630
6
+ metadata.gz: fca827e7190634ca8382ff90d074a276fd4367a609fad77b3d58a8488f85b97e3e8493fe2a717fd9a9aff3fefdde4315058c1512addaf4b1d7663f1a2dc201db
7
+ data.tar.gz: a2ce56d793153540e2c9c4959dc7af5aff0e73a4c32037a738a368f06128d4f35730411f8cce8875e5e1a3c153e6e4a4344de006171ca1b70d1b24737eeaf819
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'time'
5
+
6
+
7
+ class OptionsByExample
8
+
9
+ class PrintUsageMessage < StandardError
10
+ end
11
+
12
+ class CommandlineParser
13
+
14
+ attr_reader :option_values
15
+ attr_reader :argument_values
16
+
17
+ def initialize(usage)
18
+ @argument_names = usage.argument_names
19
+ @default_values = usage.default_values
20
+ @option_names = usage.option_names
21
+
22
+ @argument_values = @default_values.dup
23
+ @option_values = {}
24
+ end
25
+
26
+ def parse(array)
27
+
28
+ # Separate command-line options and their respective arguments into
29
+ # chunks, plus tracking leading excess arguments. This organization
30
+ # facilitates further processing and validation of the input.
31
+
32
+ @slices = []
33
+ @remainder = current = []
34
+ array.each do |each|
35
+ @slices << current = [] if each.start_with?(?-)
36
+ current << each
37
+ end
38
+
39
+ raise_if_help_option
40
+ unpack_combined_shorthand_options
41
+ expand_dash_number_to_dash_n_option
42
+ raise_if_unknown_options
43
+ parse_options
44
+ coerce_num_date_time_etc
45
+
46
+ validate_number_of_arguments
47
+ parse_required_arguments
48
+ parse_optional_arguments
49
+
50
+ raise "Internal error: unreachable state" unless @remainder.empty?
51
+ end
52
+
53
+ private
54
+
55
+ def raise_if_help_option
56
+ @slices.each do |option, *args|
57
+ case option
58
+ when '-h', '--help'
59
+ raise PrintUsageMessage
60
+ end
61
+ end
62
+ end
63
+
64
+ def unpack_combined_shorthand_options
65
+
66
+ # Expand any combined shorthand options like -svt into their
67
+ # separate components (-s, -v, and -t) and assigns any arguments
68
+ # to the last component. If an unknown shorthand is found, raise
69
+ # a helpful error message with suggestion if possible.
70
+
71
+ list = []
72
+ @slices.each do |option, *args|
73
+ if option =~ /^-([a-zA-Z]{2,})$/
74
+ shorthands = $1.each_char.map { |char| "-#{char}" }
75
+
76
+ shorthands.each do |each|
77
+ if not @option_names.include?(each)
78
+ did_you_mean = ", did you mean '-#{option}'?" if @option_names.include?("-#{option}")
79
+ raise "Found unknown option #{each} inside '#{option}'#{did_you_mean}"
80
+ end
81
+ end
82
+
83
+ list.concat shorthands.map { |each| [each] }
84
+ list.last.concat args
85
+ else
86
+ list << [option, *args]
87
+ end
88
+ end
89
+
90
+ @slices = list
91
+ end
92
+
93
+ def expand_dash_number_to_dash_n_option
94
+ @slices.each do |each|
95
+ if each.first =~ /^-(\d+)$/
96
+ each[0..0] = ['-n', $1]
97
+ end
98
+ end
99
+ end
100
+
101
+ def raise_if_unknown_options
102
+ @slices.each do |option, *args|
103
+ raise "Found unknown option '#{option}'" unless @option_names.include?(option)
104
+ end
105
+ end
106
+
107
+ def parse_options
108
+ @slices.each do |option, *args|
109
+ if @remainder.any?
110
+ raise "Unexpected arguments found before option '#{option}', please provide all options before arguments"
111
+ end
112
+
113
+ option_name, argument_name = @option_names[option]
114
+ @option_values[option_name] = true
115
+
116
+ if argument_name
117
+ raise "Expected argument for option '#{option}', got none" if args.empty?
118
+ @argument_values[option_name] = args.shift
119
+ @option_took_argument = option
120
+ else
121
+ @option_took_argument = nil
122
+ end
123
+
124
+ @remainder = args
125
+ end
126
+ end
127
+
128
+ def coerce_num_date_time_etc
129
+ @option_names.each do |option, (each, argument_name)|
130
+ next unless value = @argument_values[each]
131
+ begin
132
+ case argument_name
133
+ when 'NUM'
134
+ expected_type = 'an integer value'
135
+ @argument_values[each] = Integer value
136
+ when 'DATE'
137
+ expected_type = 'a date (e.g. YYYY-MM-DD)'
138
+ @argument_values[each] = Date.parse value
139
+ when 'TIME'
140
+ expected_type = 'a timestamp (e.g. HH:MM:SS)'
141
+ @argument_values[each] = Time.parse value
142
+ end
143
+ rescue ArgumentError
144
+ raise "Invalid argument \"#{value}\" for option '#{option}', please provide #{expected_type}"
145
+ end
146
+ end
147
+ end
148
+
149
+ def validate_number_of_arguments
150
+ count_optional_arguments = @argument_names.values.count(:optional)
151
+ count_required_arguments = @argument_names.values.count(:required)
152
+ count_vararg_arguments = @argument_names.values.count(:vararg)
153
+
154
+ min_length = count_required_arguments + count_vararg_arguments
155
+ max_length = count_required_arguments + count_optional_arguments
156
+
157
+ if @remainder.size > max_length && count_vararg_arguments == 0
158
+ range = [min_length, max_length].uniq.join(?-)
159
+ raise "Expected #{range} arguments, but received too many"
160
+ end
161
+
162
+ if @remainder.size < min_length
163
+ too_few = @remainder.empty? ? 'none' : (@remainder.size == 1 ? 'only one' : 'too few')
164
+ remark = " (considering #{@option_took_argument} takes an argument)" if @option_took_argument
165
+ raise "Expected #{min_length} required arguments, but received #{too_few}#{remark}"
166
+ end
167
+ end
168
+
169
+ def parse_required_arguments
170
+ if @argument_names.values.include?(:vararg)
171
+ remaining_arguments = @argument_names.length
172
+ @argument_names.each do |argument_name, arity|
173
+ raise "unreachable" if @remainder.empty?
174
+ remaining_arguments -= 1
175
+ case arity
176
+ when :required
177
+ @argument_values[argument_name] = @remainder.shift
178
+ when :vararg
179
+ @argument_values[argument_name] = @remainder.shift(@remainder.length - remaining_arguments)
180
+ else
181
+ raise "unreachable"
182
+ end
183
+ end
184
+ return
185
+ end
186
+
187
+ @argument_names.reverse_each do |argument_name, arity|
188
+ break if arity == :optional
189
+ raise "unreachable" if @remainder.empty?
190
+ @argument_values[argument_name] = @remainder.pop
191
+ end
192
+ end
193
+
194
+ def parse_optional_arguments
195
+ @argument_names.each do |argument_name, arity|
196
+ break unless arity == :optional
197
+ break if @remainder.empty?
198
+ @argument_values[argument_name] = @remainder.shift
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ class OptionsByExample
4
+
5
+ class UsageSpecification
6
+
7
+ attr_reader :message
8
+ attr_reader :argument_names
9
+ attr_reader :default_values
10
+ attr_reader :option_names
11
+
12
+ def initialize(text)
13
+ @message = text.gsub('$0', File.basename($0)).gsub(/\n+\Z/, "\n\n")
14
+
15
+ # --- 1) Parse argument names -------------------------------------
16
+ #
17
+ # Parse the usage string and extract both optional argument names
18
+ # and required argument names, for example:
19
+ #
20
+ # Usage: connect [options] [mode] host port
21
+
22
+ @argument_names = {}
23
+ inline_options = []
24
+
25
+ usage_line = text.lines.grep(/Usage:/).first
26
+ raise RuntimeError, "Expected usage string, got none" unless usage_line
27
+ tokens = usage_line.scan(/\[.*?\]|\w+ \.\.\.|\S+/)
28
+ raise unless tokens.shift == 'Usage:'
29
+ raise unless tokens.shift
30
+ tokens.shift if tokens.first == '[options]'
31
+
32
+ while /^\[(--?\w.*)\]$/ === tokens.first
33
+ inline_options << $1
34
+ tokens.shift
35
+ end
36
+
37
+ while /^\[(\w+)\]$/ === tokens.first
38
+ @argument_names[sanitize $1] = :optional
39
+ tokens.shift
40
+ end
41
+
42
+ while /^(\w+)( ?\.\.\.)?$/ === tokens.first
43
+ vararg_if_dotted = $2 ? :vararg : :required
44
+ @argument_names[sanitize $1] = vararg_if_dotted
45
+ tokens.shift
46
+ end
47
+
48
+ raise "Found invalid usage token '#{tokens.first}'" unless tokens.empty?
49
+
50
+ count_optional_arguments = @argument_names.values.count(:optional)
51
+ count_vararg_arguments = @argument_names.values.count(:vararg)
52
+
53
+ raise "Cannot combine dotted and optional arguments" if count_optional_arguments > 0 && count_vararg_arguments > 0
54
+ raise "Found more than one dotted arguments" if count_vararg_arguments > 1
55
+
56
+ # --- 2) Parse option names ---------------------------------------
57
+ #
58
+ # Parse the usage message and extract option names, their short and
59
+ # long forms, and the associated argument name (if any), eg:
60
+ #
61
+ # Options:
62
+ # -s, --secure Use secure connection
63
+ # -v, --verbose Enable verbose output
64
+ # -r, --retries NUM Number of connection retries (default 3)
65
+ # -t, --timeout NUM Set connection timeout in seconds
66
+
67
+ @option_names = {}
68
+ @default_values = {}
69
+
70
+ options = inline_options + text.lines.grep(/^\s*--?\w/)
71
+ options.each do |string|
72
+ tokens = string.scan(/--?\w[\w-]*(?: \w+)?|,|\(default \S+\)|\S+/)
73
+
74
+ short_form = nil
75
+ long_form = nil
76
+ option_name = nil
77
+ argument_name = nil
78
+ default_value = nil
79
+
80
+ if /^-(\w)( \w+)?$/ === tokens.first
81
+ short_form, argument_name = tokens.shift.split
82
+ option_name = sanitize $1
83
+ tokens.shift if ',' === tokens.first
84
+ end
85
+
86
+ if /^--([\w-]+)( \w+)?$/ === tokens.first
87
+ long_form, argument_name = tokens.shift.split
88
+ option_name = sanitize $1
89
+ end
90
+
91
+ if /^\(default (\S+)\)$/ === tokens.last
92
+ default_value = $1
93
+ end
94
+
95
+ [short_form, long_form].compact.each do |each|
96
+ @option_names[each] = [option_name, argument_name]
97
+ end
98
+
99
+ @default_values[option_name] = default_value if default_value
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def sanitize(string)
106
+ string.tr('^a-zA-Z0-9', '_').downcase.to_sym
107
+ end
108
+ end
109
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class OptionsByExample
4
- VERSION = '3.2.0'
4
+ VERSION = '3.4.0'
5
5
  end
6
6
 
7
7
 
@@ -11,6 +11,17 @@ __END__
11
11
  # Minor version bump when backward-compatible changes or enhancements
12
12
  # Patch version bump when backward-compatible bug fixes, security updates etc
13
13
 
14
+ 3.4.0
15
+ - Ensure default values are coerced too
16
+ - Print error message to stdout
17
+ - New method #expect_at_most_one_except (experimental)
18
+ - New method #expect_at_most_one_of (experimental)
19
+
20
+ 3.3.0
21
+ - Expand dash-number to dash-n option
22
+ - Complete support for inline specification of options
23
+ - Support repeated arguments using dot-dot-dot
24
+
14
25
  3.2.0
15
26
 
16
27
  - New method #get returns argument value or nil
@@ -1,52 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'options_by_example/version'
4
- require 'options_by_example/parser'
4
+ require 'options_by_example/commandline_parser'
5
+ require 'options_by_example/usage_specification'
5
6
 
6
7
 
7
8
  class OptionsByExample
8
9
 
9
10
  attr_reader :arguments
10
11
  attr_reader :options
11
- attr_reader :usage_message
12
+ attr_reader :usage_spec
12
13
 
13
14
  def self.read(data)
14
15
  return new data.read
15
16
  end
16
17
 
17
18
  def initialize(text)
18
- @usage_message = text.gsub('$0', File.basename($0)).gsub(/\n+\Z/, "\n\n")
19
-
20
- # --- 1) Parse argument names -------------------------------------
21
- #
22
- # Parse the usage string and extract both optional argument names
23
- # and required argument names, for example:
24
- #
25
- # Usage: connect [options] [mode] host port
26
-
27
- text =~ /Usage: (\$0|\w+)(?: \[options\])?((?: \[\w+\])*)((?: \w+)*)/
28
- raise RuntimeError, "Expected usage string, got none" unless $1
29
- @argument_names_optional = $2.to_s.split.map { |match| sanitize match.tr('[]', '') }
30
- @argument_names_required = $3.to_s.split.map { |match| sanitize match }
31
-
32
- # --- 2) Parse option names ---------------------------------------
33
- #
34
- # Parse the usage message and extract option names, their short and
35
- # long forms, and the associated argument name (if any), eg:
36
- #
37
- # Options:
38
- # -s, --secure Use secure connection
39
- # -v, --verbose Enable verbose output
40
- # -r, --retries NUM Number of connection retries (default 3)
41
- # -t, --timeout NUM Set connection timeout in seconds
42
-
43
- @option_names = {}
44
- @default_values = {}
45
- text.scan(/(?:(-\w), ?)?(--([\w-]+))(?: (\w+))?(?:.*\(default:? (\w+)\))?/) do
46
- flags = [$1, $2].compact
47
- flags.each { |each| @option_names[each] = [(sanitize $3), $4] }
48
- @default_values[sanitize $3] = $5 if $5
49
- end
19
+ @usage_spec = UsageSpecification.new(text)
50
20
 
51
21
  initialize_argument_accessors
52
22
  initialize_option_accessors
@@ -55,11 +25,21 @@ class OptionsByExample
55
25
  def parse(argv)
56
26
  parse_without_exit argv
57
27
  rescue PrintUsageMessage
58
- puts @usage_message
28
+ puts @usage_spec.message
59
29
  exit 0
60
30
  rescue RuntimeError => err
61
- puts "ERROR: #{err.message}"
62
- exit 1
31
+ abort "ERR: #{err.message}"
32
+ end
33
+
34
+ def expect_at_most_one_except(*extra_options)
35
+ expect_at_most_one_of *(@options.keys - extra_options)
36
+ end
37
+
38
+ def expect_at_most_one_of(*mutually_exclusive_options)
39
+ provided_options = @options.keys & mutually_exclusive_options
40
+ if provided_options.length > 1
41
+ abort "ERR: Found more than one mutually-exclusive option {#{provided_options.join ', '}}"
42
+ end
63
43
  end
64
44
 
65
45
  def fetch(*args, &block)
@@ -84,25 +64,19 @@ class OptionsByExample
84
64
  private
85
65
 
86
66
  def parse_without_exit(argv)
87
- parser = Parser.new(
88
- @argument_names_required,
89
- @argument_names_optional,
90
- @default_values,
91
- @option_names,
92
- )
67
+ parser = CommandlineParser.new(@usage_spec)
68
+ parser.parse(argv)
93
69
 
94
- parser.parse argv
95
- @arguments = parser.arguments
96
- @options = parser.options
70
+ @arguments = parser.argument_values
71
+ @options = parser.option_values
97
72
 
98
73
  return self
99
74
  end
100
75
 
101
76
  def initialize_argument_accessors
102
77
  [
103
- *@argument_names_required,
104
- *@argument_names_optional,
105
- *@option_names.values.select(&:last).map(&:first),
78
+ *@usage_spec.argument_names.keys,
79
+ *@usage_spec.option_names.values.select(&:last).map(&:first),
106
80
  ].each do |argument_name|
107
81
  instance_eval %{
108
82
  def argument_#{argument_name}
@@ -114,7 +88,7 @@ class OptionsByExample
114
88
  end
115
89
 
116
90
  def initialize_option_accessors
117
- @option_names.each_value do |option_name, _|
91
+ @usage_spec.option_names.each_value do |option_name, _|
118
92
  instance_eval %{
119
93
  def include_#{option_name}?
120
94
  @options.include? :#{option_name}
@@ -122,9 +96,5 @@ class OptionsByExample
122
96
  }
123
97
  end
124
98
  end
125
-
126
- def sanitize(string)
127
- string.tr('^a-zA-Z0-9', '_').downcase.to_sym
128
- end
129
99
  end
130
100
 
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: options_by_example
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.0
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Kuhn
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-06-17 00:00:00.000000000 Z
11
+ date: 2026-02-03 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description:
13
+ description:
14
14
  email:
15
15
  - akuhn@iam.unibe.ch
16
16
  executables: []
@@ -19,7 +19,8 @@ extra_rdoc_files: []
19
19
  files:
20
20
  - README.md
21
21
  - lib/options_by_example.rb
22
- - lib/options_by_example/parser.rb
22
+ - lib/options_by_example/commandline_parser.rb
23
+ - lib/options_by_example/usage_specification.rb
23
24
  - lib/options_by_example/version.rb
24
25
  homepage: https://github.com/akuhn/options_by_example
25
26
  licenses:
@@ -28,7 +29,7 @@ metadata:
28
29
  homepage_uri: https://github.com/akuhn/options_by_example
29
30
  source_code_uri: https://github.com/akuhn/options_by_example
30
31
  changelog_uri: https://github.com/akuhn/options_by_example/blob/master/lib/options_by_example/version.rb
31
- post_install_message:
32
+ post_install_message:
32
33
  rdoc_options: []
33
34
  require_paths:
34
35
  - lib
@@ -43,8 +44,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
43
44
  - !ruby/object:Gem::Version
44
45
  version: '0'
45
46
  requirements: []
46
- rubygems_version: 3.3.7
47
- signing_key:
47
+ rubygems_version: 3.0.3.1
48
+ signing_key:
48
49
  specification_version: 4
49
50
  summary: No-code options parser that extracts arguments directly from usage text.
50
51
  test_files: []
@@ -1,168 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'date'
4
- require 'time'
5
-
6
-
7
- class OptionsByExample
8
-
9
- class PrintUsageMessage < StandardError
10
- end
11
-
12
- class Parser
13
-
14
- attr_reader :options
15
- attr_reader :arguments
16
-
17
- def initialize(argument_names_required, argument_names_optional, default_values, option_names)
18
- @argument_names_required = argument_names_required
19
- @argument_names_optional = argument_names_optional
20
- @default_values = default_values
21
- @option_names = option_names
22
-
23
- @arguments = @default_values.dup
24
- @options = {}
25
- end
26
-
27
- def parse(array)
28
-
29
- # Separate command-line options and their respective arguments into
30
- # chunks, plus tracking leading excess arguments. This organization
31
- # facilitates further processing and validation of the input.
32
-
33
- @chunks = []
34
- @remainder = current = []
35
- array.each do |each|
36
- @chunks << current = [] if each.start_with?(?-)
37
- current << each
38
- end
39
-
40
- detect_help_option
41
- flatten_stacked_shorthand_options
42
- detect_unknown_options
43
- parse_options
44
-
45
- validate_number_of_arguments
46
- parse_required_arguments
47
- parse_optional_arguments
48
-
49
- raise "Internal error: unreachable state" unless @remainder.empty?
50
- end
51
-
52
- private
53
-
54
- def detect_help_option
55
- @chunks.each do |option, *args|
56
- case option
57
- when '-h', '--help'
58
- raise PrintUsageMessage
59
- end
60
- end
61
- end
62
-
63
- def flatten_stacked_shorthand_options
64
-
65
- # Expand any combined shorthand options like -svt into their
66
- # separate components (-s, -v, and -t) and assigns any arguments
67
- # to the last component. If an unknown shorthand is found, raise
68
- # a helpful error message with suggestion if possible.
69
-
70
- list = []
71
- @chunks.each do |option, *args|
72
- if option =~ /^-([a-zA-Z]{2,})$/
73
- shorthands = $1.each_char.map { |char| "-#{char}" }
74
-
75
- shorthands.each do |each|
76
- if not @option_names.include?(each)
77
- did_you_mean = ", did you mean '-#{option}'?" if @option_names.include?("-#{option}")
78
- raise "Found unknown option #{each} inside '#{option}'#{did_you_mean}"
79
- end
80
- end
81
-
82
- list.concat shorthands.map { |each| [each] }
83
- list.last.concat args
84
- else
85
- list << [option, *args]
86
- end
87
- end
88
-
89
- @chunks = list
90
- end
91
-
92
- def detect_unknown_options
93
- @chunks.each do |option, *args|
94
- raise "Found unknown option '#{option}'" unless @option_names.include?(option)
95
- end
96
- end
97
-
98
- def parse_options
99
- @chunks.each do |option, *args|
100
- if @remainder.any?
101
- raise "Unexpected arguments found before option '#{option}', please provide all options before arguments"
102
- end
103
-
104
- option_name, argument_name = @option_names[option]
105
- @options[option_name] = true
106
-
107
- if argument_name
108
- raise "Expected argument for option '#{option}', got none" if args.empty?
109
- value = args.shift
110
-
111
- begin
112
- case argument_name
113
- when 'NUM'
114
- expected_type = 'an integer value'
115
- value = Integer value
116
- when 'DATE'
117
- expected_type = 'a date (e.g. YYYY-MM-DD)'
118
- value = Date.parse value
119
- when 'TIME'
120
- expected_type = 'a timestamp (e.g. HH:MM:SS)'
121
- value = Time.parse value
122
- end
123
- rescue ArgumentError
124
- raise "Invalid argument \"#{value}\" for option '#{option}', please provide #{expected_type}"
125
- end
126
-
127
- @arguments[option_name] = value
128
- @option_took_argument = option
129
- else
130
- @option_took_argument = nil
131
- end
132
-
133
- @remainder = args
134
- end
135
- end
136
-
137
- def validate_number_of_arguments
138
- min_length = @argument_names_required.size
139
- max_length = @argument_names_optional.size + min_length
140
-
141
- if @remainder.size > max_length
142
- range = [min_length, max_length].uniq.join(?-)
143
- raise "Expected #{range} arguments, but received too many"
144
- end
145
-
146
- if @remainder.size < min_length
147
- too_few = @remainder.empty? ? 'none' : (@remainder.size == 1 ? 'only one' : 'too few')
148
- remark = " (considering #{@option_took_argument} takes an argument)" if @option_took_argument
149
- raise "Expected #{min_length} required arguments, but received #{too_few}#{remark}"
150
- end
151
- end
152
-
153
- def parse_required_arguments
154
- stash = @remainder.pop(@argument_names_required.length)
155
- @argument_names_required.each do |argument_name|
156
- raise "Missing required argument '#{argument_name}'" if stash.empty?
157
- @arguments[argument_name] = stash.shift
158
- end
159
- end
160
-
161
- def parse_optional_arguments
162
- @argument_names_optional.each do |argument_name|
163
- break if @remainder.empty?
164
- @arguments[argument_name] = @remainder.shift
165
- end
166
- end
167
- end
168
- end