options_by_example 4.0.0 → 4.1.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: 27abecec1f4038208d60c4a623de4664241e4b88db67b8e15918ba8d2e8c4ad3
4
- data.tar.gz: e57931971bc3f57ac7a59fd9233f338530afc60d2a1f507a9a2344e10d9f5ab9
3
+ metadata.gz: 3f146461611879c6934270f692e97391830d93f634b9300f7fa2883195bc0a22
4
+ data.tar.gz: 19ad57569defbb334404085af3248675cfa84000511bbf49b2803b5c6cb38dee
5
5
  SHA512:
6
- metadata.gz: 7513e6747f9269e9618e4a4e277560fcbb7741663cc01a3c5767b0a30c52277cd67590c19b21e2eb79d42bc6678ad329d465b1d0b75f2da6079d12cc9978fe52
7
- data.tar.gz: 1a4bfc340b15a0f63115e9025e4be96f37fa0d5a0808bbe81d44438c97648159154b41dc8380b94fe8aeb1a4ab5d5d7b6448572e6741ecf8a5e8bdf3d75166a3
6
+ metadata.gz: 01146df8d0c06df84d2c093d0493219d6bd9d78ab5174f089b6b13b79b5fbb355ebd16f8bd5e3b65358670b44f6479e032e8967e57df52935328e5f151bfb4a1
7
+ data.tar.gz: fbfe47bf367329b63e3661c28b964118c9dd47e013dabcfabdf6ceae9bdad3a2af9cd0998a852d5e3ede7a22b07146d64ae85fb61727aac54e1ced139075f918
data/README.md CHANGED
@@ -1,61 +1,48 @@
1
1
  # Options by Example
2
2
 
3
- No-code options parser that automatically detects command-line options from the usage text of your application. This intuitive parser identifies optional and required argument names as well as option names without requiring any additional code, making it easy to manage user input for your command-line applications.
3
+ No-code options parser that automatically detects command-line options from the usage text.
4
4
 
5
5
  Features
6
6
 
7
- - Automatically detects optional and required argument names from usage text
8
- - Automatically detects option names and associated arguments (if any) from usage text
7
+ - Automatically infers options and argument names from usage text
9
8
  - Parses those arguments and options from the command line (ARGV)
10
9
  - Raises errors for unknown options or missing required arguments
10
+ - Supports typed arguments, eg `--lines NUM` or `--since DATE`
11
11
 
12
- Installation
12
+ Example
13
13
 
14
- To use options_by_example, first install the gem by running:
14
+ ```ruby
15
+ require %(options_by_example)
15
16
 
16
- ```
17
- gem install options_by_example
18
- ```
17
+ flags = OptionsByExample.read(DATA).parse(ARGV)
19
18
 
20
- Alternatively, add this line to your Gemfile and run bundle install:
19
+ puts 'Feeling verbose today' if flags.include?(:verbose)
20
+ puts flags.get(:words).sample(flags.get(:num))
21
21
 
22
- ```
23
- gem 'options_by_example'
24
- ```
25
-
26
- Example
22
+ __END__
23
+ Choose at random from a list of provided words.
27
24
 
28
- ```ruby
29
- require 'options_by_example'
25
+ Usage: random.rb [options] words ...
30
26
 
31
- Options = OptionsByExample.read(DATA).parse(ARGV)
27
+ Options:
28
+ -n, --num NUM Number of choices (default 1)
29
+ --verbose Enable verbose mode
30
+ ```
32
31
 
33
- puts Options.include? :secure
34
- puts Options.include? :verbose
35
- puts Options.include? :retries
36
- puts Options.include? :timeout
37
- puts Options.get :retries
38
- puts Options.get :timeout
39
- puts Options.get :mode
40
- puts Options.get :host
41
- puts Options.get :port
32
+ And then call the program with eg
42
33
 
34
+ ruby random.rb -n 2 foo bar qux
43
35
 
44
- __END__
45
- Establishes a network connection to a designated host and port, enabling
46
- users to assess network connectivity and diagnose potential problems.
36
+ ### Installation
47
37
 
48
- Usage: connect [options] host port [mode]
38
+ To use options_by_example, first install the gem by running:
49
39
 
50
- Options:
51
- -s, --secure Establish a secure connection (SSL/TSL)
52
- -v, --verbose Enable verbose output for detailed information
53
- -r, --retries NUM Number of connection retries (default 3)
54
- -t, --timeout NUM Set connection timeout in seconds
55
-
56
- Arguments:
57
- host The target host to connect to (e.g., example.com)
58
- port The target port to connect to (e.g., 80)
59
- [mode] Optional connection mode (active or passive)
60
40
  ```
41
+ gem install options_by_example
42
+ ```
43
+
44
+ Alternatively, add this line to your Gemfile and run bundle install:
61
45
 
46
+ ```
47
+ gem 'options_by_example'
48
+ ```
@@ -16,37 +16,26 @@ class OptionsByExample
16
16
 
17
17
  def initialize(usage)
18
18
  @argument_names = usage.argument_names
19
- @default_values = usage.default_values
20
- @ends_with_optional_vararg = usage.ends_with_optional_vararg
21
19
  @option_names = usage.option_names
22
20
 
23
- @argument_values = @default_values.dup
21
+ @argument_values = {}
24
22
  @option_values = {}
25
23
  end
26
24
 
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
- @slices = []
34
- @remainder = current = []
35
- array.each do |each|
36
- @slices << current = [] if each.start_with?(?-)
37
- current << each
38
- end
25
+ def parse(argv)
26
+ @slices = argv.slice_before(/^-/).entries
27
+ treat_everything_after_double_dash_as_positionals
39
28
 
40
29
  exit_if_help_option
41
30
  unpack_combined_shorthand_options
42
31
  expand_dash_number_to_dash_n_option
43
32
  raise_if_unknown_options
44
- parse_options
33
+
34
+ @remainder = parse_options_and_return_remainder
45
35
  coerce_num_date_time_etc
46
36
 
47
37
  validate_number_of_arguments
48
38
  parse_positional_arguments
49
- special_case_if_ends_with_optional_vararg
50
39
 
51
40
  # :nocov:
52
41
  raise %{unreachable given we check number of arguments} unless @remainder.empty?
@@ -55,14 +44,17 @@ class OptionsByExample
55
44
 
56
45
  private
57
46
 
47
+ def treat_everything_after_double_dash_as_positionals
48
+ index = @slices.index { |head,| head == '--' }
49
+ @slices[index..] = [@slices.drop(index).flatten] if index
50
+ end
51
+
58
52
  def exit_if_help_option
59
53
  @slices.each do |option, *args|
60
54
  case option
61
55
  when '-h', '--help'
62
56
  if args.first == 'debug!'
63
57
  puts "@argument_names = #{@argument_names.inspect}"
64
- puts "@default_values = #{@default_values.inspect}"
65
- puts "@ends_with_optional_vararg = #{@ends_with_optional_vararg}"
66
58
  puts "@option_names = #{@option_names.inspect}"
67
59
  end
68
60
  raise PrintUsageMessage
@@ -108,50 +100,68 @@ class OptionsByExample
108
100
  end
109
101
 
110
102
  def raise_if_unknown_options
111
- @slices.each do |option, *args|
112
- raise "Found unknown option '#{option}'" unless @option_names.include?(option)
103
+ @slices.each do |option,|
104
+ if option =~ /^--?\w/ and not @option_names.include?(option)
105
+ raise "Found unknown option '#{option}'"
106
+ end
113
107
  end
114
108
  end
115
109
 
116
- def parse_options
117
- @slices.each do |option, *args|
118
- if @remainder.any?
119
- raise "Unexpected arguments found before option '#{option}', please provide all options before arguments"
110
+ def parse_options_and_return_remainder
111
+ pending = @slices.dup
112
+
113
+ until pending.empty?
114
+ current = pending.first
115
+
116
+ unless current.first =~ /^--?\w/
117
+ current.shift if current.first == '--' # consume double-dash
118
+ return current if pending.length == 1
119
+ raise "Unexpected arguments found before option '#{pending[1].first}', please provide all options before arguments"
120
120
  end
121
121
 
122
- option_name, argument_name = @option_names[option]
122
+ option = current.shift # consume the option/flag
123
+ option_name, has_argument, _ = @option_names[option]
123
124
  @option_values[option_name] = true
124
125
 
125
- if argument_name
126
- raise "Expected argument for option '#{option}', got none" if args.empty?
127
- @argument_values[option_name] = args.shift
126
+ if has_argument
127
+ raise "Expected argument for option '#{option}', got none" unless current.first
128
+ @argument_values[option_name] = current.shift # consume the argument
128
129
  @option_took_argument = option
129
130
  else
130
131
  @option_took_argument = nil
131
132
  end
132
133
 
133
- @remainder = args
134
+ pending.shift if current.empty?
134
135
  end
136
+
137
+ return []
135
138
  end
136
139
 
137
140
  def coerce_num_date_time_etc
138
- @option_names.each do |option, (each, argument_name)|
139
- next unless value = @argument_values[each]
141
+ @option_names.each do |option, (each, argument_name, default_value)|
142
+ value = @argument_values.fetch(each, default_value)
143
+ next unless value
144
+
140
145
  begin
141
146
  case argument_name
142
147
  when 'NUM'
143
148
  expected_type = 'an integer value'
144
- @argument_values[each] = Integer value
149
+ value = Integer value
150
+ when 'FLOAT'
151
+ expected_type = 'a floating-point value'
152
+ value = Float value
145
153
  when 'DATE'
146
154
  expected_type = 'a date (e.g. YYYY-MM-DD)'
147
- @argument_values[each] = Date.parse value
155
+ value = Date.parse value
148
156
  when 'TIME'
149
157
  expected_type = 'a timestamp (e.g. HH:MM:SS)'
150
- @argument_values[each] = Time.parse value
158
+ value = Time.parse value
151
159
  end
152
160
  rescue ArgumentError
153
161
  raise "Invalid argument \"#{value}\" for option '#{option}', please provide #{expected_type}"
154
162
  end
163
+
164
+ @argument_values[each] = value
155
165
  end
156
166
  end
157
167
 
@@ -159,14 +169,9 @@ class OptionsByExample
159
169
  # ASSUME: either varargs or optional arguments, never both. That
160
170
  # constraint is guaranteed upstream. Here, we just count
161
171
 
162
- count_required = @argument_names.values.count(:required)
163
- count_vararg = @argument_names.values.count(:vararg)
164
- count_optional = @argument_names.values.count(:optional)
165
-
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
172
+ min_length = count_arguments(:required) + count_arguments(:vararg)
173
+ max_length = count_arguments(:required) + count_arguments(/optional/)
174
+ max_length = nil if count_arguments(/vararg/) > 0
170
175
 
171
176
  unless (min_length..max_length) === @remainder.size
172
177
 
@@ -207,10 +212,10 @@ class OptionsByExample
207
212
  case arity
208
213
  when :required
209
214
  @argument_values[argument_name] = @remainder.shift
210
- when :vararg
215
+ when :vararg, :optional_vararg
211
216
  @argument_values[argument_name] = @remainder.shift(@remainder.length - remaining_arguments)
212
217
  when :optional
213
- break if @remainder.empty?
218
+ next if @remainder.empty?
214
219
  @argument_values[argument_name] = @remainder.shift
215
220
  # :nocov:
216
221
  else
@@ -220,13 +225,10 @@ class OptionsByExample
220
225
  end
221
226
  end
222
227
 
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
- ]
228
+ private
229
+
230
+ def count_arguments(pattern)
231
+ @argument_names.values.grep(pattern).count
230
232
  end
231
233
  end
232
234
  end
@@ -6,8 +6,6 @@ class OptionsByExample
6
6
 
7
7
  attr_reader :message
8
8
  attr_reader :argument_names
9
- attr_reader :default_values
10
- attr_reader :ends_with_optional_vararg
11
9
  attr_reader :option_names
12
10
 
13
11
  def initialize(text)
@@ -24,10 +22,10 @@ class OptionsByExample
24
22
  inline_options = []
25
23
 
26
24
  usage_line = text.lines.grep(/Usage:/).first
27
- raise RuntimeError, "Expected usage string, got none" unless usage_line
25
+ raise "Expected usage string, got none" unless usage_line
28
26
  tokens = usage_line.scan(/\[.*?\]|\w+ \.\.\.|\S+/)
29
- raise unless tokens.shift == 'Usage:'
30
- raise unless tokens.shift
27
+ raise "Expected usage line to start with 'Usage:'" unless tokens.shift == 'Usage:'
28
+ raise "Expected command name on same line as 'Usage:'" unless tokens.shift
31
29
  tokens.shift if tokens.first == '[options]'
32
30
 
33
31
  while /^\[(--?\w.*)\]$/ === tokens.first
@@ -47,18 +45,19 @@ class OptionsByExample
47
45
  end
48
46
 
49
47
  if /^\[(\w+) ?\.\.\.\]$/ === tokens.first
50
- @argument_names[sanitize $1] = :optional
51
- @ends_with_optional_vararg = true
48
+ @argument_names[sanitize $1] = :optional_vararg
52
49
  tokens.shift
53
50
  end
54
51
 
55
52
  raise "Found invalid usage token '#{tokens.first}'" unless tokens.empty?
56
53
 
57
- count_optional_arguments = @argument_names.values.count(:optional)
58
- count_vararg_arguments = @argument_names.values.count(:vararg)
54
+ if count_arguments(:vararg) > 0 && count_arguments(/optional/) > 0
55
+ raise "Cannot combine vararg and optional arguments"
56
+ end
59
57
 
60
- raise "Cannot combine dotted and optional arguments" if count_optional_arguments > 0 && count_vararg_arguments > 0
61
- raise "Found more than one dotted arguments" if count_vararg_arguments > 1
58
+ if count_arguments(/vararg/) > 1
59
+ raise "Found more than one vararg arguments"
60
+ end
62
61
 
63
62
  # --- 2) Parse option names ---------------------------------------
64
63
  #
@@ -72,7 +71,6 @@ class OptionsByExample
72
71
  # -t, --timeout NUM Set connection timeout in seconds
73
72
 
74
73
  @option_names = {}
75
- @default_values = {}
76
74
 
77
75
  options = inline_options + text.lines.grep(/^\s*--?\w/)
78
76
  options.each do |string|
@@ -99,11 +97,10 @@ class OptionsByExample
99
97
  default_value = $1
100
98
  end
101
99
 
102
- [short_form, long_form].compact.each do |each|
103
- @option_names[each] = [option_name, argument_name]
100
+ ary = [option_name, argument_name, default_value]
101
+ [short_form, long_form].each do |each|
102
+ @option_names[each] = ary if each
104
103
  end
105
-
106
- @default_values[option_name] = default_value if default_value
107
104
  end
108
105
  end
109
106
 
@@ -112,5 +109,9 @@ class OptionsByExample
112
109
  def sanitize(string)
113
110
  string.tr('^a-zA-Z0-9', '_').downcase.to_sym
114
111
  end
112
+
113
+ def count_arguments(pattern)
114
+ @argument_names.values.grep(pattern).count
115
+ end
115
116
  end
116
117
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class OptionsByExample
4
- VERSION = '4.0.0'
4
+ VERSION = '4.1.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.1.0
15
+ - Treat everything after double-dash as positional arguments
16
+ - Improve error message when usage is split across lines
17
+ - Add support for FLOAT arguments, eg `--ratio FLOAT`
18
+
14
19
  4.0.0
15
20
  - Remove support for leading optional arguments (breaking change)
16
21
  - Add support for trailing optional arguments
@@ -76,7 +76,9 @@ class OptionsByExample
76
76
  def initialize_argument_accessors
77
77
  [
78
78
  *@usage_spec.argument_names.keys,
79
- *@usage_spec.option_names.values.select(&:last).map(&:first),
79
+ *@usage_spec.option_names.values
80
+ .map { |option_name, has_argument| option_name if has_argument }
81
+ .compact,
80
82
  ].each do |argument_name|
81
83
  instance_eval %{
82
84
  def argument_#{argument_name}
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: 4.0.0
4
+ version: 4.1.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-03-16 00:00:00.000000000 Z
11
+ date: 2026-04-01 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: