options_by_example 3.4.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 +4 -4
- data/README.md +27 -40
- data/lib/options_by_example/commandline_parser.rb +107 -75
- data/lib/options_by_example/usage_specification.rb +24 -16
- data/lib/options_by_example/version.rb +11 -1
- data/lib/options_by_example.rb +3 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3f146461611879c6934270f692e97391830d93f634b9300f7fa2883195bc0a22
|
|
4
|
+
data.tar.gz: 19ad57569defbb334404085af3248675cfa84000511bbf49b2803b5c6cb38dee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
3
|
+
No-code options parser that automatically detects command-line options from the usage text.
|
|
4
4
|
|
|
5
5
|
Features
|
|
6
6
|
|
|
7
|
-
- Automatically
|
|
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
|
-
|
|
12
|
+
Example
|
|
13
13
|
|
|
14
|
-
|
|
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
|
-
|
|
19
|
+
puts 'Feeling verbose today' if flags.include?(:verbose)
|
|
20
|
+
puts flags.get(:words).sample(flags.get(:num))
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Example
|
|
22
|
+
__END__
|
|
23
|
+
Choose at random from a list of provided words.
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
require 'options_by_example'
|
|
25
|
+
Usage: random.rb [options] words ...
|
|
30
26
|
|
|
31
|
-
Options
|
|
27
|
+
Options:
|
|
28
|
+
-n, --num NUM Number of choices (default 1)
|
|
29
|
+
--verbose Enable verbose mode
|
|
30
|
+
```
|
|
32
31
|
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[mode] Optional connection mode (active or passive)
|
|
58
|
-
host The target host to connect to (e.g., example.com)
|
|
59
|
-
port The target port to connect to (e.g., 80)
|
|
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,46 +16,47 @@ class OptionsByExample
|
|
|
16
16
|
|
|
17
17
|
def initialize(usage)
|
|
18
18
|
@argument_names = usage.argument_names
|
|
19
|
-
@default_values = usage.default_values
|
|
20
19
|
@option_names = usage.option_names
|
|
21
20
|
|
|
22
|
-
@argument_values =
|
|
21
|
+
@argument_values = {}
|
|
23
22
|
@option_values = {}
|
|
24
23
|
end
|
|
25
24
|
|
|
26
|
-
def parse(
|
|
25
|
+
def parse(argv)
|
|
26
|
+
@slices = argv.slice_before(/^-/).entries
|
|
27
|
+
treat_everything_after_double_dash_as_positionals
|
|
27
28
|
|
|
28
|
-
|
|
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
|
|
29
|
+
exit_if_help_option
|
|
40
30
|
unpack_combined_shorthand_options
|
|
41
31
|
expand_dash_number_to_dash_n_option
|
|
42
32
|
raise_if_unknown_options
|
|
43
|
-
|
|
33
|
+
|
|
34
|
+
@remainder = parse_options_and_return_remainder
|
|
44
35
|
coerce_num_date_time_etc
|
|
45
36
|
|
|
46
37
|
validate_number_of_arguments
|
|
47
|
-
|
|
48
|
-
parse_optional_arguments
|
|
38
|
+
parse_positional_arguments
|
|
49
39
|
|
|
50
|
-
|
|
40
|
+
# :nocov:
|
|
41
|
+
raise %{unreachable given we check number of arguments} unless @remainder.empty?
|
|
42
|
+
# :nocov:
|
|
51
43
|
end
|
|
52
44
|
|
|
53
45
|
private
|
|
54
46
|
|
|
55
|
-
def
|
|
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
|
+
|
|
52
|
+
def exit_if_help_option
|
|
56
53
|
@slices.each do |option, *args|
|
|
57
54
|
case option
|
|
58
55
|
when '-h', '--help'
|
|
56
|
+
if args.first == 'debug!'
|
|
57
|
+
puts "@argument_names = #{@argument_names.inspect}"
|
|
58
|
+
puts "@option_names = #{@option_names.inspect}"
|
|
59
|
+
end
|
|
59
60
|
raise PrintUsageMessage
|
|
60
61
|
end
|
|
61
62
|
end
|
|
@@ -99,104 +100,135 @@ class OptionsByExample
|
|
|
99
100
|
end
|
|
100
101
|
|
|
101
102
|
def raise_if_unknown_options
|
|
102
|
-
@slices.each do |option
|
|
103
|
-
|
|
103
|
+
@slices.each do |option,|
|
|
104
|
+
if option =~ /^--?\w/ and not @option_names.include?(option)
|
|
105
|
+
raise "Found unknown option '#{option}'"
|
|
106
|
+
end
|
|
104
107
|
end
|
|
105
108
|
end
|
|
106
109
|
|
|
107
|
-
def
|
|
108
|
-
@slices.
|
|
109
|
-
|
|
110
|
-
|
|
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"
|
|
111
120
|
end
|
|
112
121
|
|
|
113
|
-
|
|
122
|
+
option = current.shift # consume the option/flag
|
|
123
|
+
option_name, has_argument, _ = @option_names[option]
|
|
114
124
|
@option_values[option_name] = true
|
|
115
125
|
|
|
116
|
-
if
|
|
117
|
-
raise "Expected argument for option '#{option}', got none"
|
|
118
|
-
@argument_values[option_name] =
|
|
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
|
|
119
129
|
@option_took_argument = option
|
|
120
130
|
else
|
|
121
131
|
@option_took_argument = nil
|
|
122
132
|
end
|
|
123
133
|
|
|
124
|
-
|
|
134
|
+
pending.shift if current.empty?
|
|
125
135
|
end
|
|
136
|
+
|
|
137
|
+
return []
|
|
126
138
|
end
|
|
127
139
|
|
|
128
140
|
def coerce_num_date_time_etc
|
|
129
|
-
@option_names.each do |option, (each, argument_name)|
|
|
130
|
-
|
|
141
|
+
@option_names.each do |option, (each, argument_name, default_value)|
|
|
142
|
+
value = @argument_values.fetch(each, default_value)
|
|
143
|
+
next unless value
|
|
144
|
+
|
|
131
145
|
begin
|
|
132
146
|
case argument_name
|
|
133
147
|
when 'NUM'
|
|
134
148
|
expected_type = 'an integer value'
|
|
135
|
-
|
|
149
|
+
value = Integer value
|
|
150
|
+
when 'FLOAT'
|
|
151
|
+
expected_type = 'a floating-point value'
|
|
152
|
+
value = Float value
|
|
136
153
|
when 'DATE'
|
|
137
154
|
expected_type = 'a date (e.g. YYYY-MM-DD)'
|
|
138
|
-
|
|
155
|
+
value = Date.parse value
|
|
139
156
|
when 'TIME'
|
|
140
157
|
expected_type = 'a timestamp (e.g. HH:MM:SS)'
|
|
141
|
-
|
|
158
|
+
value = Time.parse value
|
|
142
159
|
end
|
|
143
160
|
rescue ArgumentError
|
|
144
161
|
raise "Invalid argument \"#{value}\" for option '#{option}', please provide #{expected_type}"
|
|
145
162
|
end
|
|
163
|
+
|
|
164
|
+
@argument_values[each] = value
|
|
146
165
|
end
|
|
147
166
|
end
|
|
148
167
|
|
|
149
168
|
def validate_number_of_arguments
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
count_vararg_arguments = @argument_names.values.count(:vararg)
|
|
169
|
+
# ASSUME: either varargs or optional arguments, never both. That
|
|
170
|
+
# constraint is guaranteed upstream. Here, we just count
|
|
153
171
|
|
|
154
|
-
min_length =
|
|
155
|
-
max_length =
|
|
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
|
|
156
175
|
|
|
157
|
-
|
|
158
|
-
range = [min_length, max_length].uniq.join(?-)
|
|
159
|
-
raise "Expected #{range} arguments, but received too many"
|
|
160
|
-
end
|
|
176
|
+
unless (min_length..max_length) === @remainder.size
|
|
161
177
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
178
|
+
if max_length.nil?
|
|
179
|
+
msg = "Expected #{min_length} or more arguments,"
|
|
180
|
+
elsif max_length > min_length
|
|
181
|
+
msg = "Expected #{min_length}-#{max_length} arguments,"
|
|
182
|
+
else
|
|
183
|
+
msg = "Expected #{min_length} arguments,"
|
|
184
|
+
end
|
|
168
185
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
raise "unreachable"
|
|
182
|
-
end
|
|
186
|
+
if @remainder.empty?
|
|
187
|
+
msg += " but received none"
|
|
188
|
+
elsif @remainder.size == 1 && min_length > 1
|
|
189
|
+
msg += " but received only one"
|
|
190
|
+
elsif @remainder.size < min_length
|
|
191
|
+
msg += " but received too few"
|
|
192
|
+
elsif max_length && @remainder.size > max_length
|
|
193
|
+
msg += " but received too many"
|
|
194
|
+
# :nocov:
|
|
195
|
+
else
|
|
196
|
+
raise %{unreachable given the range check above}
|
|
197
|
+
# :nocov:
|
|
183
198
|
end
|
|
184
|
-
return
|
|
185
|
-
end
|
|
186
199
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
200
|
+
if @option_took_argument
|
|
201
|
+
msg += " (considering #{@option_took_argument} takes an argument)"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
raise msg
|
|
191
205
|
end
|
|
192
206
|
end
|
|
193
207
|
|
|
194
|
-
def
|
|
208
|
+
def parse_positional_arguments
|
|
209
|
+
remaining_arguments = @argument_names.length
|
|
195
210
|
@argument_names.each do |argument_name, arity|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
211
|
+
remaining_arguments -= 1
|
|
212
|
+
case arity
|
|
213
|
+
when :required
|
|
214
|
+
@argument_values[argument_name] = @remainder.shift
|
|
215
|
+
when :vararg, :optional_vararg
|
|
216
|
+
@argument_values[argument_name] = @remainder.shift(@remainder.length - remaining_arguments)
|
|
217
|
+
when :optional
|
|
218
|
+
next if @remainder.empty?
|
|
219
|
+
@argument_values[argument_name] = @remainder.shift
|
|
220
|
+
# :nocov:
|
|
221
|
+
else
|
|
222
|
+
raise %{unreachable given these are all possible values}
|
|
223
|
+
# :nocov:
|
|
224
|
+
end
|
|
199
225
|
end
|
|
200
226
|
end
|
|
227
|
+
|
|
228
|
+
private
|
|
229
|
+
|
|
230
|
+
def count_arguments(pattern)
|
|
231
|
+
@argument_names.values.grep(pattern).count
|
|
232
|
+
end
|
|
201
233
|
end
|
|
202
234
|
end
|
|
@@ -6,7 +6,6 @@ class OptionsByExample
|
|
|
6
6
|
|
|
7
7
|
attr_reader :message
|
|
8
8
|
attr_reader :argument_names
|
|
9
|
-
attr_reader :default_values
|
|
10
9
|
attr_reader :option_names
|
|
11
10
|
|
|
12
11
|
def initialize(text)
|
|
@@ -23,10 +22,10 @@ class OptionsByExample
|
|
|
23
22
|
inline_options = []
|
|
24
23
|
|
|
25
24
|
usage_line = text.lines.grep(/Usage:/).first
|
|
26
|
-
raise
|
|
25
|
+
raise "Expected usage string, got none" unless usage_line
|
|
27
26
|
tokens = usage_line.scan(/\[.*?\]|\w+ \.\.\.|\S+/)
|
|
28
|
-
raise unless tokens.shift == 'Usage:'
|
|
29
|
-
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
|
|
30
29
|
tokens.shift if tokens.first == '[options]'
|
|
31
30
|
|
|
32
31
|
while /^\[(--?\w.*)\]$/ === tokens.first
|
|
@@ -34,24 +33,31 @@ class OptionsByExample
|
|
|
34
33
|
tokens.shift
|
|
35
34
|
end
|
|
36
35
|
|
|
36
|
+
while /^(\w+)( ?\.\.\.)?$/ === tokens.first
|
|
37
|
+
vararg_if_dotted = $2 ? :vararg : :required
|
|
38
|
+
@argument_names[sanitize $1] = vararg_if_dotted
|
|
39
|
+
tokens.shift
|
|
40
|
+
end
|
|
41
|
+
|
|
37
42
|
while /^\[(\w+)\]$/ === tokens.first
|
|
38
43
|
@argument_names[sanitize $1] = :optional
|
|
39
44
|
tokens.shift
|
|
40
45
|
end
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
@argument_names[sanitize $1] = vararg_if_dotted
|
|
47
|
+
if /^\[(\w+) ?\.\.\.\]$/ === tokens.first
|
|
48
|
+
@argument_names[sanitize $1] = :optional_vararg
|
|
45
49
|
tokens.shift
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
raise "Found invalid usage token '#{tokens.first}'" unless tokens.empty?
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
if count_arguments(:vararg) > 0 && count_arguments(/optional/) > 0
|
|
55
|
+
raise "Cannot combine vararg and optional arguments"
|
|
56
|
+
end
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
if count_arguments(/vararg/) > 1
|
|
59
|
+
raise "Found more than one vararg arguments"
|
|
60
|
+
end
|
|
55
61
|
|
|
56
62
|
# --- 2) Parse option names ---------------------------------------
|
|
57
63
|
#
|
|
@@ -65,7 +71,6 @@ class OptionsByExample
|
|
|
65
71
|
# -t, --timeout NUM Set connection timeout in seconds
|
|
66
72
|
|
|
67
73
|
@option_names = {}
|
|
68
|
-
@default_values = {}
|
|
69
74
|
|
|
70
75
|
options = inline_options + text.lines.grep(/^\s*--?\w/)
|
|
71
76
|
options.each do |string|
|
|
@@ -92,11 +97,10 @@ class OptionsByExample
|
|
|
92
97
|
default_value = $1
|
|
93
98
|
end
|
|
94
99
|
|
|
95
|
-
[
|
|
96
|
-
|
|
100
|
+
ary = [option_name, argument_name, default_value]
|
|
101
|
+
[short_form, long_form].each do |each|
|
|
102
|
+
@option_names[each] = ary if each
|
|
97
103
|
end
|
|
98
|
-
|
|
99
|
-
@default_values[option_name] = default_value if default_value
|
|
100
104
|
end
|
|
101
105
|
end
|
|
102
106
|
|
|
@@ -105,5 +109,9 @@ class OptionsByExample
|
|
|
105
109
|
def sanitize(string)
|
|
106
110
|
string.tr('^a-zA-Z0-9', '_').downcase.to_sym
|
|
107
111
|
end
|
|
112
|
+
|
|
113
|
+
def count_arguments(pattern)
|
|
114
|
+
@argument_names.values.grep(pattern).count
|
|
115
|
+
end
|
|
108
116
|
end
|
|
109
117
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class OptionsByExample
|
|
4
|
-
VERSION = '
|
|
4
|
+
VERSION = '4.1.0'
|
|
5
5
|
end
|
|
6
6
|
|
|
7
7
|
|
|
@@ -11,6 +11,16 @@ __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
|
+
|
|
19
|
+
4.0.0
|
|
20
|
+
- Remove support for leading optional arguments (breaking change)
|
|
21
|
+
- Add support for trailing optional arguments
|
|
22
|
+
- Add support for optional vararg argument
|
|
23
|
+
|
|
14
24
|
3.4.0
|
|
15
25
|
- Ensure default values are coerced too
|
|
16
26
|
- Print error message to stdout
|
data/lib/options_by_example.rb
CHANGED
|
@@ -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
|
|
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
|
+
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-
|
|
11
|
+
date: 2026-04-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description:
|
|
14
14
|
email:
|