arg-parser 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|