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 +4 -4
- data/README.md +27 -40
- data/lib/options_by_example/commandline_parser.rb +54 -52
- data/lib/options_by_example/usage_specification.rb +17 -16
- data/lib/options_by_example/version.rb +6 -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
|
-
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 =
|
|
21
|
+
@argument_values = {}
|
|
24
22
|
@option_values = {}
|
|
25
23
|
end
|
|
26
24
|
|
|
27
|
-
def parse(
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
|
112
|
-
|
|
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
|
|
117
|
-
@slices.
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
|
126
|
-
raise "Expected argument for option '#{option}', got none"
|
|
127
|
-
@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
|
|
128
129
|
@option_took_argument = option
|
|
129
130
|
else
|
|
130
131
|
@option_took_argument = nil
|
|
131
132
|
end
|
|
132
133
|
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
+
value = Date.parse value
|
|
148
156
|
when 'TIME'
|
|
149
157
|
expected_type = 'a timestamp (e.g. HH:MM:SS)'
|
|
150
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
@
|
|
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
|
|
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] = :
|
|
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
|
-
|
|
58
|
-
|
|
54
|
+
if count_arguments(:vararg) > 0 && count_arguments(/optional/) > 0
|
|
55
|
+
raise "Cannot combine vararg and optional arguments"
|
|
56
|
+
end
|
|
59
57
|
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
[
|
|
103
|
-
|
|
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.
|
|
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
|
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.
|
|
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:
|