options_by_example 3.2.0 → 3.4.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/lib/options_by_example/commandline_parser.rb +202 -0
- data/lib/options_by_example/usage_specification.rb +109 -0
- data/lib/options_by_example/version.rb +12 -1
- data/lib/options_by_example.rb +24 -54
- metadata +9 -8
- data/lib/options_by_example/parser.rb +0 -168
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: beeaa2ae22a04cfe49102108f616bad5d3fb63b00156bb34647774ba07c8def0
|
|
4
|
+
data.tar.gz: e2af434e1812a6244c276bea7b00b52b97411927ccc207e843f4866e64356d08
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fca827e7190634ca8382ff90d074a276fd4367a609fad77b3d58a8488f85b97e3e8493fe2a717fd9a9aff3fefdde4315058c1512addaf4b1d7663f1a2dc201db
|
|
7
|
+
data.tar.gz: a2ce56d793153540e2c9c4959dc7af5aff0e73a4c32037a738a368f06128d4f35730411f8cce8875e5e1a3c153e6e4a4344de006171ca1b70d1b24737eeaf819
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class OptionsByExample
|
|
8
|
+
|
|
9
|
+
class PrintUsageMessage < StandardError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class CommandlineParser
|
|
13
|
+
|
|
14
|
+
attr_reader :option_values
|
|
15
|
+
attr_reader :argument_values
|
|
16
|
+
|
|
17
|
+
def initialize(usage)
|
|
18
|
+
@argument_names = usage.argument_names
|
|
19
|
+
@default_values = usage.default_values
|
|
20
|
+
@option_names = usage.option_names
|
|
21
|
+
|
|
22
|
+
@argument_values = @default_values.dup
|
|
23
|
+
@option_values = {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def parse(array)
|
|
27
|
+
|
|
28
|
+
# Separate command-line options and their respective arguments into
|
|
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
|
|
40
|
+
unpack_combined_shorthand_options
|
|
41
|
+
expand_dash_number_to_dash_n_option
|
|
42
|
+
raise_if_unknown_options
|
|
43
|
+
parse_options
|
|
44
|
+
coerce_num_date_time_etc
|
|
45
|
+
|
|
46
|
+
validate_number_of_arguments
|
|
47
|
+
parse_required_arguments
|
|
48
|
+
parse_optional_arguments
|
|
49
|
+
|
|
50
|
+
raise "Internal error: unreachable state" unless @remainder.empty?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def raise_if_help_option
|
|
56
|
+
@slices.each do |option, *args|
|
|
57
|
+
case option
|
|
58
|
+
when '-h', '--help'
|
|
59
|
+
raise PrintUsageMessage
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def unpack_combined_shorthand_options
|
|
65
|
+
|
|
66
|
+
# Expand any combined shorthand options like -svt into their
|
|
67
|
+
# separate components (-s, -v, and -t) and assigns any arguments
|
|
68
|
+
# to the last component. If an unknown shorthand is found, raise
|
|
69
|
+
# a helpful error message with suggestion if possible.
|
|
70
|
+
|
|
71
|
+
list = []
|
|
72
|
+
@slices.each do |option, *args|
|
|
73
|
+
if option =~ /^-([a-zA-Z]{2,})$/
|
|
74
|
+
shorthands = $1.each_char.map { |char| "-#{char}" }
|
|
75
|
+
|
|
76
|
+
shorthands.each do |each|
|
|
77
|
+
if not @option_names.include?(each)
|
|
78
|
+
did_you_mean = ", did you mean '-#{option}'?" if @option_names.include?("-#{option}")
|
|
79
|
+
raise "Found unknown option #{each} inside '#{option}'#{did_you_mean}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
list.concat shorthands.map { |each| [each] }
|
|
84
|
+
list.last.concat args
|
|
85
|
+
else
|
|
86
|
+
list << [option, *args]
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
@slices = list
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def expand_dash_number_to_dash_n_option
|
|
94
|
+
@slices.each do |each|
|
|
95
|
+
if each.first =~ /^-(\d+)$/
|
|
96
|
+
each[0..0] = ['-n', $1]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def raise_if_unknown_options
|
|
102
|
+
@slices.each do |option, *args|
|
|
103
|
+
raise "Found unknown option '#{option}'" unless @option_names.include?(option)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def parse_options
|
|
108
|
+
@slices.each do |option, *args|
|
|
109
|
+
if @remainder.any?
|
|
110
|
+
raise "Unexpected arguments found before option '#{option}', please provide all options before arguments"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
option_name, argument_name = @option_names[option]
|
|
114
|
+
@option_values[option_name] = true
|
|
115
|
+
|
|
116
|
+
if argument_name
|
|
117
|
+
raise "Expected argument for option '#{option}', got none" if args.empty?
|
|
118
|
+
@argument_values[option_name] = args.shift
|
|
119
|
+
@option_took_argument = option
|
|
120
|
+
else
|
|
121
|
+
@option_took_argument = nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
@remainder = args
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def coerce_num_date_time_etc
|
|
129
|
+
@option_names.each do |option, (each, argument_name)|
|
|
130
|
+
next unless value = @argument_values[each]
|
|
131
|
+
begin
|
|
132
|
+
case argument_name
|
|
133
|
+
when 'NUM'
|
|
134
|
+
expected_type = 'an integer value'
|
|
135
|
+
@argument_values[each] = Integer value
|
|
136
|
+
when 'DATE'
|
|
137
|
+
expected_type = 'a date (e.g. YYYY-MM-DD)'
|
|
138
|
+
@argument_values[each] = Date.parse value
|
|
139
|
+
when 'TIME'
|
|
140
|
+
expected_type = 'a timestamp (e.g. HH:MM:SS)'
|
|
141
|
+
@argument_values[each] = Time.parse value
|
|
142
|
+
end
|
|
143
|
+
rescue ArgumentError
|
|
144
|
+
raise "Invalid argument \"#{value}\" for option '#{option}', please provide #{expected_type}"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def validate_number_of_arguments
|
|
150
|
+
count_optional_arguments = @argument_names.values.count(:optional)
|
|
151
|
+
count_required_arguments = @argument_names.values.count(:required)
|
|
152
|
+
count_vararg_arguments = @argument_names.values.count(:vararg)
|
|
153
|
+
|
|
154
|
+
min_length = count_required_arguments + count_vararg_arguments
|
|
155
|
+
max_length = count_required_arguments + count_optional_arguments
|
|
156
|
+
|
|
157
|
+
if @remainder.size > max_length && count_vararg_arguments == 0
|
|
158
|
+
range = [min_length, max_length].uniq.join(?-)
|
|
159
|
+
raise "Expected #{range} arguments, but received too many"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
if @remainder.size < min_length
|
|
163
|
+
too_few = @remainder.empty? ? 'none' : (@remainder.size == 1 ? 'only one' : 'too few')
|
|
164
|
+
remark = " (considering #{@option_took_argument} takes an argument)" if @option_took_argument
|
|
165
|
+
raise "Expected #{min_length} required arguments, but received #{too_few}#{remark}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def parse_required_arguments
|
|
170
|
+
if @argument_names.values.include?(:vararg)
|
|
171
|
+
remaining_arguments = @argument_names.length
|
|
172
|
+
@argument_names.each do |argument_name, arity|
|
|
173
|
+
raise "unreachable" if @remainder.empty?
|
|
174
|
+
remaining_arguments -= 1
|
|
175
|
+
case arity
|
|
176
|
+
when :required
|
|
177
|
+
@argument_values[argument_name] = @remainder.shift
|
|
178
|
+
when :vararg
|
|
179
|
+
@argument_values[argument_name] = @remainder.shift(@remainder.length - remaining_arguments)
|
|
180
|
+
else
|
|
181
|
+
raise "unreachable"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
return
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
@argument_names.reverse_each do |argument_name, arity|
|
|
188
|
+
break if arity == :optional
|
|
189
|
+
raise "unreachable" if @remainder.empty?
|
|
190
|
+
@argument_values[argument_name] = @remainder.pop
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def parse_optional_arguments
|
|
195
|
+
@argument_names.each do |argument_name, arity|
|
|
196
|
+
break unless arity == :optional
|
|
197
|
+
break if @remainder.empty?
|
|
198
|
+
@argument_values[argument_name] = @remainder.shift
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class OptionsByExample
|
|
4
|
+
|
|
5
|
+
class UsageSpecification
|
|
6
|
+
|
|
7
|
+
attr_reader :message
|
|
8
|
+
attr_reader :argument_names
|
|
9
|
+
attr_reader :default_values
|
|
10
|
+
attr_reader :option_names
|
|
11
|
+
|
|
12
|
+
def initialize(text)
|
|
13
|
+
@message = text.gsub('$0', File.basename($0)).gsub(/\n+\Z/, "\n\n")
|
|
14
|
+
|
|
15
|
+
# --- 1) Parse argument names -------------------------------------
|
|
16
|
+
#
|
|
17
|
+
# Parse the usage string and extract both optional argument names
|
|
18
|
+
# and required argument names, for example:
|
|
19
|
+
#
|
|
20
|
+
# Usage: connect [options] [mode] host port
|
|
21
|
+
|
|
22
|
+
@argument_names = {}
|
|
23
|
+
inline_options = []
|
|
24
|
+
|
|
25
|
+
usage_line = text.lines.grep(/Usage:/).first
|
|
26
|
+
raise RuntimeError, "Expected usage string, got none" unless usage_line
|
|
27
|
+
tokens = usage_line.scan(/\[.*?\]|\w+ \.\.\.|\S+/)
|
|
28
|
+
raise unless tokens.shift == 'Usage:'
|
|
29
|
+
raise unless tokens.shift
|
|
30
|
+
tokens.shift if tokens.first == '[options]'
|
|
31
|
+
|
|
32
|
+
while /^\[(--?\w.*)\]$/ === tokens.first
|
|
33
|
+
inline_options << $1
|
|
34
|
+
tokens.shift
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
while /^\[(\w+)\]$/ === tokens.first
|
|
38
|
+
@argument_names[sanitize $1] = :optional
|
|
39
|
+
tokens.shift
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
while /^(\w+)( ?\.\.\.)?$/ === tokens.first
|
|
43
|
+
vararg_if_dotted = $2 ? :vararg : :required
|
|
44
|
+
@argument_names[sanitize $1] = vararg_if_dotted
|
|
45
|
+
tokens.shift
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
raise "Found invalid usage token '#{tokens.first}'" unless tokens.empty?
|
|
49
|
+
|
|
50
|
+
count_optional_arguments = @argument_names.values.count(:optional)
|
|
51
|
+
count_vararg_arguments = @argument_names.values.count(:vararg)
|
|
52
|
+
|
|
53
|
+
raise "Cannot combine dotted and optional arguments" if count_optional_arguments > 0 && count_vararg_arguments > 0
|
|
54
|
+
raise "Found more than one dotted arguments" if count_vararg_arguments > 1
|
|
55
|
+
|
|
56
|
+
# --- 2) Parse option names ---------------------------------------
|
|
57
|
+
#
|
|
58
|
+
# Parse the usage message and extract option names, their short and
|
|
59
|
+
# long forms, and the associated argument name (if any), eg:
|
|
60
|
+
#
|
|
61
|
+
# Options:
|
|
62
|
+
# -s, --secure Use secure connection
|
|
63
|
+
# -v, --verbose Enable verbose output
|
|
64
|
+
# -r, --retries NUM Number of connection retries (default 3)
|
|
65
|
+
# -t, --timeout NUM Set connection timeout in seconds
|
|
66
|
+
|
|
67
|
+
@option_names = {}
|
|
68
|
+
@default_values = {}
|
|
69
|
+
|
|
70
|
+
options = inline_options + text.lines.grep(/^\s*--?\w/)
|
|
71
|
+
options.each do |string|
|
|
72
|
+
tokens = string.scan(/--?\w[\w-]*(?: \w+)?|,|\(default \S+\)|\S+/)
|
|
73
|
+
|
|
74
|
+
short_form = nil
|
|
75
|
+
long_form = nil
|
|
76
|
+
option_name = nil
|
|
77
|
+
argument_name = nil
|
|
78
|
+
default_value = nil
|
|
79
|
+
|
|
80
|
+
if /^-(\w)( \w+)?$/ === tokens.first
|
|
81
|
+
short_form, argument_name = tokens.shift.split
|
|
82
|
+
option_name = sanitize $1
|
|
83
|
+
tokens.shift if ',' === tokens.first
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if /^--([\w-]+)( \w+)?$/ === tokens.first
|
|
87
|
+
long_form, argument_name = tokens.shift.split
|
|
88
|
+
option_name = sanitize $1
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if /^\(default (\S+)\)$/ === tokens.last
|
|
92
|
+
default_value = $1
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
[short_form, long_form].compact.each do |each|
|
|
96
|
+
@option_names[each] = [option_name, argument_name]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
@default_values[option_name] = default_value if default_value
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def sanitize(string)
|
|
106
|
+
string.tr('^a-zA-Z0-9', '_').downcase.to_sym
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class OptionsByExample
|
|
4
|
-
VERSION = '3.
|
|
4
|
+
VERSION = '3.4.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
|
+
3.4.0
|
|
15
|
+
- Ensure default values are coerced too
|
|
16
|
+
- Print error message to stdout
|
|
17
|
+
- New method #expect_at_most_one_except (experimental)
|
|
18
|
+
- New method #expect_at_most_one_of (experimental)
|
|
19
|
+
|
|
20
|
+
3.3.0
|
|
21
|
+
- Expand dash-number to dash-n option
|
|
22
|
+
- Complete support for inline specification of options
|
|
23
|
+
- Support repeated arguments using dot-dot-dot
|
|
24
|
+
|
|
14
25
|
3.2.0
|
|
15
26
|
|
|
16
27
|
- New method #get returns argument value or nil
|
data/lib/options_by_example.rb
CHANGED
|
@@ -1,52 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'options_by_example/version'
|
|
4
|
-
require 'options_by_example/
|
|
4
|
+
require 'options_by_example/commandline_parser'
|
|
5
|
+
require 'options_by_example/usage_specification'
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class OptionsByExample
|
|
8
9
|
|
|
9
10
|
attr_reader :arguments
|
|
10
11
|
attr_reader :options
|
|
11
|
-
attr_reader :
|
|
12
|
+
attr_reader :usage_spec
|
|
12
13
|
|
|
13
14
|
def self.read(data)
|
|
14
15
|
return new data.read
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def initialize(text)
|
|
18
|
-
@
|
|
19
|
-
|
|
20
|
-
# --- 1) Parse argument names -------------------------------------
|
|
21
|
-
#
|
|
22
|
-
# Parse the usage string and extract both optional argument names
|
|
23
|
-
# and required argument names, for example:
|
|
24
|
-
#
|
|
25
|
-
# Usage: connect [options] [mode] host port
|
|
26
|
-
|
|
27
|
-
text =~ /Usage: (\$0|\w+)(?: \[options\])?((?: \[\w+\])*)((?: \w+)*)/
|
|
28
|
-
raise RuntimeError, "Expected usage string, got none" unless $1
|
|
29
|
-
@argument_names_optional = $2.to_s.split.map { |match| sanitize match.tr('[]', '') }
|
|
30
|
-
@argument_names_required = $3.to_s.split.map { |match| sanitize match }
|
|
31
|
-
|
|
32
|
-
# --- 2) Parse option names ---------------------------------------
|
|
33
|
-
#
|
|
34
|
-
# Parse the usage message and extract option names, their short and
|
|
35
|
-
# long forms, and the associated argument name (if any), eg:
|
|
36
|
-
#
|
|
37
|
-
# Options:
|
|
38
|
-
# -s, --secure Use secure connection
|
|
39
|
-
# -v, --verbose Enable verbose output
|
|
40
|
-
# -r, --retries NUM Number of connection retries (default 3)
|
|
41
|
-
# -t, --timeout NUM Set connection timeout in seconds
|
|
42
|
-
|
|
43
|
-
@option_names = {}
|
|
44
|
-
@default_values = {}
|
|
45
|
-
text.scan(/(?:(-\w), ?)?(--([\w-]+))(?: (\w+))?(?:.*\(default:? (\w+)\))?/) do
|
|
46
|
-
flags = [$1, $2].compact
|
|
47
|
-
flags.each { |each| @option_names[each] = [(sanitize $3), $4] }
|
|
48
|
-
@default_values[sanitize $3] = $5 if $5
|
|
49
|
-
end
|
|
19
|
+
@usage_spec = UsageSpecification.new(text)
|
|
50
20
|
|
|
51
21
|
initialize_argument_accessors
|
|
52
22
|
initialize_option_accessors
|
|
@@ -55,11 +25,21 @@ class OptionsByExample
|
|
|
55
25
|
def parse(argv)
|
|
56
26
|
parse_without_exit argv
|
|
57
27
|
rescue PrintUsageMessage
|
|
58
|
-
puts @
|
|
28
|
+
puts @usage_spec.message
|
|
59
29
|
exit 0
|
|
60
30
|
rescue RuntimeError => err
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
63
43
|
end
|
|
64
44
|
|
|
65
45
|
def fetch(*args, &block)
|
|
@@ -84,25 +64,19 @@ class OptionsByExample
|
|
|
84
64
|
private
|
|
85
65
|
|
|
86
66
|
def parse_without_exit(argv)
|
|
87
|
-
parser =
|
|
88
|
-
|
|
89
|
-
@argument_names_optional,
|
|
90
|
-
@default_values,
|
|
91
|
-
@option_names,
|
|
92
|
-
)
|
|
67
|
+
parser = CommandlineParser.new(@usage_spec)
|
|
68
|
+
parser.parse(argv)
|
|
93
69
|
|
|
94
|
-
parser.
|
|
95
|
-
@
|
|
96
|
-
@options = parser.options
|
|
70
|
+
@arguments = parser.argument_values
|
|
71
|
+
@options = parser.option_values
|
|
97
72
|
|
|
98
73
|
return self
|
|
99
74
|
end
|
|
100
75
|
|
|
101
76
|
def initialize_argument_accessors
|
|
102
77
|
[
|
|
103
|
-
*@
|
|
104
|
-
*@
|
|
105
|
-
*@option_names.values.select(&:last).map(&:first),
|
|
78
|
+
*@usage_spec.argument_names.keys,
|
|
79
|
+
*@usage_spec.option_names.values.select(&:last).map(&:first),
|
|
106
80
|
].each do |argument_name|
|
|
107
81
|
instance_eval %{
|
|
108
82
|
def argument_#{argument_name}
|
|
@@ -114,7 +88,7 @@ class OptionsByExample
|
|
|
114
88
|
end
|
|
115
89
|
|
|
116
90
|
def initialize_option_accessors
|
|
117
|
-
@option_names.each_value do |option_name, _|
|
|
91
|
+
@usage_spec.option_names.each_value do |option_name, _|
|
|
118
92
|
instance_eval %{
|
|
119
93
|
def include_#{option_name}?
|
|
120
94
|
@options.include? :#{option_name}
|
|
@@ -122,9 +96,5 @@ class OptionsByExample
|
|
|
122
96
|
}
|
|
123
97
|
end
|
|
124
98
|
end
|
|
125
|
-
|
|
126
|
-
def sanitize(string)
|
|
127
|
-
string.tr('^a-zA-Z0-9', '_').downcase.to_sym
|
|
128
|
-
end
|
|
129
99
|
end
|
|
130
100
|
|
metadata
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: options_by_example
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adrian Kuhn
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-02-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
|
-
description:
|
|
13
|
+
description:
|
|
14
14
|
email:
|
|
15
15
|
- akuhn@iam.unibe.ch
|
|
16
16
|
executables: []
|
|
@@ -19,7 +19,8 @@ extra_rdoc_files: []
|
|
|
19
19
|
files:
|
|
20
20
|
- README.md
|
|
21
21
|
- lib/options_by_example.rb
|
|
22
|
-
- lib/options_by_example/
|
|
22
|
+
- lib/options_by_example/commandline_parser.rb
|
|
23
|
+
- lib/options_by_example/usage_specification.rb
|
|
23
24
|
- lib/options_by_example/version.rb
|
|
24
25
|
homepage: https://github.com/akuhn/options_by_example
|
|
25
26
|
licenses:
|
|
@@ -28,7 +29,7 @@ metadata:
|
|
|
28
29
|
homepage_uri: https://github.com/akuhn/options_by_example
|
|
29
30
|
source_code_uri: https://github.com/akuhn/options_by_example
|
|
30
31
|
changelog_uri: https://github.com/akuhn/options_by_example/blob/master/lib/options_by_example/version.rb
|
|
31
|
-
post_install_message:
|
|
32
|
+
post_install_message:
|
|
32
33
|
rdoc_options: []
|
|
33
34
|
require_paths:
|
|
34
35
|
- lib
|
|
@@ -43,8 +44,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
43
44
|
- !ruby/object:Gem::Version
|
|
44
45
|
version: '0'
|
|
45
46
|
requirements: []
|
|
46
|
-
rubygems_version: 3.3.
|
|
47
|
-
signing_key:
|
|
47
|
+
rubygems_version: 3.0.3.1
|
|
48
|
+
signing_key:
|
|
48
49
|
specification_version: 4
|
|
49
50
|
summary: No-code options parser that extracts arguments directly from usage text.
|
|
50
51
|
test_files: []
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'date'
|
|
4
|
-
require 'time'
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class OptionsByExample
|
|
8
|
-
|
|
9
|
-
class PrintUsageMessage < StandardError
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
class Parser
|
|
13
|
-
|
|
14
|
-
attr_reader :options
|
|
15
|
-
attr_reader :arguments
|
|
16
|
-
|
|
17
|
-
def initialize(argument_names_required, argument_names_optional, default_values, option_names)
|
|
18
|
-
@argument_names_required = argument_names_required
|
|
19
|
-
@argument_names_optional = argument_names_optional
|
|
20
|
-
@default_values = default_values
|
|
21
|
-
@option_names = option_names
|
|
22
|
-
|
|
23
|
-
@arguments = @default_values.dup
|
|
24
|
-
@options = {}
|
|
25
|
-
end
|
|
26
|
-
|
|
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
|
-
@chunks = []
|
|
34
|
-
@remainder = current = []
|
|
35
|
-
array.each do |each|
|
|
36
|
-
@chunks << current = [] if each.start_with?(?-)
|
|
37
|
-
current << each
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
detect_help_option
|
|
41
|
-
flatten_stacked_shorthand_options
|
|
42
|
-
detect_unknown_options
|
|
43
|
-
parse_options
|
|
44
|
-
|
|
45
|
-
validate_number_of_arguments
|
|
46
|
-
parse_required_arguments
|
|
47
|
-
parse_optional_arguments
|
|
48
|
-
|
|
49
|
-
raise "Internal error: unreachable state" unless @remainder.empty?
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
private
|
|
53
|
-
|
|
54
|
-
def detect_help_option
|
|
55
|
-
@chunks.each do |option, *args|
|
|
56
|
-
case option
|
|
57
|
-
when '-h', '--help'
|
|
58
|
-
raise PrintUsageMessage
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def flatten_stacked_shorthand_options
|
|
64
|
-
|
|
65
|
-
# Expand any combined shorthand options like -svt into their
|
|
66
|
-
# separate components (-s, -v, and -t) and assigns any arguments
|
|
67
|
-
# to the last component. If an unknown shorthand is found, raise
|
|
68
|
-
# a helpful error message with suggestion if possible.
|
|
69
|
-
|
|
70
|
-
list = []
|
|
71
|
-
@chunks.each do |option, *args|
|
|
72
|
-
if option =~ /^-([a-zA-Z]{2,})$/
|
|
73
|
-
shorthands = $1.each_char.map { |char| "-#{char}" }
|
|
74
|
-
|
|
75
|
-
shorthands.each do |each|
|
|
76
|
-
if not @option_names.include?(each)
|
|
77
|
-
did_you_mean = ", did you mean '-#{option}'?" if @option_names.include?("-#{option}")
|
|
78
|
-
raise "Found unknown option #{each} inside '#{option}'#{did_you_mean}"
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
list.concat shorthands.map { |each| [each] }
|
|
83
|
-
list.last.concat args
|
|
84
|
-
else
|
|
85
|
-
list << [option, *args]
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
@chunks = list
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def detect_unknown_options
|
|
93
|
-
@chunks.each do |option, *args|
|
|
94
|
-
raise "Found unknown option '#{option}'" unless @option_names.include?(option)
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def parse_options
|
|
99
|
-
@chunks.each do |option, *args|
|
|
100
|
-
if @remainder.any?
|
|
101
|
-
raise "Unexpected arguments found before option '#{option}', please provide all options before arguments"
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
option_name, argument_name = @option_names[option]
|
|
105
|
-
@options[option_name] = true
|
|
106
|
-
|
|
107
|
-
if argument_name
|
|
108
|
-
raise "Expected argument for option '#{option}', got none" if args.empty?
|
|
109
|
-
value = args.shift
|
|
110
|
-
|
|
111
|
-
begin
|
|
112
|
-
case argument_name
|
|
113
|
-
when 'NUM'
|
|
114
|
-
expected_type = 'an integer value'
|
|
115
|
-
value = Integer value
|
|
116
|
-
when 'DATE'
|
|
117
|
-
expected_type = 'a date (e.g. YYYY-MM-DD)'
|
|
118
|
-
value = Date.parse value
|
|
119
|
-
when 'TIME'
|
|
120
|
-
expected_type = 'a timestamp (e.g. HH:MM:SS)'
|
|
121
|
-
value = Time.parse value
|
|
122
|
-
end
|
|
123
|
-
rescue ArgumentError
|
|
124
|
-
raise "Invalid argument \"#{value}\" for option '#{option}', please provide #{expected_type}"
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
@arguments[option_name] = value
|
|
128
|
-
@option_took_argument = option
|
|
129
|
-
else
|
|
130
|
-
@option_took_argument = nil
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
@remainder = args
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def validate_number_of_arguments
|
|
138
|
-
min_length = @argument_names_required.size
|
|
139
|
-
max_length = @argument_names_optional.size + min_length
|
|
140
|
-
|
|
141
|
-
if @remainder.size > max_length
|
|
142
|
-
range = [min_length, max_length].uniq.join(?-)
|
|
143
|
-
raise "Expected #{range} arguments, but received too many"
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
if @remainder.size < min_length
|
|
147
|
-
too_few = @remainder.empty? ? 'none' : (@remainder.size == 1 ? 'only one' : 'too few')
|
|
148
|
-
remark = " (considering #{@option_took_argument} takes an argument)" if @option_took_argument
|
|
149
|
-
raise "Expected #{min_length} required arguments, but received #{too_few}#{remark}"
|
|
150
|
-
end
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def parse_required_arguments
|
|
154
|
-
stash = @remainder.pop(@argument_names_required.length)
|
|
155
|
-
@argument_names_required.each do |argument_name|
|
|
156
|
-
raise "Missing required argument '#{argument_name}'" if stash.empty?
|
|
157
|
-
@arguments[argument_name] = stash.shift
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def parse_optional_arguments
|
|
162
|
-
@argument_names_optional.each do |argument_name|
|
|
163
|
-
break if @remainder.empty?
|
|
164
|
-
@arguments[argument_name] = @remainder.shift
|
|
165
|
-
end
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
end
|