options_by_example 4.0.0 → 4.2.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: 59f737886cd6b035ebefb01b6d513d6a4cd66957858d9c3f9d4b489058fe5725
4
+ data.tar.gz: d3f6f2d559a4f5c077f878dec4e8fb8c8f9cf9e4daaeebaba8b5611f343ac86b
5
5
  SHA512:
6
- metadata.gz: 7513e6747f9269e9618e4a4e277560fcbb7741663cc01a3c5767b0a30c52277cd67590c19b21e2eb79d42bc6678ad329d465b1d0b75f2da6079d12cc9978fe52
7
- data.tar.gz: 1a4bfc340b15a0f63115e9025e4be96f37fa0d5a0808bbe81d44438c97648159154b41dc8380b94fe8aeb1a4ab5d5d7b6448572e6741ecf8a5e8bdf3d75166a3
6
+ metadata.gz: ad0b05e43b8ec21c9bbc434085896082fe0a956190d255c5ef24382f3f0ec8faf5f05b32f3acda272ca8d0fbbb63e736d9a5db4860e6bd750be26f7601a263c6
7
+ data.tar.gz: e2008e435bbfc129170599f7e28626ede57501b078e65684c436fea0644d69b8a564c388da1bd197a60332c4c34404d60424541dd9912307c0b2316ad069a811
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..-1] = [@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,85 @@ 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, argument_arity, _, _ = @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
128
- @option_took_argument = option
129
- else
130
- @option_took_argument = nil
126
+ if argument_arity == :required
127
+ raise "Expected argument for option '#{option}', got none" unless current.first
128
+
129
+ if pending.length == 1
130
+ minimum_count = count_arguments(:required) + count_arguments(:vararg)
131
+ if current.length <= minimum_count
132
+ raise "Ambiguous argument for option '#{option}', not enough remaining positional arguments"
133
+ end
134
+ end
135
+
136
+ @argument_values[option_name] = current.shift
137
+ elsif argument_arity == :optional && current.first
138
+ if pending.length == 1
139
+ required_count = count_arguments(:required)
140
+ if current.length <= required_count and required_count == @argument_names.length
141
+ return current
142
+ end
143
+ if current.length <= required_count or required_count != @argument_names.length
144
+ raise "Ambiguous argument for option '#{option}', please use -- before positional arguments"
145
+ end
146
+ end
147
+
148
+ @argument_values[option_name] = current.shift
131
149
  end
132
150
 
133
- @remainder = args
151
+ pending.shift if current.empty?
134
152
  end
153
+
154
+ return []
135
155
  end
136
156
 
137
157
  def coerce_num_date_time_etc
138
- @option_names.each do |option, (each, argument_name)|
139
- next unless value = @argument_values[each]
158
+ @option_names.each do |option, (each, _, argument_type, default_value)|
159
+ value = @argument_values.fetch(each, default_value)
160
+ next unless value
161
+
140
162
  begin
141
- case argument_name
163
+ case argument_type
142
164
  when 'NUM'
143
165
  expected_type = 'an integer value'
144
- @argument_values[each] = Integer value
166
+ value = Integer value
167
+ when 'FLOAT'
168
+ expected_type = 'a floating-point value'
169
+ value = Float value
145
170
  when 'DATE'
146
171
  expected_type = 'a date (e.g. YYYY-MM-DD)'
147
- @argument_values[each] = Date.parse value
172
+ value = Date.parse value
148
173
  when 'TIME'
149
174
  expected_type = 'a timestamp (e.g. HH:MM:SS)'
150
- @argument_values[each] = Time.parse value
175
+ value = Time.parse value
151
176
  end
152
177
  rescue ArgumentError
153
178
  raise "Invalid argument \"#{value}\" for option '#{option}', please provide #{expected_type}"
154
179
  end
180
+
181
+ @argument_values[each] = value
155
182
  end
156
183
  end
157
184
 
@@ -159,14 +186,9 @@ class OptionsByExample
159
186
  # ASSUME: either varargs or optional arguments, never both. That
160
187
  # constraint is guaranteed upstream. Here, we just count
161
188
 
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
189
+ min_length = count_arguments(:required) + count_arguments(:vararg)
190
+ max_length = count_arguments(:required) + count_arguments(/optional/)
191
+ max_length = nil if count_arguments(/vararg/) > 0
170
192
 
171
193
  unless (min_length..max_length) === @remainder.size
172
194
 
@@ -192,10 +214,6 @@ class OptionsByExample
192
214
  # :nocov:
193
215
  end
194
216
 
195
- if @option_took_argument
196
- msg += " (considering #{@option_took_argument} takes an argument)"
197
- end
198
-
199
217
  raise msg
200
218
  end
201
219
  end
@@ -207,10 +225,10 @@ class OptionsByExample
207
225
  case arity
208
226
  when :required
209
227
  @argument_values[argument_name] = @remainder.shift
210
- when :vararg
228
+ when :vararg, :optional_vararg
211
229
  @argument_values[argument_name] = @remainder.shift(@remainder.length - remaining_arguments)
212
230
  when :optional
213
- break if @remainder.empty?
231
+ next if @remainder.empty?
214
232
  @argument_values[argument_name] = @remainder.shift
215
233
  # :nocov:
216
234
  else
@@ -220,13 +238,10 @@ class OptionsByExample
220
238
  end
221
239
  end
222
240
 
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
- ]
241
+ private
242
+
243
+ def count_arguments(pattern)
244
+ @argument_names.values.grep(pattern).count
230
245
  end
231
246
  end
232
247
  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
28
- tokens = usage_line.scan(/\[.*?\]|\w+ \.\.\.|\S+/)
29
- raise unless tokens.shift == 'Usage:'
30
- raise unless tokens.shift
25
+ raise "Expected usage string, got none" unless usage_line
26
+ tokens = usage_line.split(/(\[.*?\]?\]|\w+ \.\.\.)|\s+/).reject(&:empty?)
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,38 +71,48 @@ 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|
79
- tokens = string.scan(/--?\w[\w-]*(?: \w+)?|,|\(default \S+\)|\S+/)
77
+ tokens = string.strip.split(/(\s+)|(,)\s*/)
80
78
 
81
79
  short_form = nil
82
80
  long_form = nil
83
81
  option_name = nil
84
- argument_name = nil
82
+ argument_arity = nil
83
+ argument_type = nil
85
84
  default_value = nil
86
85
 
87
- if /^-(\w)( \w+)?$/ === tokens.first
88
- short_form, argument_name = tokens.shift.split
86
+ if /^-(\w)$/ === tokens.first
87
+ short_form = tokens.shift
89
88
  option_name = sanitize $1
90
- tokens.shift if ',' === tokens.first
89
+ tokens.shift if tokens.first == ','
91
90
  end
92
91
 
93
- if /^--([\w-]+)( \w+)?$/ === tokens.first
94
- long_form, argument_name = tokens.shift.split
92
+ if /^--([\w-]+)$/ === tokens.first
93
+ long_form = tokens.shift
95
94
  option_name = sanitize $1
96
95
  end
97
96
 
98
- if /^\(default (\S+)\)$/ === tokens.last
99
- default_value = $1
97
+ if tokens.shift == ' '
98
+ if /^(\[(.+)\]|(.+)\?)$/ === tokens.first
99
+ argument_type = $2 || $3
100
+ argument_arity = :optional
101
+ tokens.shift
102
+ else
103
+ argument_type = tokens.shift
104
+ argument_arity = :required
105
+ end
100
106
  end
101
107
 
102
- [short_form, long_form].compact.each do |each|
103
- @option_names[each] = [option_name, argument_name]
108
+ if /\(default\s+(\S+)\)$/ === tokens.join
109
+ default_value = $1
104
110
  end
105
111
 
106
- @default_values[option_name] = default_value if default_value
112
+ ary = [option_name, argument_arity, argument_type, default_value]
113
+ [short_form, long_form].each do |each|
114
+ @option_names[each] = ary if each
115
+ end
107
116
  end
108
117
  end
109
118
 
@@ -112,5 +121,9 @@ class OptionsByExample
112
121
  def sanitize(string)
113
122
  string.tr('^a-zA-Z0-9', '_').downcase.to_sym
114
123
  end
124
+
125
+ def count_arguments(pattern)
126
+ @argument_names.values.grep(pattern).count
127
+ end
115
128
  end
116
129
  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.2.0'
5
5
  end
6
6
 
7
7
 
@@ -11,6 +11,19 @@ __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.2.0
15
+ - Generate #get_NAME methods for arguments
16
+ - Keep #argument_NAME as backwards-compatible aliases
17
+ - New method #get_at_most_one returns the provided option or nil
18
+ - New method #get_mutually_exclusive aliases #get_at_most_one
19
+ - Deprecate #expect_at_most_one_of in favor of #expect_at_most_one
20
+ - Support optional option arguments, eg '--compress [NUM]'
21
+
22
+ 4.1.0
23
+ - Treat everything after double-dash as positional arguments
24
+ - Improve error message when usage is split across lines
25
+ - Add support for FLOAT arguments, eg `--ratio FLOAT`
26
+
14
27
  4.0.0
15
28
  - Remove support for leading optional arguments (breaking change)
16
29
  - Add support for trailing optional arguments
@@ -32,16 +32,24 @@ class OptionsByExample
32
32
  end
33
33
 
34
34
  def expect_at_most_one_except(*extra_options)
35
- expect_at_most_one_of *(@options.keys - extra_options)
35
+ expect_at_most_one *(@options.keys - extra_options)
36
36
  end
37
37
 
38
38
  def expect_at_most_one_of(*mutually_exclusive_options)
39
+ expect_at_most_one *mutually_exclusive_options
40
+ end
41
+
42
+ def get_at_most_one(*mutually_exclusive_options)
39
43
  provided_options = @options.keys & mutually_exclusive_options
40
44
  if provided_options.length > 1
41
45
  abort "ERR: Found more than one mutually-exclusive option {#{provided_options.join ', '}}"
42
46
  end
47
+ provided_options.first
43
48
  end
44
49
 
50
+ alias expect_at_most_one get_at_most_one
51
+ alias get_mutually_exclusive get_at_most_one
52
+
45
53
  def fetch(*args, &block)
46
54
  @arguments.fetch(*args, &block)
47
55
  end
@@ -76,13 +84,17 @@ class OptionsByExample
76
84
  def initialize_argument_accessors
77
85
  [
78
86
  *@usage_spec.argument_names.keys,
79
- *@usage_spec.option_names.values.select(&:last).map(&:first),
87
+ *@usage_spec.option_names.values
88
+ .map { |option_name, argument_arity| option_name if argument_arity }
89
+ .compact,
80
90
  ].each do |argument_name|
81
91
  instance_eval %{
82
- def argument_#{argument_name}
92
+ def get_#{argument_name}
83
93
  val = @arguments[:#{argument_name}]
84
94
  val && block_given? ? (yield val) : val
85
95
  end
96
+
97
+ alias argument_#{argument_name} get_#{argument_name}
86
98
  }
87
99
  end
88
100
  end
@@ -97,4 +109,3 @@ class OptionsByExample
97
109
  end
98
110
  end
99
111
  end
100
-
metadata CHANGED
@@ -1,16 +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.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Kuhn
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-03-16 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
- description:
14
12
  email:
15
13
  - akuhn@iam.unibe.ch
16
14
  executables: []
@@ -29,7 +27,6 @@ metadata:
29
27
  homepage_uri: https://github.com/akuhn/options_by_example
30
28
  source_code_uri: https://github.com/akuhn/options_by_example
31
29
  changelog_uri: https://github.com/akuhn/options_by_example/blob/master/lib/options_by_example/version.rb
32
- post_install_message:
33
30
  rdoc_options: []
34
31
  require_paths:
35
32
  - lib
@@ -44,8 +41,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
44
41
  - !ruby/object:Gem::Version
45
42
  version: '0'
46
43
  requirements: []
47
- rubygems_version: 3.0.3.1
48
- signing_key:
44
+ rubygems_version: 3.6.9
49
45
  specification_version: 4
50
46
  summary: No-code options parser that extracts arguments directly from usage text.
51
47
  test_files: []