options_by_example 3.4.0 → 4.0.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: beeaa2ae22a04cfe49102108f616bad5d3fb63b00156bb34647774ba07c8def0
4
- data.tar.gz: e2af434e1812a6244c276bea7b00b52b97411927ccc207e843f4866e64356d08
3
+ metadata.gz: 27abecec1f4038208d60c4a623de4664241e4b88db67b8e15918ba8d2e8c4ad3
4
+ data.tar.gz: e57931971bc3f57ac7a59fd9233f338530afc60d2a1f507a9a2344e10d9f5ab9
5
5
  SHA512:
6
- metadata.gz: fca827e7190634ca8382ff90d074a276fd4367a609fad77b3d58a8488f85b97e3e8493fe2a717fd9a9aff3fefdde4315058c1512addaf4b1d7663f1a2dc201db
7
- data.tar.gz: a2ce56d793153540e2c9c4959dc7af5aff0e73a4c32037a738a368f06128d4f35730411f8cce8875e5e1a3c153e6e4a4344de006171ca1b70d1b24737eeaf819
6
+ metadata.gz: 7513e6747f9269e9618e4a4e277560fcbb7741663cc01a3c5767b0a30c52277cd67590c19b21e2eb79d42bc6678ad329d465b1d0b75f2da6079d12cc9978fe52
7
+ data.tar.gz: 1a4bfc340b15a0f63115e9025e4be96f37fa0d5a0808bbe81d44438c97648159154b41dc8380b94fe8aeb1a4ab5d5d7b6448572e6741ecf8a5e8bdf3d75166a3
data/README.md CHANGED
@@ -45,7 +45,7 @@ __END__
45
45
  Establishes a network connection to a designated host and port, enabling
46
46
  users to assess network connectivity and diagnose potential problems.
47
47
 
48
- Usage: connect [options] [mode] host port
48
+ Usage: connect [options] host port [mode]
49
49
 
50
50
  Options:
51
51
  -s, --secure Establish a secure connection (SSL/TSL)
@@ -54,8 +54,8 @@ Options:
54
54
  -t, --timeout NUM Set connection timeout in seconds
55
55
 
56
56
  Arguments:
57
- [mode] Optional connection mode (active or passive)
58
57
  host The target host to connect to (e.g., example.com)
59
58
  port The target port to connect to (e.g., 80)
59
+ [mode] Optional connection mode (active or passive)
60
60
  ```
61
61
 
@@ -17,6 +17,7 @@ class OptionsByExample
17
17
  def initialize(usage)
18
18
  @argument_names = usage.argument_names
19
19
  @default_values = usage.default_values
20
+ @ends_with_optional_vararg = usage.ends_with_optional_vararg
20
21
  @option_names = usage.option_names
21
22
 
22
23
  @argument_values = @default_values.dup
@@ -36,7 +37,7 @@ class OptionsByExample
36
37
  current << each
37
38
  end
38
39
 
39
- raise_if_help_option
40
+ exit_if_help_option
40
41
  unpack_combined_shorthand_options
41
42
  expand_dash_number_to_dash_n_option
42
43
  raise_if_unknown_options
@@ -44,18 +45,26 @@ class OptionsByExample
44
45
  coerce_num_date_time_etc
45
46
 
46
47
  validate_number_of_arguments
47
- parse_required_arguments
48
- parse_optional_arguments
48
+ parse_positional_arguments
49
+ special_case_if_ends_with_optional_vararg
49
50
 
50
- raise "Internal error: unreachable state" unless @remainder.empty?
51
+ # :nocov:
52
+ raise %{unreachable given we check number of arguments} unless @remainder.empty?
53
+ # :nocov:
51
54
  end
52
55
 
53
56
  private
54
57
 
55
- def raise_if_help_option
58
+ def exit_if_help_option
56
59
  @slices.each do |option, *args|
57
60
  case option
58
61
  when '-h', '--help'
62
+ if args.first == 'debug!'
63
+ puts "@argument_names = #{@argument_names.inspect}"
64
+ puts "@default_values = #{@default_values.inspect}"
65
+ puts "@ends_with_optional_vararg = #{@ends_with_optional_vararg}"
66
+ puts "@option_names = #{@option_names.inspect}"
67
+ end
59
68
  raise PrintUsageMessage
60
69
  end
61
70
  end
@@ -147,56 +156,77 @@ class OptionsByExample
147
156
  end
148
157
 
149
158
  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)
159
+ # ASSUME: either varargs or optional arguments, never both. That
160
+ # constraint is guaranteed upstream. Here, we just count
153
161
 
154
- min_length = count_required_arguments + count_vararg_arguments
155
- max_length = count_required_arguments + count_optional_arguments
162
+ count_required = @argument_names.values.count(:required)
163
+ count_vararg = @argument_names.values.count(:vararg)
164
+ count_optional = @argument_names.values.count(:optional)
156
165
 
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
166
+ min_length = count_required + count_vararg
167
+ max_length = count_required + count_optional
168
+ max_length = nil if @ends_with_optional_vararg
169
+ max_length = nil if count_vararg > 0
161
170
 
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
171
+ unless (min_length..max_length) === @remainder.size
168
172
 
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
173
+ if max_length.nil?
174
+ msg = "Expected #{min_length} or more arguments,"
175
+ elsif max_length > min_length
176
+ msg = "Expected #{min_length}-#{max_length} arguments,"
177
+ else
178
+ msg = "Expected #{min_length} arguments,"
179
+ end
180
+
181
+ if @remainder.empty?
182
+ msg += " but received none"
183
+ elsif @remainder.size == 1 && min_length > 1
184
+ msg += " but received only one"
185
+ elsif @remainder.size < min_length
186
+ msg += " but received too few"
187
+ elsif max_length && @remainder.size > max_length
188
+ msg += " but received too many"
189
+ # :nocov:
190
+ else
191
+ raise %{unreachable given the range check above}
192
+ # :nocov:
193
+ end
194
+
195
+ if @option_took_argument
196
+ msg += " (considering #{@option_took_argument} takes an argument)"
183
197
  end
184
- return
185
- end
186
198
 
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
199
+ raise msg
191
200
  end
192
201
  end
193
202
 
194
- def parse_optional_arguments
203
+ def parse_positional_arguments
204
+ remaining_arguments = @argument_names.length
195
205
  @argument_names.each do |argument_name, arity|
196
- break unless arity == :optional
197
- break if @remainder.empty?
198
- @argument_values[argument_name] = @remainder.shift
206
+ remaining_arguments -= 1
207
+ case arity
208
+ when :required
209
+ @argument_values[argument_name] = @remainder.shift
210
+ when :vararg
211
+ @argument_values[argument_name] = @remainder.shift(@remainder.length - remaining_arguments)
212
+ when :optional
213
+ break if @remainder.empty?
214
+ @argument_values[argument_name] = @remainder.shift
215
+ # :nocov:
216
+ else
217
+ raise %{unreachable given these are all possible values}
218
+ # :nocov:
219
+ end
199
220
  end
200
221
  end
222
+
223
+ def special_case_if_ends_with_optional_vararg
224
+ return unless @ends_with_optional_vararg
225
+ final_argument_name = @argument_names.keys.last
226
+ @argument_values[final_argument_name] = [
227
+ *@argument_values[final_argument_name],
228
+ *@remainder.shift(@remainder.length),
229
+ ]
230
+ end
201
231
  end
202
232
  end
@@ -7,6 +7,7 @@ class OptionsByExample
7
7
  attr_reader :message
8
8
  attr_reader :argument_names
9
9
  attr_reader :default_values
10
+ attr_reader :ends_with_optional_vararg
10
11
  attr_reader :option_names
11
12
 
12
13
  def initialize(text)
@@ -34,14 +35,20 @@ class OptionsByExample
34
35
  tokens.shift
35
36
  end
36
37
 
38
+ while /^(\w+)( ?\.\.\.)?$/ === tokens.first
39
+ vararg_if_dotted = $2 ? :vararg : :required
40
+ @argument_names[sanitize $1] = vararg_if_dotted
41
+ tokens.shift
42
+ end
43
+
37
44
  while /^\[(\w+)\]$/ === tokens.first
38
45
  @argument_names[sanitize $1] = :optional
39
46
  tokens.shift
40
47
  end
41
48
 
42
- while /^(\w+)( ?\.\.\.)?$/ === tokens.first
43
- vararg_if_dotted = $2 ? :vararg : :required
44
- @argument_names[sanitize $1] = vararg_if_dotted
49
+ if /^\[(\w+) ?\.\.\.\]$/ === tokens.first
50
+ @argument_names[sanitize $1] = :optional
51
+ @ends_with_optional_vararg = true
45
52
  tokens.shift
46
53
  end
47
54
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class OptionsByExample
4
- VERSION = '3.4.0'
4
+ VERSION = '4.0.0'
5
5
  end
6
6
 
7
7
 
@@ -11,6 +11,11 @@ __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
+ 4.0.0
15
+ - Remove support for leading optional arguments (breaking change)
16
+ - Add support for trailing optional arguments
17
+ - Add support for optional vararg argument
18
+
14
19
  3.4.0
15
20
  - Ensure default values are coerced too
16
21
  - Print error message to stdout
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: options_by_example
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.4.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Kuhn
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-03 00:00:00.000000000 Z
11
+ date: 2026-03-16 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: