options_by_example 3.2.0 → 3.3.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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ced46483d8abae62122bb45cd0d4753408c4d93e08405a3409583ec9fcfb477
|
|
4
|
+
data.tar.gz: e8484baa8976c72154fb2da2509482d985501c08960e7e67fdf04731e307787b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22f691bf1fc36e14868996b32b15de17a1760add666aa046e6dc57fb0e09f8dad75c7c18260255930c88ea3e56a1c994285dd320a659e6c9ee92fc4ae45d3f10
|
|
7
|
+
data.tar.gz: 1d54dca36e9829f2018af7493586e18ca7d590819d112e28482c93684212adc4ccd53e5c5183d49885c927f4ddb9e23c0654874ddbceb0e617f95c6fc6b96258
|
|
@@ -9,19 +9,18 @@ class OptionsByExample
|
|
|
9
9
|
class PrintUsageMessage < StandardError
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
class
|
|
12
|
+
class CommandlineParser
|
|
13
13
|
|
|
14
|
-
attr_reader :
|
|
15
|
-
attr_reader :
|
|
14
|
+
attr_reader :option_values
|
|
15
|
+
attr_reader :argument_values
|
|
16
16
|
|
|
17
|
-
def initialize(
|
|
18
|
-
@
|
|
19
|
-
@
|
|
20
|
-
@
|
|
21
|
-
@option_names = option_names
|
|
17
|
+
def initialize(usage)
|
|
18
|
+
@argument_names = usage.argument_names
|
|
19
|
+
@default_values = usage.default_values
|
|
20
|
+
@option_names = usage.option_names
|
|
22
21
|
|
|
23
|
-
@
|
|
24
|
-
@
|
|
22
|
+
@argument_values = @default_values.dup
|
|
23
|
+
@option_values = {}
|
|
25
24
|
end
|
|
26
25
|
|
|
27
26
|
def parse(array)
|
|
@@ -30,16 +29,17 @@ class OptionsByExample
|
|
|
30
29
|
# chunks, plus tracking leading excess arguments. This organization
|
|
31
30
|
# facilitates further processing and validation of the input.
|
|
32
31
|
|
|
33
|
-
@
|
|
32
|
+
@slices = []
|
|
34
33
|
@remainder = current = []
|
|
35
34
|
array.each do |each|
|
|
36
|
-
@
|
|
35
|
+
@slices << current = [] if each.start_with?(?-)
|
|
37
36
|
current << each
|
|
38
37
|
end
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
raise_if_help_option
|
|
40
|
+
unpack_combined_shorthand_options
|
|
41
|
+
expand_dash_number_to_dash_n_option
|
|
42
|
+
raise_if_unknown_options
|
|
43
43
|
parse_options
|
|
44
44
|
|
|
45
45
|
validate_number_of_arguments
|
|
@@ -51,8 +51,8 @@ class OptionsByExample
|
|
|
51
51
|
|
|
52
52
|
private
|
|
53
53
|
|
|
54
|
-
def
|
|
55
|
-
@
|
|
54
|
+
def raise_if_help_option
|
|
55
|
+
@slices.each do |option, *args|
|
|
56
56
|
case option
|
|
57
57
|
when '-h', '--help'
|
|
58
58
|
raise PrintUsageMessage
|
|
@@ -60,7 +60,7 @@ class OptionsByExample
|
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
def
|
|
63
|
+
def unpack_combined_shorthand_options
|
|
64
64
|
|
|
65
65
|
# Expand any combined shorthand options like -svt into their
|
|
66
66
|
# separate components (-s, -v, and -t) and assigns any arguments
|
|
@@ -68,7 +68,7 @@ class OptionsByExample
|
|
|
68
68
|
# a helpful error message with suggestion if possible.
|
|
69
69
|
|
|
70
70
|
list = []
|
|
71
|
-
@
|
|
71
|
+
@slices.each do |option, *args|
|
|
72
72
|
if option =~ /^-([a-zA-Z]{2,})$/
|
|
73
73
|
shorthands = $1.each_char.map { |char| "-#{char}" }
|
|
74
74
|
|
|
@@ -86,23 +86,31 @@ class OptionsByExample
|
|
|
86
86
|
end
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
-
@
|
|
89
|
+
@slices = list
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
def
|
|
93
|
-
@
|
|
92
|
+
def expand_dash_number_to_dash_n_option
|
|
93
|
+
@slices.each do |each|
|
|
94
|
+
if each.first =~ /^-(\d+)$/
|
|
95
|
+
each[0..0] = ['-n', $1]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def raise_if_unknown_options
|
|
101
|
+
@slices.each do |option, *args|
|
|
94
102
|
raise "Found unknown option '#{option}'" unless @option_names.include?(option)
|
|
95
103
|
end
|
|
96
104
|
end
|
|
97
105
|
|
|
98
106
|
def parse_options
|
|
99
|
-
@
|
|
107
|
+
@slices.each do |option, *args|
|
|
100
108
|
if @remainder.any?
|
|
101
109
|
raise "Unexpected arguments found before option '#{option}', please provide all options before arguments"
|
|
102
110
|
end
|
|
103
111
|
|
|
104
112
|
option_name, argument_name = @option_names[option]
|
|
105
|
-
@
|
|
113
|
+
@option_values[option_name] = true
|
|
106
114
|
|
|
107
115
|
if argument_name
|
|
108
116
|
raise "Expected argument for option '#{option}', got none" if args.empty?
|
|
@@ -124,7 +132,7 @@ class OptionsByExample
|
|
|
124
132
|
raise "Invalid argument \"#{value}\" for option '#{option}', please provide #{expected_type}"
|
|
125
133
|
end
|
|
126
134
|
|
|
127
|
-
@
|
|
135
|
+
@argument_values[option_name] = value
|
|
128
136
|
@option_took_argument = option
|
|
129
137
|
else
|
|
130
138
|
@option_took_argument = nil
|
|
@@ -135,10 +143,14 @@ class OptionsByExample
|
|
|
135
143
|
end
|
|
136
144
|
|
|
137
145
|
def validate_number_of_arguments
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
count_optional_arguments = @argument_names.values.count(:optional)
|
|
147
|
+
count_required_arguments = @argument_names.values.count(:required)
|
|
148
|
+
count_vararg_arguments = @argument_names.values.count(:vararg)
|
|
149
|
+
|
|
150
|
+
min_length = count_required_arguments + count_vararg_arguments
|
|
151
|
+
max_length = count_required_arguments + count_optional_arguments
|
|
140
152
|
|
|
141
|
-
if @remainder.size > max_length
|
|
153
|
+
if @remainder.size > max_length && count_vararg_arguments == 0
|
|
142
154
|
range = [min_length, max_length].uniq.join(?-)
|
|
143
155
|
raise "Expected #{range} arguments, but received too many"
|
|
144
156
|
end
|
|
@@ -151,17 +163,35 @@ class OptionsByExample
|
|
|
151
163
|
end
|
|
152
164
|
|
|
153
165
|
def parse_required_arguments
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
166
|
+
if @argument_names.values.include?(:vararg)
|
|
167
|
+
remaining_arguments = @argument_names.length
|
|
168
|
+
@argument_names.each do |argument_name, arity|
|
|
169
|
+
raise "unreachable" if @remainder.empty?
|
|
170
|
+
remaining_arguments -= 1
|
|
171
|
+
case arity
|
|
172
|
+
when :required
|
|
173
|
+
@argument_values[argument_name] = @remainder.shift
|
|
174
|
+
when :vararg
|
|
175
|
+
@argument_values[argument_name] = @remainder.shift(@remainder.length - remaining_arguments)
|
|
176
|
+
else
|
|
177
|
+
raise "unreachable"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
return
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
@argument_names.reverse_each do |argument_name, arity|
|
|
184
|
+
break if arity == :optional
|
|
185
|
+
raise "unreachable" if @remainder.empty?
|
|
186
|
+
@argument_values[argument_name] = @remainder.pop
|
|
158
187
|
end
|
|
159
188
|
end
|
|
160
189
|
|
|
161
190
|
def parse_optional_arguments
|
|
162
|
-
@
|
|
191
|
+
@argument_names.each do |argument_name, arity|
|
|
192
|
+
break unless arity == :optional
|
|
163
193
|
break if @remainder.empty?
|
|
164
|
-
@
|
|
194
|
+
@argument_values[argument_name] = @remainder.shift
|
|
165
195
|
end
|
|
166
196
|
end
|
|
167
197
|
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.3.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
|
+
3.3.0
|
|
15
|
+
- Expand dash-number to dash-n option
|
|
16
|
+
- Complete support for inline specification of options
|
|
17
|
+
- Support repeated arguments using dot-dot-dot
|
|
18
|
+
|
|
14
19
|
3.2.0
|
|
15
20
|
|
|
16
21
|
- 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,7 +25,7 @@ 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
31
|
puts "ERROR: #{err.message}"
|
|
@@ -84,25 +54,19 @@ class OptionsByExample
|
|
|
84
54
|
private
|
|
85
55
|
|
|
86
56
|
def parse_without_exit(argv)
|
|
87
|
-
parser =
|
|
88
|
-
|
|
89
|
-
@argument_names_optional,
|
|
90
|
-
@default_values,
|
|
91
|
-
@option_names,
|
|
92
|
-
)
|
|
57
|
+
parser = CommandlineParser.new(@usage_spec)
|
|
58
|
+
parser.parse(argv)
|
|
93
59
|
|
|
94
|
-
parser.
|
|
95
|
-
@
|
|
96
|
-
@options = parser.options
|
|
60
|
+
@arguments = parser.argument_values
|
|
61
|
+
@options = parser.option_values
|
|
97
62
|
|
|
98
63
|
return self
|
|
99
64
|
end
|
|
100
65
|
|
|
101
66
|
def initialize_argument_accessors
|
|
102
67
|
[
|
|
103
|
-
*@
|
|
104
|
-
*@
|
|
105
|
-
*@option_names.values.select(&:last).map(&:first),
|
|
68
|
+
*@usage_spec.argument_names.keys,
|
|
69
|
+
*@usage_spec.option_names.values.select(&:last).map(&:first),
|
|
106
70
|
].each do |argument_name|
|
|
107
71
|
instance_eval %{
|
|
108
72
|
def argument_#{argument_name}
|
|
@@ -114,7 +78,7 @@ class OptionsByExample
|
|
|
114
78
|
end
|
|
115
79
|
|
|
116
80
|
def initialize_option_accessors
|
|
117
|
-
@option_names.each_value do |option_name, _|
|
|
81
|
+
@usage_spec.option_names.each_value do |option_name, _|
|
|
118
82
|
instance_eval %{
|
|
119
83
|
def include_#{option_name}?
|
|
120
84
|
@options.include? :#{option_name}
|
|
@@ -122,9 +86,5 @@ class OptionsByExample
|
|
|
122
86
|
}
|
|
123
87
|
end
|
|
124
88
|
end
|
|
125
|
-
|
|
126
|
-
def sanitize(string)
|
|
127
|
-
string.tr('^a-zA-Z0-9', '_').downcase.to_sym
|
|
128
|
-
end
|
|
129
89
|
end
|
|
130
90
|
|
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.3.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-01 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: []
|