options_by_example 3.3.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: 8ced46483d8abae62122bb45cd0d4753408c4d93e08405a3409583ec9fcfb477
4
- data.tar.gz: e8484baa8976c72154fb2da2509482d985501c08960e7e67fdf04731e307787b
3
+ metadata.gz: 27abecec1f4038208d60c4a623de4664241e4b88db67b8e15918ba8d2e8c4ad3
4
+ data.tar.gz: e57931971bc3f57ac7a59fd9233f338530afc60d2a1f507a9a2344e10d9f5ab9
5
5
  SHA512:
6
- metadata.gz: 22f691bf1fc36e14868996b32b15de17a1760add666aa046e6dc57fb0e09f8dad75c7c18260255930c88ea3e56a1c994285dd320a659e6c9ee92fc4ae45d3f10
7
- data.tar.gz: 1d54dca36e9829f2018af7493586e18ca7d590819d112e28482c93684212adc4ccd53e5c5183d49885c927f4ddb9e23c0654874ddbceb0e617f95c6fc6b96258
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,25 +37,34 @@ 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
43
44
  parse_options
45
+ coerce_num_date_time_etc
44
46
 
45
47
  validate_number_of_arguments
46
- parse_required_arguments
47
- parse_optional_arguments
48
+ parse_positional_arguments
49
+ special_case_if_ends_with_optional_vararg
48
50
 
49
- raise "Internal error: unreachable state" unless @remainder.empty?
51
+ # :nocov:
52
+ raise %{unreachable given we check number of arguments} unless @remainder.empty?
53
+ # :nocov:
50
54
  end
51
55
 
52
56
  private
53
57
 
54
- def raise_if_help_option
58
+ def exit_if_help_option
55
59
  @slices.each do |option, *args|
56
60
  case option
57
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
58
68
  raise PrintUsageMessage
59
69
  end
60
70
  end
@@ -114,25 +124,7 @@ class OptionsByExample
114
124
 
115
125
  if argument_name
116
126
  raise "Expected argument for option '#{option}', got none" if args.empty?
117
- value = args.shift
118
-
119
- begin
120
- case argument_name
121
- when 'NUM'
122
- expected_type = 'an integer value'
123
- value = Integer value
124
- when 'DATE'
125
- expected_type = 'a date (e.g. YYYY-MM-DD)'
126
- value = Date.parse value
127
- when 'TIME'
128
- expected_type = 'a timestamp (e.g. HH:MM:SS)'
129
- value = Time.parse value
130
- end
131
- rescue ArgumentError
132
- raise "Invalid argument \"#{value}\" for option '#{option}', please provide #{expected_type}"
133
- end
134
-
135
- @argument_values[option_name] = value
127
+ @argument_values[option_name] = args.shift
136
128
  @option_took_argument = option
137
129
  else
138
130
  @option_took_argument = nil
@@ -142,57 +134,99 @@ class OptionsByExample
142
134
  end
143
135
  end
144
136
 
137
+ def coerce_num_date_time_etc
138
+ @option_names.each do |option, (each, argument_name)|
139
+ next unless value = @argument_values[each]
140
+ begin
141
+ case argument_name
142
+ when 'NUM'
143
+ expected_type = 'an integer value'
144
+ @argument_values[each] = Integer value
145
+ when 'DATE'
146
+ expected_type = 'a date (e.g. YYYY-MM-DD)'
147
+ @argument_values[each] = Date.parse value
148
+ when 'TIME'
149
+ expected_type = 'a timestamp (e.g. HH:MM:SS)'
150
+ @argument_values[each] = Time.parse value
151
+ end
152
+ rescue ArgumentError
153
+ raise "Invalid argument \"#{value}\" for option '#{option}', please provide #{expected_type}"
154
+ end
155
+ end
156
+ end
157
+
145
158
  def validate_number_of_arguments
146
- count_optional_arguments = @argument_names.values.count(:optional)
147
- count_required_arguments = @argument_names.values.count(:required)
148
- 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
149
161
 
150
- min_length = count_required_arguments + count_vararg_arguments
151
- 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)
152
165
 
153
- if @remainder.size > max_length && count_vararg_arguments == 0
154
- range = [min_length, max_length].uniq.join(?-)
155
- raise "Expected #{range} arguments, but received too many"
156
- 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
157
170
 
158
- if @remainder.size < min_length
159
- too_few = @remainder.empty? ? 'none' : (@remainder.size == 1 ? 'only one' : 'too few')
160
- remark = " (considering #{@option_took_argument} takes an argument)" if @option_took_argument
161
- raise "Expected #{min_length} required arguments, but received #{too_few}#{remark}"
162
- end
163
- end
171
+ unless (min_length..max_length) === @remainder.size
164
172
 
165
- def parse_required_arguments
166
- if @argument_names.values.include?(:vararg)
167
- remaining_arguments = @argument_names.length
168
- @argument_names.each do |argument_name, arity|
169
- raise "unreachable" if @remainder.empty?
170
- remaining_arguments -= 1
171
- case arity
172
- when :required
173
- @argument_values[argument_name] = @remainder.shift
174
- when :vararg
175
- @argument_values[argument_name] = @remainder.shift(@remainder.length - remaining_arguments)
176
- else
177
- raise "unreachable"
178
- 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:
179
193
  end
180
- return
181
- end
182
194
 
183
- @argument_names.reverse_each do |argument_name, arity|
184
- break if arity == :optional
185
- raise "unreachable" if @remainder.empty?
186
- @argument_values[argument_name] = @remainder.pop
195
+ if @option_took_argument
196
+ msg += " (considering #{@option_took_argument} takes an argument)"
197
+ end
198
+
199
+ raise msg
187
200
  end
188
201
  end
189
202
 
190
- def parse_optional_arguments
203
+ def parse_positional_arguments
204
+ remaining_arguments = @argument_names.length
191
205
  @argument_names.each do |argument_name, arity|
192
- break unless arity == :optional
193
- break if @remainder.empty?
194
- @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
195
220
  end
196
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
197
231
  end
198
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.3.0'
4
+ VERSION = '4.0.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
+ 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
+
19
+ 3.4.0
20
+ - Ensure default values are coerced too
21
+ - Print error message to stdout
22
+ - New method #expect_at_most_one_except (experimental)
23
+ - New method #expect_at_most_one_of (experimental)
24
+
14
25
  3.3.0
15
26
  - Expand dash-number to dash-n option
16
27
  - Complete support for inline specification of options
@@ -28,8 +28,18 @@ class OptionsByExample
28
28
  puts @usage_spec.message
29
29
  exit 0
30
30
  rescue RuntimeError => err
31
- puts "ERROR: #{err.message}"
32
- 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
33
43
  end
34
44
 
35
45
  def fetch(*args, &block)
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.3.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-01 00:00:00.000000000 Z
11
+ date: 2026-03-16 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: