arg-parser 0.1
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.
- data/LICENSE +22 -0
- data/README.md +80 -0
- data/lib/arg-parser.rb +5 -0
- data/lib/arg-parser/argument.rb +332 -0
- data/lib/arg-parser/definition.rb +422 -0
- data/lib/arg-parser/dsl.rb +151 -0
- data/lib/arg-parser/parser.rb +237 -0
- data/lib/arg_parser.rb +2 -0
- metadata +54 -0
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013, Adam Gardiner
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
* Redistributions of source code must retain the above copyright notice, this
|
8
|
+
list of conditions and the following disclaimer.
|
9
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
11
|
+
and/or other materials provided with the distribution.
|
12
|
+
|
13
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
14
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
15
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
16
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
17
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
18
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
19
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
20
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
21
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
22
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# ArgParser
|
2
|
+
|
3
|
+
ArgParser is a small library for parsing command-line arguments (or indeed any string or Array of text).
|
4
|
+
It provides a simple DSL for defining the possible arguments, which may be one of the following:
|
5
|
+
* Positional arguments, where values are specified without any keyword, and their meaning is based on the
|
6
|
+
order in which they appear in the command-line.
|
7
|
+
* Keyword arguments, which are identified by a long- or short-key preceding the value.
|
8
|
+
* Flag arguments, which are essentially boolean keyword arguments. The presence of the key implies a true
|
9
|
+
value.
|
10
|
+
* A Rest argument, which is an argument definition that takes 0 or more trailing positional arguments.
|
11
|
+
|
12
|
+
## Usage
|
13
|
+
|
14
|
+
ArgParser is supplied as a gem, and has no dependencies. To use it, simply:
|
15
|
+
```
|
16
|
+
gem install arg-parser
|
17
|
+
```
|
18
|
+
|
19
|
+
ArgParser provides a DSL module that can be included into any class to provide argument parsing.
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
require 'arg-parser'
|
23
|
+
|
24
|
+
class MyClass
|
25
|
+
|
26
|
+
include ArgParser::DSL
|
27
|
+
|
28
|
+
purpose <<-EOT
|
29
|
+
This is where you specify the purpose of your program. It will be displayed in the
|
30
|
+
generated help screen if you pass /? or --help on the command-line.
|
31
|
+
EOT
|
32
|
+
|
33
|
+
positional_arg :my_positional_arg, 'This is a positional arg'
|
34
|
+
keyword_arg :my_keyword_arg, 'This is a keyword arg'
|
35
|
+
flag_arg :flag, 'This is a flag argument'
|
36
|
+
rest_arg :files, 'This is where we specify that remaining args will be collected in an array'
|
37
|
+
|
38
|
+
|
39
|
+
def run
|
40
|
+
if opts = parse_arguments
|
41
|
+
# Do something with opts.my_positional_arg, opts.my_keyword_arg,
|
42
|
+
# opts.flag, and opts.files
|
43
|
+
# ...
|
44
|
+
else
|
45
|
+
show_help? ? show_help : show_usage
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
MyClass.new.run
|
53
|
+
|
54
|
+
```
|
55
|
+
|
56
|
+
## Functionality
|
57
|
+
|
58
|
+
ArgParser provides a fairly broad range of functionality for argument parsing. Following is a non-exhaustive
|
59
|
+
list of features:
|
60
|
+
* Built-in usage and help display
|
61
|
+
* Mandatory vs Optional: All arguments your program accepts can be defined as optional or mandatory.
|
62
|
+
By default, positional arguments are considered mandatory, and all others default to optional. To change
|
63
|
+
the default, simply specify `required: true` or `required: false` when defining the argument.
|
64
|
+
* Short-keys and long-keys: Arguments are defined with a long_key name which will be used to access the
|
65
|
+
parsed value in the results. However, arguments can also define a single letter or digit short-key form
|
66
|
+
which can be used as an alternate means for indicating a value. To define a short key, simply pass
|
67
|
+
`short_key: '<letter_or_digit>'` when defining an argument.
|
68
|
+
* Validation: Arguments can define validation requirements that must be satisfied. This can take several
|
69
|
+
forms:
|
70
|
+
- List of values: Pass an array containing the allowed values the argument can take.
|
71
|
+
`validation: %w{one two three}`
|
72
|
+
- Regular expression: Pass a regular expression that the argument value must satisfy.
|
73
|
+
`validation: /.*\.rb$/`
|
74
|
+
- Proc: Pass a proc that will be called to validate the supplied argument value. If the proc returns
|
75
|
+
a non-falsey value, the argument is accepted, otherwise it is rejected.
|
76
|
+
`validation: lambda{ |val, arg, hsh| val.upcase == 'TRUE' }`
|
77
|
+
* On-parse handler: A proc can be passed that will be called when the argument value is encountered
|
78
|
+
during parsing. The return value of the proc will be used as the argument result.
|
79
|
+
`on_parse: lambda{ |val, arg, hsh| val.split(',') }`
|
80
|
+
|
data/lib/arg-parser.rb
ADDED
@@ -0,0 +1,332 @@
|
|
1
|
+
# Namespace for classes defined by ArgParser, the command-line argument parser.
|
2
|
+
module ArgParser
|
3
|
+
|
4
|
+
# Hash containing registered handlers for :on_parse options
|
5
|
+
OnParseHandlers = {
|
6
|
+
:split_to_array => lambda{ |val, arg, hsh| val.split(',') }
|
7
|
+
}
|
8
|
+
|
9
|
+
|
10
|
+
# Abstract base class of all command-line argument types.
|
11
|
+
#
|
12
|
+
# @abstract
|
13
|
+
class Argument
|
14
|
+
|
15
|
+
# The key used to identify this argument value in the parsed command-
|
16
|
+
# line results Struct.
|
17
|
+
# @return [Symbol] the key/method by which this argument can be retrieved
|
18
|
+
# from the parse result Struct.
|
19
|
+
attr_reader :key
|
20
|
+
# @return [String] the description for this argument, which will be shown
|
21
|
+
# in the usage display.
|
22
|
+
attr_reader :description
|
23
|
+
# @return [Symbol] a single letter or digit that can be used as a short
|
24
|
+
# alternative to the full key to identify an argument value in a command-
|
25
|
+
# line.
|
26
|
+
attr_reader :short_key
|
27
|
+
# @return [Boolean] whether this argument is a required (i.e. mandatory)
|
28
|
+
# argument. Mandatory arguments that do not get specified result in a
|
29
|
+
# ParseException.
|
30
|
+
attr_accessor :required
|
31
|
+
alias_method :required?, :required
|
32
|
+
# @return [String] the default value for the argument, returned in the
|
33
|
+
# command-line parse results if no other value is specified.
|
34
|
+
attr_accessor :default
|
35
|
+
# An optional on_parse callback handler. The supplied block/Proc will be
|
36
|
+
# called after this argument has been parsed, with three arguments:
|
37
|
+
# @param [String] The value from the command-line that was entered for
|
38
|
+
# this argument.
|
39
|
+
# @param [Argument] The Argument sub-class object that represents the
|
40
|
+
# argument that was parsed.
|
41
|
+
# @param [Hash] The results Hash containing the argument keys and their
|
42
|
+
# values parsed so far.
|
43
|
+
# @return [Proc] the user supplied block to be called when the argument
|
44
|
+
# has been parsed.
|
45
|
+
attr_accessor :on_parse
|
46
|
+
# @return [String] a label to use for a new section of options in the
|
47
|
+
# argument usage display. Should be specified on the first argument in
|
48
|
+
# the group.
|
49
|
+
attr_accessor :usage_break
|
50
|
+
|
51
|
+
|
52
|
+
# Converts an argument key specification into a valid key, by stripping
|
53
|
+
# leading dashes, converting remaining dashes to underscores, and lower-
|
54
|
+
# casing all text. This is required to ensure the key name will be a
|
55
|
+
# valid accessor name on the parse results.
|
56
|
+
#
|
57
|
+
# @return [Symbol] the key by which an argument can be retrieved from
|
58
|
+
# the arguments definition, and the parse results.
|
59
|
+
def self.to_key(label)
|
60
|
+
label.to_s.gsub(/^-+/, '').gsub('-', '_').downcase.intern
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def initialize(key, desc, opts = {}, &block)
|
67
|
+
@key = self.class.to_key(key)
|
68
|
+
@description = desc
|
69
|
+
@default = opts[:default]
|
70
|
+
@on_parse = block || opts[:on_parse]
|
71
|
+
if @on_parse.is_a?(Symbol)
|
72
|
+
op = opts[:on_parse]
|
73
|
+
@on_parse = case
|
74
|
+
when OnParseHandlers.has_key?(op)
|
75
|
+
OnParseHandlers[op]
|
76
|
+
when "".respond_to?(op)
|
77
|
+
lambda{ |val, arg, hsh| val.send(op) }
|
78
|
+
else
|
79
|
+
raise ArgumentError, "No on_parse handler registered for #{op.inspect}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
@usage_break = opts[:usage_break]
|
83
|
+
if sk = opts[:short_key]
|
84
|
+
if sk =~ /^-?([a-z0-9])$/i
|
85
|
+
@short_key = $1.intern
|
86
|
+
else
|
87
|
+
raise ArgumentError, "An argument short key must be a single digit or letter"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
# Abstract base class of arguments that take a value (i.e. positional and
|
96
|
+
# keyword arguments).
|
97
|
+
#
|
98
|
+
# @abstract
|
99
|
+
class ValueArgument < Argument
|
100
|
+
|
101
|
+
# @return [Boolean] Flag indicating that the value for this argument is
|
102
|
+
# a sensitive value (e.g. a password) that should not be displayed.
|
103
|
+
attr_accessor :sensitive
|
104
|
+
alias_method :sensitive?, :sensitive
|
105
|
+
# @return [Array, Regexp, Proc] An optional validation that will be
|
106
|
+
# applied to the argument value for this argument, to determine if it
|
107
|
+
# is valid. The validation can take the following forms:
|
108
|
+
# @param [Array] If an Array is specified, the supplied value will be
|
109
|
+
# checked to verify it is one of the allowed values specified in the
|
110
|
+
# Array.
|
111
|
+
# @param [Regexp] If a Regexp object is supplied, the argument value
|
112
|
+
# will be tested against the Regexp to verify it is valid.
|
113
|
+
# @param [Proc] The most flexible option; the supplied value, this
|
114
|
+
# ValueArgument sub-class object, and the parse results (thus far)
|
115
|
+
# will be passed to the Proc for validation. The Proc must return a
|
116
|
+
# non-falsey value for the argument to be accepted.
|
117
|
+
attr_accessor :validation
|
118
|
+
# @return [String] A label that will be used in the usage string printed
|
119
|
+
# for this ValueArgument. If not specified, defaults to the upper-case
|
120
|
+
# value of the argument key. For example, if the argument key is :foo_bar,
|
121
|
+
# the default usage value for this argument would be FOO-BAR, as in:
|
122
|
+
# Usage:
|
123
|
+
# my-prog.rb FOO-BAR
|
124
|
+
attr_accessor :usage_value
|
125
|
+
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
def initialize(key, desc, opts = {}, &block)
|
130
|
+
super
|
131
|
+
@sensitive = opts[:sensitive]
|
132
|
+
@validation = opts[:validation]
|
133
|
+
@usage_value = opts.fetch(:usage_value, key.to_s.gsub('_', '-').upcase)
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
|
139
|
+
# An argument that is set by position on the command-line. PositionalArguments
|
140
|
+
# do not require a --key to be specified before the argument value; they are
|
141
|
+
# typically used when there are a small number of mandatory arguments.
|
142
|
+
#
|
143
|
+
# Positional arguments still have a key that is used to identify the parsed
|
144
|
+
# argument value in the results Struct. As such, it is not an error for a
|
145
|
+
# positional argument to be specified with its key - its just not mandatory
|
146
|
+
# for the key to be provided.
|
147
|
+
class PositionalArgument < ValueArgument
|
148
|
+
|
149
|
+
# Creates a new positional argument, which is an argument value that may
|
150
|
+
# be specified without a keyword, in which case it is matched to the
|
151
|
+
# available positional arguments by its position on the command-line.
|
152
|
+
#
|
153
|
+
# @param [Symbol] key The name that will be used for the accessor used
|
154
|
+
# to return this argument value in the parse results.
|
155
|
+
# @param [String] desc A description for this argument. Appears in the
|
156
|
+
# help output that is generated when the user specifies the --help or
|
157
|
+
# /? flags on the command-line.
|
158
|
+
# @param [Hash] opts Contains any options that are desired for this
|
159
|
+
# argument.
|
160
|
+
# @yield [val, arg, hsh] If supplied, the block passed will be invoked
|
161
|
+
# after this argument value has been parsed from the command-line.
|
162
|
+
# Blocks are usually used when the value to be returned needs to be
|
163
|
+
# converted from a String to some other type.
|
164
|
+
# @yieldparam val [String] the String value read from the command-line
|
165
|
+
# for this argument
|
166
|
+
# @yieldparam arg [PositionalArgument] this argument definition
|
167
|
+
# @yieldparam hsh [Hash] a Hash containing the argument values parsed
|
168
|
+
# so far.
|
169
|
+
# @yieldreturn [Object] the return value from the block will be used as
|
170
|
+
# the argument value parsed from the command-line for this argument.
|
171
|
+
def initialize(key, desc, opts = {}, &block)
|
172
|
+
super
|
173
|
+
@required = opts.fetch(:required, !opts.has_key?(:default))
|
174
|
+
end
|
175
|
+
|
176
|
+
# @return [String] the word that will appear in the help display for
|
177
|
+
# this argument.
|
178
|
+
def to_s
|
179
|
+
usage_value
|
180
|
+
end
|
181
|
+
|
182
|
+
# @return [String] the string for this argument position in a command-line
|
183
|
+
# usage display.
|
184
|
+
def to_use
|
185
|
+
required? ? usage_value : "[#{usage_value}]"
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|
189
|
+
|
190
|
+
|
191
|
+
# An argument that is specified via a keyword prefix; typically used
|
192
|
+
# for optional arguments, although Keyword arguments can also be used for
|
193
|
+
# mandatory arguments where there is no natural ordering of arguments.
|
194
|
+
class KeywordArgument < ValueArgument
|
195
|
+
|
196
|
+
# Whether the keyword argument must be specified with a non-missing
|
197
|
+
# value.
|
198
|
+
# @return [Boolean] true if the keyword can be specified without a value.
|
199
|
+
attr_accessor :value_optional
|
200
|
+
alias_method :value_optional?, :value_optional
|
201
|
+
|
202
|
+
|
203
|
+
# Creates a KeywordArgument, which is an argument that must be specified
|
204
|
+
# on a command-line using either a long form key (i.e. --key), or
|
205
|
+
# optionally, a short-form key (i.e. -k) should one be defined for this
|
206
|
+
# argument.
|
207
|
+
# @param key [Symbol] the key that will be used to identify this argument
|
208
|
+
# value in the parse results.
|
209
|
+
# @param desc [String] the description of this argument, displayed in the
|
210
|
+
# generated help screen.
|
211
|
+
# @param opts [Hash] a hash of options that govern the behaviour of this
|
212
|
+
# argument.
|
213
|
+
# @option opts [Boolean] :required whether the keyword argument is a required
|
214
|
+
# argument that must appear in the command-line. Defaults to false.
|
215
|
+
# @option opts [Boolean] :value_optional whether the keyword argument can be
|
216
|
+
# specified without a value. For example, a keyword argument might
|
217
|
+
# be used both as a flag, and to override a default value. Specifying
|
218
|
+
# the argument without a value would signify that the option is set,
|
219
|
+
# but the default value for the option should be used. Defaults to
|
220
|
+
# false (keyword argument cannot be specified without a value).
|
221
|
+
def initialize(key, desc, opts = {}, &block)
|
222
|
+
super
|
223
|
+
@required = opts.fetch(:required, false)
|
224
|
+
@value_optional = opts.fetch(:value_optional, false)
|
225
|
+
end
|
226
|
+
|
227
|
+
def to_s
|
228
|
+
"--#{key}".gsub('_', '-')
|
229
|
+
end
|
230
|
+
|
231
|
+
def to_use
|
232
|
+
sk = short_key ? "-#{short_key}, " : ''
|
233
|
+
uv = value_optional ? "[#{usage_value}]" : usage_value
|
234
|
+
"#{sk}#{self.to_s} #{uv}"
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
|
239
|
+
|
240
|
+
# A boolean argument that is set if its key is encountered on the command-line.
|
241
|
+
# Flag arguments normally default to false, and become true if the argument
|
242
|
+
# key is specified. However, it is also possible to define a flag argument
|
243
|
+
# that defaults to true, in which case the option can be disabled by pre-
|
244
|
+
# pending the argument key with a 'no-' prefix, e.g. --no-export can be
|
245
|
+
# specified to disable the normally enabled --export flag.
|
246
|
+
class FlagArgument < Argument
|
247
|
+
|
248
|
+
# Creates a new flag argument, which is an argument with a boolean value.
|
249
|
+
#
|
250
|
+
# @param [Symbol] key The name that will be used for the accessor used
|
251
|
+
# to return this argument value in the parse results.
|
252
|
+
# @param [String] desc A description for this argument. Appears in the
|
253
|
+
# help output that is generated when the user specifies the --help or
|
254
|
+
# /? flags on the command-line.
|
255
|
+
# @param [Hash] opts Contains any options that are desired for this
|
256
|
+
# argument.
|
257
|
+
# @param [Block] block If supplied, the block passed will be invoked
|
258
|
+
# after this argument value has been parsed from the command-line.
|
259
|
+
# The block will be called with three arguments: this argument
|
260
|
+
# definition, the String value read from the command-line for this
|
261
|
+
# argument, and a Hash containing the argument values parsed so far.
|
262
|
+
# The return value from the block will be used as the argument value
|
263
|
+
# parsed from the command-line for this argument. Blocks are usually
|
264
|
+
# used when the value to be returned needs to be converted from a
|
265
|
+
# String to some other type.
|
266
|
+
def initialize(key, desc, opts = {}, &block)
|
267
|
+
super
|
268
|
+
end
|
269
|
+
|
270
|
+
def required
|
271
|
+
false
|
272
|
+
end
|
273
|
+
|
274
|
+
def to_s
|
275
|
+
"--#{self.default ? 'no-' : ''}#{key}".gsub('_', '-')
|
276
|
+
end
|
277
|
+
|
278
|
+
def to_use
|
279
|
+
sk = short_key ? "-#{short_key}, " : ''
|
280
|
+
"#{sk}#{self.to_s}"
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
284
|
+
|
285
|
+
|
286
|
+
# A command-line argument that takes 0 to N values from the command-line.
|
287
|
+
class RestArgument < ValueArgument
|
288
|
+
|
289
|
+
# Creates a new rest argument, which is an argument that consumes all
|
290
|
+
# remaining positional argument values.
|
291
|
+
#
|
292
|
+
# @param [Symbol] key The name that will be used for the accessor used
|
293
|
+
# to return this argument value in the parse results.
|
294
|
+
# @param [String] desc A description for this argument. Appears in the
|
295
|
+
# help output that is generated when the user specifies the --help or
|
296
|
+
# /? flags on the command-line.
|
297
|
+
# @param [Hash] opts Contains any options that are desired for this
|
298
|
+
# argument.
|
299
|
+
# @param [Block] block If supplied, the block passed will be invoked
|
300
|
+
# after this argument value has been parsed from the command-line.
|
301
|
+
# The block will be called with three arguments: this argument
|
302
|
+
# definition, the String value read from the command-line for this
|
303
|
+
# argument, and a Hash containing the argument values parsed so far.
|
304
|
+
# The return value from the block will be used as the argument value
|
305
|
+
# parsed from the command-line for this argument. Blocks are usually
|
306
|
+
# used when the value to be returned needs to be converted from a
|
307
|
+
# String to some other type.
|
308
|
+
def initialize(key, desc, opts = {}, &block)
|
309
|
+
super
|
310
|
+
@min_values = opts.fetch(:min_values, opts.fetch(:required, true) ? 1 : 0)
|
311
|
+
end
|
312
|
+
|
313
|
+
def required
|
314
|
+
@min_values > 0
|
315
|
+
end
|
316
|
+
|
317
|
+
# @return [String] the word that will appear in the help display for
|
318
|
+
# this argument.
|
319
|
+
def to_s
|
320
|
+
usage_value
|
321
|
+
end
|
322
|
+
|
323
|
+
# @return [String] The string for this argument position in a command-line.
|
324
|
+
# usage display.
|
325
|
+
def to_use
|
326
|
+
required? ? "#{usage_value} [...]" : "[#{usage_value} [...]]"
|
327
|
+
end
|
328
|
+
|
329
|
+
end
|
330
|
+
|
331
|
+
end
|
332
|
+
|
@@ -0,0 +1,422 @@
|
|
1
|
+
module ArgParser
|
2
|
+
|
3
|
+
# Exeption thrown when an attempt is made to access an argument that is not
|
4
|
+
# defined.
|
5
|
+
class NoSuchArgumentError < RuntimeError; end
|
6
|
+
|
7
|
+
|
8
|
+
# Represents the collection of possible command-line arguments for a script.
|
9
|
+
class Definition
|
10
|
+
|
11
|
+
# @return [String] A title for the script, displayed at the top of the
|
12
|
+
# usage and help outputs.
|
13
|
+
attr_accessor :title
|
14
|
+
# @return [String] A short description of the purpose of the script, for
|
15
|
+
# display when showing the usage help.
|
16
|
+
attr_accessor :purpose
|
17
|
+
|
18
|
+
|
19
|
+
# Create a new Definition, which is a collection of valid Arguments to
|
20
|
+
# be used when parsing a command-line.
|
21
|
+
def initialize
|
22
|
+
@arguments = {}
|
23
|
+
@short_keys = {}
|
24
|
+
@require_set = Hash.new{ |h,k| h[k] = [] }
|
25
|
+
@title = $0.respond_to?(:titleize) ? $0.titleize : $0
|
26
|
+
yield self if block_given?
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
# @return [Argument] the argument for the given key if it exists, or nil
|
31
|
+
# if it does not.
|
32
|
+
def has_key?(key)
|
33
|
+
k = Argument.to_key(key)
|
34
|
+
arg = @arguments[k] || @short_keys[k]
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# @return [Argument] the argument with the specified key
|
39
|
+
# @raise [ArgumentError] if no argument has been defined with the
|
40
|
+
# specified key.
|
41
|
+
def [](key)
|
42
|
+
arg = has_key?(key)
|
43
|
+
arg or raise NoSuchArgumentError, "No argument defined for key '#{Argument.to_key(key)}'"
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
# Adds the specified argument to the command-line definition.
|
48
|
+
#
|
49
|
+
# @param arg [Argument] An Argument sub-class to be added to the command-
|
50
|
+
# line definition.
|
51
|
+
def <<(arg)
|
52
|
+
case arg
|
53
|
+
when PositionalArgument, KeywordArgument, FlagArgument, RestArgument
|
54
|
+
if @arguments[arg.key]
|
55
|
+
raise ArgumentError, "An argument with key '#{arg.key}' has already been defined"
|
56
|
+
end
|
57
|
+
if arg.short_key && @short_keys[arg.short_key]
|
58
|
+
raise ArgumentError, "An argument with short key '#{arg.short_key}' has already been defined"
|
59
|
+
end
|
60
|
+
if rest_args
|
61
|
+
raise ArgumentError, "Only one rest argument can be defined"
|
62
|
+
end
|
63
|
+
@arguments[arg.key] = arg
|
64
|
+
@short_keys[arg.short_key] = arg if arg.short_key
|
65
|
+
else
|
66
|
+
raise ArgumentError, "arg must be an instance of PositionalArgument, KeywordArgument, " +
|
67
|
+
"FlagArgument or RestArgument"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
# Add a positional argument to the set of arguments in this command-line
|
73
|
+
# argument definition.
|
74
|
+
# @see PositionalArgument#initialize
|
75
|
+
def positional_arg(key, desc, opts = {}, &block)
|
76
|
+
self << ArgParser::PositionalArgument.new(key, desc, opts, &block)
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
# Add a keyword argument to the set of arguments in this command-line
|
81
|
+
# argument definition.
|
82
|
+
# @see KeywordArgument#initialize
|
83
|
+
def keyword_arg(key, desc, opts = {}, &block)
|
84
|
+
self << ArgParser::KeywordArgument.new(key, desc, opts, &block)
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
# Add a flag argument to the set of arguments in this command-line
|
89
|
+
# argument definition.
|
90
|
+
# @see FlagArgument#initialize
|
91
|
+
def flag_arg(key, desc, opts = {}, &block)
|
92
|
+
self << ArgParser::FlagArgument.new(key, desc, opts, &block)
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
# Add a rest argument to the set of arguments in this command-line
|
97
|
+
# argument definition.
|
98
|
+
# @see RestArgument#initialize
|
99
|
+
def rest_arg(key, desc, opts = {}, &block)
|
100
|
+
self << ArgParser::RestArgument.new(key, desc, opts, &block)
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
# Individual arguments are optional, but exactly one of +keys+ arguments
|
105
|
+
# is required.
|
106
|
+
def require_one_of(*keys)
|
107
|
+
@require_set[:one] << keys.map{ |k| self[k] }
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
# Individual arguments are optional, but at least one of +keys+ arguments
|
112
|
+
# is required.
|
113
|
+
def require_any_of(*keys)
|
114
|
+
@require_set[:any] << keys.map{ |k| self[k] }
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
# True if at least one argument is required out of multiple optional args.
|
119
|
+
def requires_some?
|
120
|
+
@require_set.size > 0
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
# @return [Parser] a Parser instance that can be used to parse this
|
125
|
+
# command-line Definition.
|
126
|
+
def parser
|
127
|
+
@parser ||= Parser.new(self)
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
# Parse the +args+ array of arguments using this command-line definition.
|
132
|
+
#
|
133
|
+
# @param args [Array, String] an array of arguments, or a String representing
|
134
|
+
# the command-line that is to be parsed.
|
135
|
+
# @return [OpenStruct, false] if successful, an OpenStruct object with all
|
136
|
+
# arguments defined as accessors, and the parsed or default values for each
|
137
|
+
# argument as values. If unsuccessful, returns false indicating a parse
|
138
|
+
# failure.
|
139
|
+
# @see Parser#parse, Parser#errors, Parser#show_usage, Parser#show_help
|
140
|
+
def parse(args = ARGV)
|
141
|
+
parser.parse(args)
|
142
|
+
end
|
143
|
+
|
144
|
+
|
145
|
+
# Return an array of parse errors.
|
146
|
+
# @see Parser#errors
|
147
|
+
def errors
|
148
|
+
parser.errors
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
# Whether user indicated they would like help on usage.
|
153
|
+
# @see Parser#show_usage
|
154
|
+
def show_usage?
|
155
|
+
parser.show_usage?
|
156
|
+
end
|
157
|
+
|
158
|
+
|
159
|
+
# Whether user indicated they would like help on supported arguments.
|
160
|
+
# @see Parser#show_help
|
161
|
+
def show_help?
|
162
|
+
parser.show_help?
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
# Validates the supplied +args+ Hash object, verifying that any argument
|
167
|
+
# set requirements have been satisfied. Returns an array of error
|
168
|
+
# messages for each set requirement that is not satisfied.
|
169
|
+
#
|
170
|
+
# @param args [Hash] a Hash containing the keys and values identified
|
171
|
+
# by the parser.
|
172
|
+
# @return [Array] a list of errors for any argument requirements that
|
173
|
+
# have not been satisfied.
|
174
|
+
def validate_requirements(args)
|
175
|
+
errors = []
|
176
|
+
@require_set.each do |req, sets|
|
177
|
+
sets.each do |set|
|
178
|
+
count = set.count{ |arg| args.has_key?(arg.key) }
|
179
|
+
case req
|
180
|
+
when :one
|
181
|
+
if count == 0
|
182
|
+
errors << "No argument has been specified for one of: #{set.join(', ')}"
|
183
|
+
elsif count > 1
|
184
|
+
errors << "Only one argument can been specified from: #{set.join(', ')}"
|
185
|
+
end
|
186
|
+
when :any
|
187
|
+
if count == 0
|
188
|
+
errors << "At least one of the arguments must be specified from: #{set.join(', ')}"
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
errors
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
# @return [Array] all arguments that have been defined.
|
198
|
+
def args
|
199
|
+
@arguments.values
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
# @return [Array] all positional arguments that have been defined
|
204
|
+
def positional_args
|
205
|
+
@arguments.values.select{ |arg| PositionalArgument === arg }
|
206
|
+
end
|
207
|
+
|
208
|
+
|
209
|
+
# @return True if any positional arguments have been defined.
|
210
|
+
def positional_args?
|
211
|
+
positional_args.size > 0
|
212
|
+
end
|
213
|
+
|
214
|
+
|
215
|
+
# @return [Array] the non-positional (i.e. keyword and flag)
|
216
|
+
# arguments that have been defined.
|
217
|
+
def non_positional_args
|
218
|
+
@arguments.values.reject{ |arg| PositionalArgument === arg || RestArgument === arg }
|
219
|
+
end
|
220
|
+
|
221
|
+
|
222
|
+
# @return True if any non-positional arguments have been defined.
|
223
|
+
def non_positional_args?
|
224
|
+
non_positional_args.size > 0
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
# @return [Array] the keyword arguments that have been defined.
|
229
|
+
def keyword_args
|
230
|
+
@arguments.values.select{ |arg| KeywordArgument === arg }
|
231
|
+
end
|
232
|
+
|
233
|
+
|
234
|
+
# @return True if any keyword arguments have been defined.
|
235
|
+
def keyword_args?
|
236
|
+
keyword_args.size > 0
|
237
|
+
end
|
238
|
+
|
239
|
+
|
240
|
+
# @return [Array] the flag arguments that have been defined
|
241
|
+
def flag_args
|
242
|
+
@arguments.values.select{ |arg| FlagArgument === arg }
|
243
|
+
end
|
244
|
+
|
245
|
+
|
246
|
+
# @return True if any flag arguments have been defined.
|
247
|
+
def flag_args?
|
248
|
+
flag_args.size > 0
|
249
|
+
end
|
250
|
+
|
251
|
+
|
252
|
+
# @return [RestArgument] the RestArgument defined for this command-line,
|
253
|
+
# or nil if no RestArgument is defined.
|
254
|
+
def rest_args
|
255
|
+
@arguments.values.find{ |arg| RestArgument === arg }
|
256
|
+
end
|
257
|
+
|
258
|
+
|
259
|
+
# @return True if a RestArgument has been defined.
|
260
|
+
def rest_args?
|
261
|
+
!!rest_args
|
262
|
+
end
|
263
|
+
|
264
|
+
|
265
|
+
# @return [Array] all the positional, keyword, and rest arguments
|
266
|
+
# that have been defined.
|
267
|
+
def value_args
|
268
|
+
@arguments.values.select{ |arg| ValueArgument === arg }
|
269
|
+
end
|
270
|
+
|
271
|
+
|
272
|
+
# @return [Integer] the number of arguments that have been defined.
|
273
|
+
def size
|
274
|
+
@arguments.size
|
275
|
+
end
|
276
|
+
|
277
|
+
|
278
|
+
# Generates a usage display string
|
279
|
+
def show_usage(out = STDERR, width = 80)
|
280
|
+
lines = ['']
|
281
|
+
pos_args = positional_args
|
282
|
+
opt_args = size - pos_args.size
|
283
|
+
usage_args = pos_args.map(&:to_use)
|
284
|
+
usage_args << (requires_some? ? 'OPTIONS' : '[OPTIONS]') if opt_args > 0
|
285
|
+
usage_args << rest_args.to_use if rest_args?
|
286
|
+
lines.concat(wrap_text("USAGE: #{RUBY_ENGINE} #{$0} #{usage_args.join(' ')}", width))
|
287
|
+
lines << ''
|
288
|
+
lines << 'Specify the /? or --help option for more detailed help'
|
289
|
+
lines << ''
|
290
|
+
lines.each{ |line| out.puts line } if out
|
291
|
+
lines
|
292
|
+
end
|
293
|
+
|
294
|
+
|
295
|
+
# Generates a more detailed help screen.
|
296
|
+
# @param out [IO] an IO object on which the help information will be
|
297
|
+
# output. Pass +nil+ if no output to any device is desired.
|
298
|
+
# @param width [Integer] the width at which to wrap text.
|
299
|
+
# @return [Array] An array of lines of text, containing the help text.
|
300
|
+
def show_help(out = STDOUT, width = 80)
|
301
|
+
lines = ['', '']
|
302
|
+
lines << title
|
303
|
+
lines << title.gsub(/./, '=')
|
304
|
+
lines << ''
|
305
|
+
if purpose
|
306
|
+
lines.concat(wrap_text(purpose, width))
|
307
|
+
lines << ''
|
308
|
+
lines << ''
|
309
|
+
end
|
310
|
+
|
311
|
+
lines << 'USAGE'
|
312
|
+
lines << '-----'
|
313
|
+
pos_args = positional_args
|
314
|
+
opt_args = size - pos_args.size
|
315
|
+
usage_args = pos_args.map(&:to_use)
|
316
|
+
usage_args << (requires_some? ? 'OPTIONS' : '[OPTIONS]') if opt_args > 0
|
317
|
+
usage_args << rest_args.to_use if rest_args?
|
318
|
+
lines.concat(wrap_text(" #{RUBY_ENGINE} #{$0} #{usage_args.join(' ')}", width))
|
319
|
+
lines << ''
|
320
|
+
|
321
|
+
if positional_args?
|
322
|
+
max = positional_args.map{ |a| a.to_s.length }.max
|
323
|
+
pos_args = positional_args
|
324
|
+
pos_args << rest_args if rest_args?
|
325
|
+
pos_args.each do |arg|
|
326
|
+
if arg.usage_break
|
327
|
+
lines << ''
|
328
|
+
lines << arg.usage_break
|
329
|
+
end
|
330
|
+
desc = arg.description
|
331
|
+
desc << "\n[Default: #{arg.default}]" unless arg.default.nil?
|
332
|
+
wrap_text(desc, width - max - 6).each_with_index do |line, i|
|
333
|
+
lines << " %-#{max}s %s" % [[arg.to_s][i], line]
|
334
|
+
end
|
335
|
+
end
|
336
|
+
lines << ''
|
337
|
+
end
|
338
|
+
if non_positional_args?
|
339
|
+
lines << ''
|
340
|
+
lines << 'OPTIONS'
|
341
|
+
lines << '-------'
|
342
|
+
max = non_positional_args.map{ |a| a.to_use.length }.max
|
343
|
+
non_positional_args.each do |arg|
|
344
|
+
if arg.usage_break
|
345
|
+
lines << ''
|
346
|
+
lines << arg.usage_break
|
347
|
+
end
|
348
|
+
desc = arg.description
|
349
|
+
desc << "\n[Default: #{arg.default}]" unless arg.default.nil?
|
350
|
+
wrap_text(desc, width - max - 6).each_with_index do |line, i|
|
351
|
+
lines << " %-#{max}s %s" % [[arg.to_use][i], line]
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
lines << ''
|
356
|
+
|
357
|
+
lines.each{ |line| line.length < width ? out.puts(line) : out.print(line) } if out
|
358
|
+
lines
|
359
|
+
end
|
360
|
+
|
361
|
+
|
362
|
+
# Utility method for wrapping lines of +text+ at +width+ characters.
|
363
|
+
#
|
364
|
+
# @param text [String] a string of text that is to be wrapped to a
|
365
|
+
# maximum width.
|
366
|
+
# @param width [Integer] the maximum length of each line of text.
|
367
|
+
# @return [Array] an Array of lines of text, each no longer than +width+
|
368
|
+
# characters.
|
369
|
+
def wrap_text(text, width)
|
370
|
+
if width > 0 && (text.length > width || text.index("\n"))
|
371
|
+
lines = []
|
372
|
+
start, nl_pos, ws_pos, wb_pos, end_pos = 0, 0, 0, 0, text.rindex(/[^\s]/)
|
373
|
+
while start < end_pos
|
374
|
+
last_start = start
|
375
|
+
nl_pos = text.index("\n", start)
|
376
|
+
ws_pos = text.rindex(/ +/, start + width)
|
377
|
+
wb_pos = text.rindex(/[\-,.;#)}\]\/\\]/, start + width - 1)
|
378
|
+
### Debug code ###
|
379
|
+
#STDERR.puts self
|
380
|
+
#ind = ' ' * end_pos
|
381
|
+
#ind[start] = '('
|
382
|
+
#ind[start+width < end_pos ? start+width : end_pos] = ']'
|
383
|
+
#ind[nl_pos] = 'n' if nl_pos
|
384
|
+
#ind[wb_pos] = 'b' if wb_pos
|
385
|
+
#ind[ws_pos] = 's' if ws_pos
|
386
|
+
#STDERR.puts ind
|
387
|
+
### End debug code ###
|
388
|
+
if nl_pos && nl_pos <= start + width
|
389
|
+
lines << text[start...nl_pos].strip
|
390
|
+
start = nl_pos + 1
|
391
|
+
elsif end_pos < start + width
|
392
|
+
lines << text[start..end_pos]
|
393
|
+
start = end_pos
|
394
|
+
elsif ws_pos && ws_pos > start && ((wb_pos.nil? || ws_pos > wb_pos) ||
|
395
|
+
(wb_pos && wb_pos > 5 && wb_pos - 5 < ws_pos))
|
396
|
+
lines << text[start...ws_pos]
|
397
|
+
start = text.index(/[^\s]/, ws_pos + 1)
|
398
|
+
elsif wb_pos && wb_pos > start
|
399
|
+
lines << text[start..wb_pos]
|
400
|
+
start = wb_pos + 1
|
401
|
+
else
|
402
|
+
lines << text[start...(start+width)]
|
403
|
+
start += width
|
404
|
+
end
|
405
|
+
if start <= last_start
|
406
|
+
# Detect an infinite loop, and just return the original text
|
407
|
+
STDERR.puts "Inifinite loop detected at #{__FILE__}:#{__LINE__}"
|
408
|
+
STDERR.puts " width: #{width}, start: #{start}, nl_pos: #{nl_pos}, " +
|
409
|
+
"ws_pos: #{ws_pos}, wb_pos: #{wb_pos}"
|
410
|
+
return [text]
|
411
|
+
end
|
412
|
+
end
|
413
|
+
lines
|
414
|
+
else
|
415
|
+
[text]
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
end
|
420
|
+
|
421
|
+
end
|
422
|
+
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module ArgParser
|
2
|
+
|
3
|
+
# Namespace for DSL methods that can be imported into a class for defining
|
4
|
+
# command-line argument handling.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# class MyClass
|
8
|
+
# include ArgParser::DSL
|
9
|
+
#
|
10
|
+
# # These class methods are added by the DSL, and allow us to define
|
11
|
+
# # the command-line arguments we want our class to handle.
|
12
|
+
# positional_arg :command, 'The name of the sub-command to run',
|
13
|
+
# validation: ['process', 'list'] do |arg, val, hsh|
|
14
|
+
# # On parse, return the argument value as a symbol
|
15
|
+
# val.intern
|
16
|
+
# end
|
17
|
+
# rest_arg :files, 'The file(s) to process'
|
18
|
+
#
|
19
|
+
# def run
|
20
|
+
# # Parse the command-line arguments, and call the appropriate command
|
21
|
+
# args = parse_arguments
|
22
|
+
# send(args.command, *args.files)
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# def process(*files)
|
26
|
+
# ...
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# def list(*files)
|
30
|
+
# ...
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
module DSL
|
34
|
+
|
35
|
+
# Class methods added when DSL module is included into a class.
|
36
|
+
module ClassMethods
|
37
|
+
|
38
|
+
# Accessor to return a Definition object holding the command-line
|
39
|
+
# argument definitions.
|
40
|
+
def args_def
|
41
|
+
@args_def ||= ArgParser::Definition.new
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns true if any arguments have been defined
|
45
|
+
def args_defined?
|
46
|
+
@args_def && @args_def.args.size > 0
|
47
|
+
end
|
48
|
+
|
49
|
+
# Sets the title that will appear in the Usage output generated from
|
50
|
+
# the Definition.
|
51
|
+
def title(val)
|
52
|
+
args_def.title = val
|
53
|
+
end
|
54
|
+
|
55
|
+
# Sets the descriptive text that describes the purpose of the job
|
56
|
+
# represented by this class.
|
57
|
+
def purpose(desc)
|
58
|
+
args_def.purpose = desc
|
59
|
+
end
|
60
|
+
|
61
|
+
# Define a new positional argument.
|
62
|
+
# @see PositionalArgument#initialize
|
63
|
+
def positional_arg(key, desc, opts = {}, &block)
|
64
|
+
args_def.positional_arg(key, desc, opts, &block)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Define a new positional argument.
|
68
|
+
# @see KeywordArgument#initialize
|
69
|
+
def keyword_arg(key, desc, opts = {}, &block)
|
70
|
+
args_def.keyword_arg(key, desc, opts, &block)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Define a new flag argument.
|
74
|
+
# @see FlagArgument#initialize
|
75
|
+
def flag_arg(key, desc, opts = {}, &block)
|
76
|
+
args_def.flag_arg(key, desc, opts, &block)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Define a rest argument.
|
80
|
+
# @see RestArgument#initialize
|
81
|
+
def rest_arg(key, desc, opts = {}, &block)
|
82
|
+
args_def.rest_arg(key, desc, opts, &block)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Make exactly one of the specified arguments mandatory.
|
86
|
+
# @see Definition#require_one_of
|
87
|
+
def require_one_of(*keys)
|
88
|
+
args_def.require_one_of(*keys)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Make one or more of the specified arguments mandatory.
|
92
|
+
# @see Definition#require_any_of
|
93
|
+
def require_any_of(*keys)
|
94
|
+
args_def.require_any_of(*keys)
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
# Hook used to extend the including class with class methods defined in
|
100
|
+
# the DSL ClassMethods module.
|
101
|
+
def self.included(base)
|
102
|
+
base.extend(ClassMethods)
|
103
|
+
end
|
104
|
+
|
105
|
+
# @return [Definition] The arguments Definition object defined on this
|
106
|
+
# class.
|
107
|
+
def args_def
|
108
|
+
self.class.args_def
|
109
|
+
end
|
110
|
+
|
111
|
+
# Defines a +parse_arguments+ instance method to be added to classes that
|
112
|
+
# include this module. Uses the +args_def+ argument definition stored on
|
113
|
+
# on the class to define the arguments to parse.
|
114
|
+
def parse_arguments(args = ARGV)
|
115
|
+
args_def.parse(args)
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
# Defines a +parse_errors+ instance method to be added to classes that
|
120
|
+
# include this module.
|
121
|
+
def parse_errors
|
122
|
+
args_def.errors
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
# Whether usage information should be displayed.
|
127
|
+
def show_usage?
|
128
|
+
args_def.show_usage?
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
# Whether help should be displayed.
|
133
|
+
def show_help?
|
134
|
+
args_def.show_usage?
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
# Outputs brief usgae details.
|
139
|
+
def show_usage(*args)
|
140
|
+
args_def.show_usage(*args)
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
# Outputs detailed help about available arguments.
|
145
|
+
def show_help(*args)
|
146
|
+
args_def.show_help(*args)
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
@@ -0,0 +1,237 @@
|
|
1
|
+
module ArgParser
|
2
|
+
|
3
|
+
# Parser for parsing a command-line
|
4
|
+
class Parser
|
5
|
+
|
6
|
+
# @return [Definition] The supported Arguments to be used when parsing
|
7
|
+
# the command-line.
|
8
|
+
attr_reader :definition
|
9
|
+
# @return [Array] An Array of error message Strings generated during
|
10
|
+
# parsing.
|
11
|
+
attr_reader :errors
|
12
|
+
# @return [Boolean] Flag set during parsing if the usage display should
|
13
|
+
# be shown. Set if there are any parse errors encountered.
|
14
|
+
def show_usage?
|
15
|
+
@show_usage
|
16
|
+
end
|
17
|
+
# @return [Boolean] Flag set during parsing if the user has requested
|
18
|
+
# the help display to be shown (via --help or /?).
|
19
|
+
def show_help?
|
20
|
+
@show_help
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
# Instantiates a new command-line parser, with the specified command-
|
25
|
+
# line definition. A Parser instance delegates unknown methods to the
|
26
|
+
# Definition, so its possible to work only with a Parser instance to
|
27
|
+
# both define and parse a command-line.
|
28
|
+
#
|
29
|
+
# @param [Definition] definition A Definition object that defines the
|
30
|
+
# possible arguments that may appear in a command-line. If no definition
|
31
|
+
# is supplied, an empty definition is created.
|
32
|
+
def initialize(definition = nil)
|
33
|
+
@definition = definition || Definition.new
|
34
|
+
@errors = []
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# Parse the specified Array[String] of +tokens+, or ARGV if +tokens+ is
|
39
|
+
# nil. Returns false if unable to parse successfully, or an OpenStruct
|
40
|
+
# with accessors for every defined argument. Arguments whose values are
|
41
|
+
# not specified will contain the agument default value, or nil if no
|
42
|
+
# default is specified.
|
43
|
+
def parse(tokens = ARGV)
|
44
|
+
@show_usage = nil
|
45
|
+
@show_help = nil
|
46
|
+
@errors = []
|
47
|
+
begin
|
48
|
+
pos_vals, kw_vals, rest_vals = classify_tokens(tokens)
|
49
|
+
args = process_args(pos_vals, kw_vals, rest_vals) unless @show_help
|
50
|
+
rescue NoSuchArgumentError => ex
|
51
|
+
self.errors << ex.message
|
52
|
+
@show_usage = true
|
53
|
+
end
|
54
|
+
(@show_usage || @show_help) ? false : args
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
# Delegate unknown methods to the associated argument Definition object.
|
59
|
+
def method_missing(mthd, *args)
|
60
|
+
if @definition.respond_to?(mthd)
|
61
|
+
@definition.send(mthd, *args)
|
62
|
+
else
|
63
|
+
super
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
# Evaluate the list of values in +tokens+, and classify them as either
|
69
|
+
# keyword/value pairs, or positional arguments. Ideally this would be
|
70
|
+
# done without any reference to the defined arguments, but unfortunately
|
71
|
+
# a keyword arg cannot be distinguished from a flag arg followed by a
|
72
|
+
# positional arg without the context of what arguments are expected.
|
73
|
+
def classify_tokens(tokens)
|
74
|
+
if tokens.is_a?(String)
|
75
|
+
require 'csv'
|
76
|
+
tokens = CSV.parse(tokens, col_sep: ' ').first
|
77
|
+
end
|
78
|
+
tokens = [] unless tokens
|
79
|
+
pos_vals = []
|
80
|
+
kw_vals = {}
|
81
|
+
rest_vals = []
|
82
|
+
|
83
|
+
arg = nil
|
84
|
+
tokens.each_with_index do |token, i|
|
85
|
+
case token
|
86
|
+
when '/?', '-?', '--help'
|
87
|
+
@show_help = true
|
88
|
+
when /^-([a-z0-9]+)/i
|
89
|
+
$1.to_s.each_char do |sk|
|
90
|
+
kw_vals[arg] = nil if arg
|
91
|
+
arg = @definition[sk]
|
92
|
+
if FlagArgument === arg
|
93
|
+
kw_vals[arg] = true
|
94
|
+
arg = nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
when /^(?:--|\/)(no-)?(.+)/i
|
98
|
+
kw_vals[arg] = nil if arg
|
99
|
+
arg = @definition[$2]
|
100
|
+
if FlagArgument === arg || (KeywordArgument === arg && $1)
|
101
|
+
kw_vals[arg] = $1 ? false : true
|
102
|
+
arg = nil
|
103
|
+
end
|
104
|
+
when '--'
|
105
|
+
# All subsequent values are rest args
|
106
|
+
kw_vals[arg] = nil if arg
|
107
|
+
rest_vals = tokens[(i + 1)..-1]
|
108
|
+
break
|
109
|
+
else
|
110
|
+
if arg
|
111
|
+
kw_vals[arg] = token
|
112
|
+
else
|
113
|
+
pos_vals << token
|
114
|
+
arg = @definition.positional_args[i]
|
115
|
+
end
|
116
|
+
tokens[i] = '******' if arg && arg.sensitive?
|
117
|
+
arg = nil
|
118
|
+
end
|
119
|
+
end
|
120
|
+
kw_vals[arg] = nil if arg
|
121
|
+
[pos_vals, kw_vals, rest_vals]
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
# Process arguments using the supplied +pos_vals+ Array of positional
|
126
|
+
# argument values, and the +kw_vals+ Hash of keyword/value.
|
127
|
+
def process_args(pos_vals, kw_vals, rest_vals)
|
128
|
+
result = {}
|
129
|
+
|
130
|
+
# Process positional arguments
|
131
|
+
pos_args = @definition.positional_args
|
132
|
+
pos_args.each_with_index do |arg, i|
|
133
|
+
break if i >= pos_vals.length
|
134
|
+
result[arg.key] = process_arg_val(arg, pos_vals[i], result)
|
135
|
+
end
|
136
|
+
if pos_vals.size > pos_args.size
|
137
|
+
if @definition.rest_args?
|
138
|
+
rest_vals = pos_vals[pos_args.size..-1] + rest_vals
|
139
|
+
else
|
140
|
+
self.errors << "#{pos_vals.size} positional #{pos_vals.size == 1 ? 'argument' : 'arguments'} #{
|
141
|
+
pos_vals.size == 1 ? 'was' : 'were'} supplied, but only #{pos_args.size} #{
|
142
|
+
pos_args.size == 1 ? 'is' : 'are'} defined"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Process key-word based arguments
|
147
|
+
kw_vals.each do |arg, val|
|
148
|
+
result[arg.key] = process_arg_val(arg, val, result)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Process rest values
|
152
|
+
if arg = @definition.rest_args
|
153
|
+
result[arg.key] = process_arg_val(arg, rest_vals, result)
|
154
|
+
elsif rest_vals.size > 0
|
155
|
+
self.errors << "#{rest_vals.size} rest #{rest_vals.size == 1 ? 'value' : 'values'} #{
|
156
|
+
rest_vals.size == 1 ? 'was' : 'were'} supplied, but no rest argument is defined"
|
157
|
+
end
|
158
|
+
|
159
|
+
# Default unspecified arguments
|
160
|
+
@definition.args.select{ |arg| !result.has_key?(arg.key) }.each do |arg|
|
161
|
+
result[arg.key] = process_arg_val(arg, arg.default, result, true)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Validate if any set requirements have been satisfied
|
165
|
+
self.errors.concat(@definition.validate_requirements(result))
|
166
|
+
if self.errors.size > 0
|
167
|
+
@show_usage = true
|
168
|
+
else
|
169
|
+
props = result.keys
|
170
|
+
@definition.args.each{ |arg| props << arg.key unless result.has_key?(arg.key) }
|
171
|
+
args = Struct.new(*props)
|
172
|
+
args.new(*result.values)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
|
177
|
+
protected
|
178
|
+
|
179
|
+
|
180
|
+
# Process a single argument value
|
181
|
+
def process_arg_val(arg, val, hsh, is_default = false)
|
182
|
+
if is_default && arg.required? && (val.nil? || val.empty?)
|
183
|
+
self.errors << "No value was specified for required argument '#{arg}'"
|
184
|
+
return
|
185
|
+
end
|
186
|
+
if !is_default && val.nil? && KeywordArgument === arg && !arg.value_optional?
|
187
|
+
self.errors << "No value was specified for keyword argument '#{arg}'"
|
188
|
+
return
|
189
|
+
end
|
190
|
+
|
191
|
+
# Argument value validation
|
192
|
+
if ValueArgument === arg && arg.validation && val
|
193
|
+
valid = case arg.validation
|
194
|
+
when Regexp
|
195
|
+
[val].flatten.each do |v|
|
196
|
+
add_value_error(arg, val) unless v =~ arg.validation
|
197
|
+
end
|
198
|
+
when Array
|
199
|
+
[val].flatten.each do |v|
|
200
|
+
add_value_error(arg, val) unless arg.validation.include?(v)
|
201
|
+
end
|
202
|
+
when Proc
|
203
|
+
begin
|
204
|
+
arg.validation.call(val, arg, hsh)
|
205
|
+
rescue StandardError => ex
|
206
|
+
self.errors << "An error occurred in the validation handler for argument '#{arg}': #{ex}"
|
207
|
+
return
|
208
|
+
end
|
209
|
+
else
|
210
|
+
raise "Unknown validation type: #{arg.validation.class.name}"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# TODO: Argument value coercion
|
215
|
+
|
216
|
+
# Call any registered on_parse handler
|
217
|
+
begin
|
218
|
+
val = arg.on_parse.call(val, arg, hsh) if val && arg.on_parse
|
219
|
+
rescue StandardError => ex
|
220
|
+
self.errors << "An error occurred in the on_parse handler for argument '#{arg}': #{ex}"
|
221
|
+
return
|
222
|
+
end
|
223
|
+
|
224
|
+
# Return result
|
225
|
+
val
|
226
|
+
end
|
227
|
+
|
228
|
+
|
229
|
+
# Add an error for an invalid value
|
230
|
+
def add_value_error(arg, val)
|
231
|
+
self.errors << "The value '#{val}' is not valid for argument '#{arg}'"
|
232
|
+
end
|
233
|
+
|
234
|
+
end
|
235
|
+
|
236
|
+
end
|
237
|
+
|
data/lib/arg_parser.rb
ADDED
metadata
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: arg-parser
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Adam Gardiner
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-05-21 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: ! " ArgParser is a simple, yet powerful command-line argument
|
15
|
+
parser, with\n support for positional, keyword, flag and rest arguments,
|
16
|
+
any of which\n may be optional or mandatory.\n"
|
17
|
+
email: adam.b.gardiner@gmail.com
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- README.md
|
23
|
+
- LICENSE
|
24
|
+
- lib/arg-parser/argument.rb
|
25
|
+
- lib/arg-parser/definition.rb
|
26
|
+
- lib/arg-parser/dsl.rb
|
27
|
+
- lib/arg-parser/parser.rb
|
28
|
+
- lib/arg-parser.rb
|
29
|
+
- lib/arg_parser.rb
|
30
|
+
homepage: https://github.com/agardiner/arg-parser
|
31
|
+
licenses: []
|
32
|
+
post_install_message:
|
33
|
+
rdoc_options: []
|
34
|
+
require_paths:
|
35
|
+
- lib
|
36
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
37
|
+
none: false
|
38
|
+
requirements:
|
39
|
+
- - ! '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
requirements: []
|
49
|
+
rubyforge_project:
|
50
|
+
rubygems_version: 1.8.21
|
51
|
+
signing_key:
|
52
|
+
specification_version: 3
|
53
|
+
summary: ArgParser is a simple, yet powerful, command-line argument (option) parser
|
54
|
+
test_files: []
|