arg-parser 0.3.1 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +177 -0
- data/lib/arg-parser/argument.rb +119 -12
- data/lib/arg-parser/definition.rb +368 -129
- data/lib/arg-parser/dsl.rb +35 -0
- data/lib/arg-parser/parser.rb +70 -15
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ff4e92f478416dc226d866f7763340e5bad9be7
|
4
|
+
data.tar.gz: d367fbaa43753a938f599ea0cf04181a3cabaadd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c1994d8da4359cc9c19735975840835b156cd8d79afd255660e9285bcfec1bb18eb806230113da2103d129ac5abf725c38af4fd60ac052eeaa9ba4512ec4a85
|
7
|
+
data.tar.gz: 7dd2c412243ea6b4f15d0ea41fccec25ff173404817536e72aec01112e43ddc99ae52a16fd3748fcb908601bc2fbe75e4f3b875cd1c953b97d47869a5d1b0e3f
|
data/README.md
CHANGED
@@ -47,6 +47,12 @@ class MyClass
|
|
47
47
|
# opts.flag, and opts.files
|
48
48
|
# ...
|
49
49
|
else
|
50
|
+
# False is returned if argument parsing was not completed
|
51
|
+
# This may be due to an error or because the help command
|
52
|
+
# was used (by specifying --help or /?). The #show_help?
|
53
|
+
# method returns true if help was requested, otherwise if
|
54
|
+
# a parse error was encountered, #show_usage? is true and
|
55
|
+
# parse errors are in #parse_errors
|
50
56
|
show_help? ? show_help : show_usage
|
51
57
|
end
|
52
58
|
end
|
@@ -63,6 +69,27 @@ MyClass.new.run
|
|
63
69
|
ArgParser provides a fairly broad range of functionality for argument parsing. Following is a non-exhaustive
|
64
70
|
list of features:
|
65
71
|
* Built-in usage and help display
|
72
|
+
* Multiple different types of arguments can be defined:
|
73
|
+
- Command arguments indicate an argument that can be one of a limited selection of pre-defined
|
74
|
+
argument values. Each command argument must define its commands, as well as any command-specific
|
75
|
+
arguments.
|
76
|
+
- Positional arguments are arguments that are normally supplied without any key, and are matched
|
77
|
+
to an argument definition based on the order in which they are encountered. Positional arguments
|
78
|
+
can also be supplied using the key if preferred. Generally only a small number of positional
|
79
|
+
arguments would be defined for those parameters required on every run.
|
80
|
+
- Keyword arguments are arguments that must be specified by using a key to indicate which argument
|
81
|
+
the subsequent value is for.
|
82
|
+
- Flag arguments are boolean arguments that normally default to nil, but can be set to true just
|
83
|
+
by specifying the argument key. It is also possible to define flag arguments with a default value
|
84
|
+
of true; the argument can then be set to false by speciying the argument name with --no-<flag>
|
85
|
+
(or --not-<flag> or --non-<flag> when those are more understandable).
|
86
|
+
- Rest arguments use a single argument key to collect all remaining values from the command-line,
|
87
|
+
and return these as an array in the resulting argument results.
|
88
|
+
* Parsed results are returned in an OpenStruct, with every defined argument having a value set (whether
|
89
|
+
or not it was encountered on the command-line). Use the `default: <value>` option to set default
|
90
|
+
values for non-specified arguments where you don't want nil returned in the parse results. Note that
|
91
|
+
in the case of command arguments with command-specific arguments, only the arguments from the selected
|
92
|
+
command appear in the results.
|
66
93
|
* Mandatory vs Optional: All arguments your program accepts can be defined as optional or mandatory.
|
67
94
|
By default, positional arguments are considered mandatory, and all others default to optional. To change
|
68
95
|
the default, simply specify `required: true` or `required: false` when defining the argument.
|
@@ -72,6 +99,10 @@ list of features:
|
|
72
99
|
parsed value in the results. However, arguments can also define a single letter or digit short-key form
|
73
100
|
which can be used as an alternate means for indicating a value. To define a short key, simply pass
|
74
101
|
`short_key: '<letter_or_digit>'` when defining an argument.
|
102
|
+
* Requiring from a set of arguments: If you need to ensure that only one of several mutually exclusive
|
103
|
+
arguments is specified, use `require_one_of <arg1>, <arg2>, ..., <argN>`. If you need to ensure that
|
104
|
+
at least one argument is specified from a list of arguments, use
|
105
|
+
`require_any_of <arg1>, <arg2>, ..., <argN>`
|
75
106
|
* Validation: Arguments can define validation requirements that must be satisfied. This can take several
|
76
107
|
forms:
|
77
108
|
- List of values: Pass an array containing the allowed values the argument can take.
|
@@ -90,3 +121,149 @@ list of features:
|
|
90
121
|
* Pre-defined arguments: Arguments can be registered under a key, and then re-used across multiple
|
91
122
|
definitions via the #predefined_arg DSL method. Common arguments would typically be defined in a
|
92
123
|
shared file that was included into each job. See Argument.register and DSL.predefined_arg.
|
124
|
+
|
125
|
+
|
126
|
+
### Commands
|
127
|
+
|
128
|
+
Often command-line utilities can perform several actions, and we want to define an argument that will
|
129
|
+
be used to indicate to the program which action it should perform. This can be done very easily using
|
130
|
+
positional arguments, and setting a list of allowed values in the argument validation, e.g.
|
131
|
+
`positional_arg :command, 'The command to perform', validation: %w{list export import}`
|
132
|
+
|
133
|
+
However, it is often the case that the different commands require different additional arguments
|
134
|
+
to be specified, and this makes it necessary to make any argument not required by all commands into
|
135
|
+
an optional argument. However, now the program has to ensure that it checks for those optional
|
136
|
+
arguments that are required for the command to actually be present.
|
137
|
+
|
138
|
+
In this circumstance, it is better to instead make use of command arguments. These are arguments
|
139
|
+
that look like positional arguments that accept only a specific set of values, however, each command
|
140
|
+
can then define its own set of additional arguments (of any type). During parsing, when a command
|
141
|
+
argument is found, the argument definition is updated on-the-fly to include any additional arguments
|
142
|
+
defined by the now-matched command. Additionally, the usage and help displays are also updated to
|
143
|
+
reflect the arguments defined by the specified command.
|
144
|
+
|
145
|
+
Defining command arguments is a little more complex, due to the need to associate command arguments
|
146
|
+
with the commands.
|
147
|
+
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
require 'arg-parser'
|
151
|
+
|
152
|
+
class MyClass
|
153
|
+
|
154
|
+
include ArgParser::DSL
|
155
|
+
|
156
|
+
purpose <<-EOT
|
157
|
+
This is where you specify the purpose of your program. It will be displayed in the
|
158
|
+
generated help screen if you pass /? or --help on the command-line.
|
159
|
+
EOT
|
160
|
+
|
161
|
+
command_arg :command, 'The command to perform' do
|
162
|
+
# We often want the same arguments to appear in several but not all commands.
|
163
|
+
# Rather than re-defining them for each command, we can first define them, and
|
164
|
+
# then reference them in the commands that need them.
|
165
|
+
define_args do
|
166
|
+
positional_arg :source, 'The source to use'
|
167
|
+
positional_arg :target, 'The target to use'
|
168
|
+
positional_arg :local_dir, 'The path to a local directory',
|
169
|
+
default: 'extracts'
|
170
|
+
end
|
171
|
+
|
172
|
+
command :list, 'List available items for transfer' do
|
173
|
+
predefined_arg :source
|
174
|
+
end
|
175
|
+
command :export, 'Export items to a local directory' do
|
176
|
+
predefined_arg :source
|
177
|
+
predefined_arg :local_dir
|
178
|
+
end
|
179
|
+
command :import, 'Import items from a local directory' do
|
180
|
+
predefined_arg :local_dir
|
181
|
+
predefined_arg :target
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
positional_arg :user_id, 'The user id for connecting to the source/target'
|
186
|
+
positional_arg :password, 'The password for connecting to the source/target',
|
187
|
+
sensitive: true
|
188
|
+
|
189
|
+
...
|
190
|
+
|
191
|
+
def run
|
192
|
+
if opts = parse_arguments
|
193
|
+
case opts.command
|
194
|
+
when 'list'
|
195
|
+
do_list(opts.source)
|
196
|
+
when 'export'
|
197
|
+
do_export(opts.source, opts.local_dir)
|
198
|
+
when 'import'
|
199
|
+
do_import(opts.local_dir, opts.target)
|
200
|
+
end
|
201
|
+
else
|
202
|
+
show_help? ? show_help : show_usage
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
MyClass.new.run
|
209
|
+
```
|
210
|
+
|
211
|
+
## Pre-defined Arguments
|
212
|
+
|
213
|
+
We saw above in the case of commands that it can be useful to pre-define an argument with all its
|
214
|
+
parameters once, and then reference that argument in multiple separate commands. This allows us
|
215
|
+
to follow DRY principles when defining an argument, making maintenance simpler and reducing the
|
216
|
+
possibility of mis-configuring one or more instances of the same argument.
|
217
|
+
|
218
|
+
This principle does not only apply to arguments for use within a single utility or set of commands.
|
219
|
+
Arguments can also be pre-defined in an argument scope, and then referenced in multiple utilities.
|
220
|
+
Thus if there are common arguments needed in many utilities that share some common code, you can
|
221
|
+
also define these common arguments in the shared code, and then include the pre-defined arguments
|
222
|
+
into each utility that shares that functionality.
|
223
|
+
|
224
|
+
|
225
|
+
```ruby
|
226
|
+
require 'arg-parser'
|
227
|
+
|
228
|
+
class MyLibrary
|
229
|
+
|
230
|
+
include ArgParser::DSL
|
231
|
+
|
232
|
+
LIBRARY_ARGS = define_args('Database Args') do
|
233
|
+
positional_arg :database, 'The name of the database to connect to',
|
234
|
+
on_parse: lambda{ |val, arg, hsh| # Lookup database connection details and return as hash }
|
235
|
+
positional_arg :user_id, 'The user id for connecting to the database'
|
236
|
+
positional_arg :password, 'The password for connecting to the database',
|
237
|
+
sensitive: true
|
238
|
+
end
|
239
|
+
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
class Utility
|
244
|
+
|
245
|
+
include 'arg-parser'
|
246
|
+
|
247
|
+
with_predefined_args MyLibrary::LIBRARY_ARGS do
|
248
|
+
predefined_arg :database
|
249
|
+
predefined_arg :user_id
|
250
|
+
predefined_arg :password
|
251
|
+
end
|
252
|
+
|
253
|
+
keyword_arg :log_level, 'Set the log level for database interactions'
|
254
|
+
|
255
|
+
...
|
256
|
+
|
257
|
+
|
258
|
+
def run
|
259
|
+
if opts = parse_arguments
|
260
|
+
# Connect to database, set log level etc
|
261
|
+
else
|
262
|
+
show_help? ? show_help : show_usage
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
end
|
267
|
+
|
268
|
+
MyClass.new.run
|
269
|
+
```
|
data/lib/arg-parser/argument.rb
CHANGED
@@ -5,7 +5,11 @@ module ArgParser
|
|
5
5
|
OnParseHandlers = {
|
6
6
|
:split_to_array => lambda{ |val, arg, hsh| val.split(',') }
|
7
7
|
}
|
8
|
-
# Hash containing registered arguments available via
|
8
|
+
# Hash containing globally registered predefined arguments available via
|
9
|
+
# #predefined_arg.
|
10
|
+
# Predefined arguments should normally be defined within an ArgumentScope,
|
11
|
+
# rather than globally. See DSL.define_args for pre-defining arguments on
|
12
|
+
# an including class, or use the argument creation methods on ArgumentScope,
|
9
13
|
PredefinedArguments = { }
|
10
14
|
|
11
15
|
|
@@ -14,14 +18,12 @@ module ArgParser
|
|
14
18
|
# @abstract
|
15
19
|
class Argument
|
16
20
|
|
17
|
-
# The key used to identify this argument value in the parsed command-
|
18
|
-
# line results Struct.
|
19
21
|
# @return [Symbol] the key/method by which this argument can be retrieved
|
20
22
|
# from the parse result Struct.
|
21
23
|
attr_reader :key
|
22
24
|
# @return [String] the description for this argument, which will be shown
|
23
25
|
# in the usage display.
|
24
|
-
|
26
|
+
attr_accessor :description
|
25
27
|
# @return [Symbol] a single letter or digit that can be used as a short
|
26
28
|
# alternative to the full key to identify an argument value in a command-
|
27
29
|
# line.
|
@@ -50,20 +52,19 @@ module ArgParser
|
|
50
52
|
# the group.
|
51
53
|
attr_accessor :usage_break
|
52
54
|
|
53
|
-
|
54
55
|
# Converts an argument key specification into a valid key, by stripping
|
55
56
|
# leading dashes, converting remaining dashes to underscores, and lower-
|
56
57
|
# casing all text. This is required to ensure the key name will be a
|
57
58
|
# valid accessor name on the parse results.
|
58
59
|
#
|
60
|
+
# @param label [String|Symbol] the value supplied for an argument key
|
59
61
|
# @return [Symbol] the key by which an argument can be retrieved from
|
60
|
-
# the arguments definition
|
62
|
+
# the arguments definition and the parse results.
|
61
63
|
def self.to_key(label)
|
62
64
|
k = label.to_s.gsub(/^-+/, '').gsub('-', '_')
|
63
65
|
k.length > 1 ? k.downcase.intern : k.intern
|
64
66
|
end
|
65
67
|
|
66
|
-
|
67
68
|
# Register a common argument for use in multiple argument definitions. The
|
68
69
|
# registered argument is a completely defined argument that can be added to
|
69
70
|
# any argument definition via Definition#predefined_arg.
|
@@ -86,7 +87,6 @@ module ArgParser
|
|
86
87
|
PredefinedArguments[key] = arg
|
87
88
|
end
|
88
89
|
|
89
|
-
|
90
90
|
# Return a copy of a pre-defined argument for use in an argument
|
91
91
|
# definition.
|
92
92
|
#
|
@@ -104,9 +104,22 @@ module ArgParser
|
|
104
104
|
arg.clone
|
105
105
|
end
|
106
106
|
|
107
|
+
# Set the short key (a single letter or digit) that may be used as an
|
108
|
+
# alternative when specifyin gthis argument.
|
109
|
+
#
|
110
|
+
# @param sk [String] The short key specification
|
111
|
+
def short_key=(sk)
|
112
|
+
if sk =~ /^-?([a-z0-9])$/i
|
113
|
+
@short_key = $1.intern
|
114
|
+
else
|
115
|
+
raise ArgumentError, "An argument short key must be a single digit or letter"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
107
119
|
|
108
120
|
private
|
109
121
|
|
122
|
+
# Private to prevent instantiation of this abstract class
|
110
123
|
def initialize(key, desc, opts = {}, &block)
|
111
124
|
@key = self.class.to_key(key)
|
112
125
|
@description = desc
|
@@ -125,12 +138,100 @@ module ArgParser
|
|
125
138
|
end
|
126
139
|
@usage_break = opts[:usage_break]
|
127
140
|
if sk = opts[:short_key]
|
128
|
-
|
129
|
-
|
141
|
+
self.short_key=(sk)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
# An argument type that defines a command. Each command placeholder can have
|
149
|
+
# one or more command arguments (i.e. allowed values). Depending on the command,
|
150
|
+
# different additional arguments may then be specified in the command's
|
151
|
+
# ArgumentSet.
|
152
|
+
class CommandArgument < Argument
|
153
|
+
|
154
|
+
# @return [Array<Symbol] List of valid commands that may be specified.
|
155
|
+
attr_accessor :commands
|
156
|
+
|
157
|
+
|
158
|
+
# Create an instance of a CommandArgument, a positional argument that
|
159
|
+
# indicates a particular command to be invoked.
|
160
|
+
#
|
161
|
+
# @param key [String|Symbol] The value under which the specified command
|
162
|
+
# can be retrieved following parsing (e.g. :command)
|
163
|
+
# @param desc [String] A description for the command argument.
|
164
|
+
# @param opts [Hash] An options hash. See Argument#initialize for supported
|
165
|
+
# option values.
|
166
|
+
def initialize(key, desc, opts = {})
|
167
|
+
super(key, desc, opts)
|
168
|
+
@commands = {}
|
169
|
+
@required = opts.fetch(:required, !opts.has_key?(:default))
|
170
|
+
@usage_value = opts.fetch(:usage_value, key.to_s.gsub('_', '-').upcase)
|
171
|
+
end
|
172
|
+
|
173
|
+
# Adds a specific command verb/value to this command argument
|
174
|
+
def <<(cmd_instance)
|
175
|
+
raise ArgumentError, "must be a CommandInstance object" unless cmd_instance.is_a?(CommandInstance)
|
176
|
+
@commands[cmd_instance.command_value] = cmd_instance
|
177
|
+
end
|
178
|
+
|
179
|
+
# Return the CommandInstance for a particular command value
|
180
|
+
#
|
181
|
+
# @param cmd_val [String|Symbol] A command token identifying the command
|
182
|
+
# @return [CommandInstance] The CommandInstance for the specified command
|
183
|
+
def [](cmd_val)
|
184
|
+
k = Argument.to_key(cmd_val)
|
185
|
+
@commands[k]
|
186
|
+
end
|
187
|
+
|
188
|
+
def to_s
|
189
|
+
@usage_value
|
190
|
+
end
|
191
|
+
|
192
|
+
def to_use
|
193
|
+
required? ? usage_value : "[#{usage_value}]"
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
|
199
|
+
# Represents a specific command value for a CommandArgument, along with any
|
200
|
+
# additional arguments specific to this command.
|
201
|
+
class CommandInstance < Argument
|
202
|
+
|
203
|
+
# @return the constant value that identifies this CommandInstance
|
204
|
+
attr_reader :command_value
|
205
|
+
# Return the CommandArgument to which this CommandInstance relates
|
206
|
+
attr_reader :command_arg
|
207
|
+
# Return an ArgumentScope for any additional arguments this command takes
|
208
|
+
attr_reader :argument_scope
|
209
|
+
|
210
|
+
|
211
|
+
def initialize(cmd_val, desc, cmd_arg, arg_scope, opts = {})
|
212
|
+
if cmd_arg.on_parse
|
213
|
+
if cmd_inst_op = opts[:on_parse]
|
214
|
+
# Command and command instance both defined on_parse handlers
|
215
|
+
opts[:on_parse] = lambda do |val, arg, hsh|
|
216
|
+
cmd_arg.on_parse(val, arg, hsh)
|
217
|
+
cmd_inst_op.on_parse(val, arg, hsh)
|
218
|
+
end
|
130
219
|
else
|
131
|
-
|
220
|
+
opts[:on_parse] = cmd_arg.on_parse
|
132
221
|
end
|
133
222
|
end
|
223
|
+
super(cmd_arg.key, desc, opts)
|
224
|
+
@command_value = cmd_val
|
225
|
+
@command_arg = cmd_arg
|
226
|
+
@argument_scope = arg_scope
|
227
|
+
end
|
228
|
+
|
229
|
+
def to_s
|
230
|
+
@command_value
|
231
|
+
end
|
232
|
+
|
233
|
+
def to_use
|
234
|
+
@command_value
|
134
235
|
end
|
135
236
|
|
136
237
|
end
|
@@ -320,6 +421,7 @@ module ArgParser
|
|
320
421
|
# String to some other type.
|
321
422
|
def initialize(key, desc, opts = {}, &block)
|
322
423
|
super
|
424
|
+
@usage_value = opts[:usage_value]
|
323
425
|
end
|
324
426
|
|
325
427
|
def required
|
@@ -332,7 +434,11 @@ module ArgParser
|
|
332
434
|
|
333
435
|
def to_use
|
334
436
|
sk = short_key ? "-#{short_key}, " : ''
|
335
|
-
|
437
|
+
if @usage_value
|
438
|
+
"#{sk}#{@usage_value[0..1] == '--' ? '' : '--'}#{@usage_value}"
|
439
|
+
else
|
440
|
+
"#{sk}#{self.to_s}"
|
441
|
+
end
|
336
442
|
end
|
337
443
|
|
338
444
|
end
|
@@ -366,6 +472,7 @@ module ArgParser
|
|
366
472
|
def initialize(key, desc, opts = {}, &block)
|
367
473
|
super
|
368
474
|
@min_values = opts.fetch(:min_values, opts.fetch(:required, true) ? 1 : 0)
|
475
|
+
@default = [@default] if @default.is_a?(String)
|
369
476
|
end
|
370
477
|
|
371
478
|
def required
|
@@ -5,25 +5,76 @@ module ArgParser
|
|
5
5
|
class NoSuchArgumentError < RuntimeError; end
|
6
6
|
|
7
7
|
|
8
|
-
# Represents
|
9
|
-
|
8
|
+
# Represents a scope within which an argument is defined/alid.
|
9
|
+
# Scopes may be nested, and argument requests will search the scope chain to
|
10
|
+
# find a matching argument.
|
11
|
+
class ArgumentScope
|
10
12
|
|
11
|
-
|
12
|
-
|
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
|
13
|
+
attr_reader :name, :parent
|
14
|
+
attr_accessor :predefined_args
|
17
15
|
|
18
16
|
|
19
|
-
|
20
|
-
|
21
|
-
|
17
|
+
def initialize(name, parent = nil)
|
18
|
+
@name = name
|
19
|
+
@parent = parent
|
20
|
+
@parent.add_child(self) if @parent
|
21
|
+
@children = []
|
22
22
|
@arguments = {}
|
23
23
|
@short_keys = {}
|
24
|
-
@
|
25
|
-
|
26
|
-
|
24
|
+
@predefined_args = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
# Adds a Scope as a child of this scope.
|
29
|
+
def add_child(arg_scope)
|
30
|
+
raise ArgumentError, "#{arg_scope} must be an ArgumentScope instance" unless arg_scope.is_a?(ArgumentScope)
|
31
|
+
raise ArgumentError, "#{arg_scope} parent not set to this ArgumentScope" if arg_scope.parent != self
|
32
|
+
@children << arg_scope
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# Checks if a key has been used in this scope, any ancestor scopes, or
|
37
|
+
# any descendant scopes.
|
38
|
+
#
|
39
|
+
# @param key [String] The key under which an argument is to be registered
|
40
|
+
# @return [Argument|nil] The Argument that already uses the key, or nil
|
41
|
+
# if the key is not used.
|
42
|
+
def key_used?(key)
|
43
|
+
self.walk_ancestors do |anc|
|
44
|
+
arg = anc.has_key?(key)
|
45
|
+
return arg if arg
|
46
|
+
end
|
47
|
+
self.walk_children do |child|
|
48
|
+
arg = child.has_key?(key)
|
49
|
+
return arg if arg
|
50
|
+
end
|
51
|
+
nil
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
# Yields each key/argument pair for this ArgumentScope
|
56
|
+
def walk_arguments(&blk)
|
57
|
+
@arguments.each(&blk)
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
# Yields each ancestor of this scope, optionally including this scope.
|
62
|
+
def walk_ancestors(inc_self = true, &blk)
|
63
|
+
scope = inc_self ? self : self.parent
|
64
|
+
while scope do
|
65
|
+
yield scope
|
66
|
+
scope = scope.parent
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
# Recursively walks and yields each descendant scopes of this scope.
|
72
|
+
def walk_children(inc_self = false, &blk)
|
73
|
+
yield self if inc_self
|
74
|
+
@children.each do |child|
|
75
|
+
yield child
|
76
|
+
child.walk_children(&blk)
|
77
|
+
end
|
27
78
|
end
|
28
79
|
|
29
80
|
|
@@ -50,26 +101,35 @@ module ArgParser
|
|
50
101
|
# line definition.
|
51
102
|
def <<(arg)
|
52
103
|
case arg
|
53
|
-
when PositionalArgument, KeywordArgument, FlagArgument, RestArgument
|
54
|
-
if
|
55
|
-
raise ArgumentError, "An argument with key '#{arg.key}' has already been defined"
|
104
|
+
when CommandArgument, PositionalArgument, KeywordArgument, FlagArgument, RestArgument
|
105
|
+
if used = self.key_used?(arg.key)
|
106
|
+
raise ArgumentError, "An argument with key '#{arg.key}' has already been defined: #{used}"
|
56
107
|
end
|
57
|
-
if arg.short_key &&
|
58
|
-
raise ArgumentError, "The short key '#{arg.short_key}' has already been registered
|
59
|
-
@short_keys[arg.short_key]}' argument"
|
108
|
+
if arg.short_key && used = self.key_used?(arg.short_key)
|
109
|
+
raise ArgumentError, "The short key '#{arg.short_key}' has already been registered: #{used}"
|
60
110
|
end
|
61
|
-
if arg.is_a?(RestArgument) && rest_args
|
111
|
+
if arg.is_a?(RestArgument) && rest_args?
|
62
112
|
raise ArgumentError, "Only one rest argument can be defined"
|
63
113
|
end
|
64
114
|
@arguments[arg.key] = arg
|
65
115
|
@short_keys[arg.short_key] = arg if arg.short_key
|
66
116
|
else
|
67
|
-
raise ArgumentError, "arg must be an instance of
|
68
|
-
"FlagArgument or RestArgument"
|
117
|
+
raise ArgumentError, "arg must be an instance of CommandArgument, PositionalArgument, " +
|
118
|
+
"KeywordArgument, FlagArgument or RestArgument"
|
69
119
|
end
|
70
120
|
end
|
71
121
|
|
72
122
|
|
123
|
+
# Add a command argument to the set of arguments in this command-line
|
124
|
+
# argument definition.
|
125
|
+
# @see CommandArgument#initialize
|
126
|
+
def command_arg(key, desc, opts = {}, &block)
|
127
|
+
cmd_arg = ArgParser::CommandArgument.new(key, desc, opts)
|
128
|
+
CommandBlock.new(self, cmd_arg, &block)
|
129
|
+
self << cmd_arg
|
130
|
+
end
|
131
|
+
|
132
|
+
|
73
133
|
# Add a positional argument to the set of arguments in this command-line
|
74
134
|
# argument definition.
|
75
135
|
# @see PositionalArgument#initialize
|
@@ -123,108 +183,23 @@ module ArgParser
|
|
123
183
|
# returned in the command-line parse results if no other value is
|
124
184
|
# specified.
|
125
185
|
def predefined_arg(lookup_key, opts = {})
|
126
|
-
arg = Argument.lookup(lookup_key)
|
127
|
-
arg
|
128
|
-
|
186
|
+
#arg = Argument.lookup(lookup_key)
|
187
|
+
arg = nil
|
188
|
+
self.walk_ancestors do |scope|
|
189
|
+
arg = scope.predefined_args && scope.predefined_args.has_key?(lookup_key)
|
190
|
+
break if arg
|
191
|
+
end
|
192
|
+
raise ArgumentError, "No predefined argument with key '#{lookup_key}' found" unless arg
|
193
|
+
arg.short_key = opts[:short_key] if opts.has_key?(:short_key)
|
194
|
+
arg.description = opts[:description] if opts.has_key?(:description)
|
195
|
+
arg.usage_break = opts[:usage_break] if opts.has_key?(:usage_break)
|
129
196
|
arg.required = opts[:required] if opts.has_key?(:required)
|
130
197
|
arg.default = opts[:default] if opts.has_key?(:default)
|
198
|
+
arg.on_parse = opts[:on_parse] if opts.has_key?(:on_parse)
|
131
199
|
self << arg
|
132
200
|
end
|
133
201
|
|
134
202
|
|
135
|
-
# Individual arguments are optional, but exactly one of +keys+ arguments
|
136
|
-
# is required.
|
137
|
-
def require_one_of(*keys)
|
138
|
-
@require_set[:one] << keys.map{ |k| self[k] }
|
139
|
-
end
|
140
|
-
|
141
|
-
|
142
|
-
# Individual arguments are optional, but at least one of +keys+ arguments
|
143
|
-
# is required.
|
144
|
-
def require_any_of(*keys)
|
145
|
-
@require_set[:any] << keys.map{ |k| self[k] }
|
146
|
-
end
|
147
|
-
|
148
|
-
|
149
|
-
# True if at least one argument is required out of multiple optional args.
|
150
|
-
def requires_some?
|
151
|
-
@require_set.size > 0
|
152
|
-
end
|
153
|
-
|
154
|
-
|
155
|
-
# @return [Parser] a Parser instance that can be used to parse this
|
156
|
-
# command-line Definition.
|
157
|
-
def parser
|
158
|
-
@parser ||= Parser.new(self)
|
159
|
-
end
|
160
|
-
|
161
|
-
|
162
|
-
# Parse the +args+ array of arguments using this command-line definition.
|
163
|
-
#
|
164
|
-
# @param args [Array, String] an array of arguments, or a String representing
|
165
|
-
# the command-line that is to be parsed.
|
166
|
-
# @return [OpenStruct, false] if successful, an OpenStruct object with all
|
167
|
-
# arguments defined as accessors, and the parsed or default values for each
|
168
|
-
# argument as values. If unsuccessful, returns false indicating a parse
|
169
|
-
# failure.
|
170
|
-
# @see Parser#parse, Parser#errors, Parser#show_usage, Parser#show_help
|
171
|
-
def parse(args = ARGV)
|
172
|
-
parser.parse(args)
|
173
|
-
end
|
174
|
-
|
175
|
-
|
176
|
-
# Return an array of parse errors.
|
177
|
-
# @see Parser#errors
|
178
|
-
def errors
|
179
|
-
parser.errors
|
180
|
-
end
|
181
|
-
|
182
|
-
|
183
|
-
# Whether user indicated they would like help on usage.
|
184
|
-
# @see Parser#show_usage
|
185
|
-
def show_usage?
|
186
|
-
parser.show_usage?
|
187
|
-
end
|
188
|
-
|
189
|
-
|
190
|
-
# Whether user indicated they would like help on supported arguments.
|
191
|
-
# @see Parser#show_help
|
192
|
-
def show_help?
|
193
|
-
parser.show_help?
|
194
|
-
end
|
195
|
-
|
196
|
-
|
197
|
-
# Validates the supplied +args+ Hash object, verifying that any argument
|
198
|
-
# set requirements have been satisfied. Returns an array of error
|
199
|
-
# messages for each set requirement that is not satisfied.
|
200
|
-
#
|
201
|
-
# @param args [Hash] a Hash containing the keys and values identified
|
202
|
-
# by the parser.
|
203
|
-
# @return [Array] a list of errors for any argument requirements that
|
204
|
-
# have not been satisfied.
|
205
|
-
def validate_requirements(args)
|
206
|
-
errors = []
|
207
|
-
@require_set.each do |req, sets|
|
208
|
-
sets.each do |set|
|
209
|
-
count = set.count{ |arg| args.has_key?(arg.key) && args[arg.key] }
|
210
|
-
case req
|
211
|
-
when :one
|
212
|
-
if count == 0
|
213
|
-
errors << "No argument has been specified for one of: #{set.join(', ')}"
|
214
|
-
elsif count > 1
|
215
|
-
errors << "Only one argument can been specified from: #{set.join(', ')}"
|
216
|
-
end
|
217
|
-
when :any
|
218
|
-
if count == 0
|
219
|
-
errors << "At least one of the arguments must be specified from: #{set.join(', ')}"
|
220
|
-
end
|
221
|
-
end
|
222
|
-
end
|
223
|
-
end
|
224
|
-
errors
|
225
|
-
end
|
226
|
-
|
227
|
-
|
228
203
|
# @return [Array] all argument keys that have been defined.
|
229
204
|
def keys
|
230
205
|
@arguments.keys
|
@@ -243,9 +218,23 @@ module ArgParser
|
|
243
218
|
end
|
244
219
|
|
245
220
|
|
221
|
+
# @return [Array] all command arguments that have been defined
|
222
|
+
def command_args
|
223
|
+
@arguments.values.select{ |arg| CommandArgument === arg }
|
224
|
+
end
|
225
|
+
|
226
|
+
|
227
|
+
# @return True if a command arg has been defined
|
228
|
+
def command_args?
|
229
|
+
command_args.size > 0
|
230
|
+
end
|
231
|
+
|
232
|
+
|
246
233
|
# @return [Array] all positional arguments that have been defined
|
247
234
|
def positional_args
|
248
|
-
@arguments.values.select{ |arg|
|
235
|
+
@arguments.values.select{ |arg| CommandArgument === arg ||
|
236
|
+
CommandInstance === arg ||
|
237
|
+
PositionalArgument === arg }
|
249
238
|
end
|
250
239
|
|
251
240
|
|
@@ -258,7 +247,10 @@ module ArgParser
|
|
258
247
|
# @return [Array] the non-positional (i.e. keyword and flag)
|
259
248
|
# arguments that have been defined.
|
260
249
|
def non_positional_args
|
261
|
-
@arguments.values.reject{ |arg|
|
250
|
+
@arguments.values.reject{ |arg| CommandArgument === arg ||
|
251
|
+
CommandInstance === arg ||
|
252
|
+
PositionalArgument === arg ||
|
253
|
+
RestArgument === arg }
|
262
254
|
end
|
263
255
|
|
264
256
|
|
@@ -317,13 +309,232 @@ module ArgParser
|
|
317
309
|
@arguments.size
|
318
310
|
end
|
319
311
|
|
312
|
+
end
|
313
|
+
|
314
|
+
|
315
|
+
# Used to define arguments specific to a CommandInstance
|
316
|
+
class CommandBlock
|
317
|
+
|
318
|
+
def initialize(arg_scope, cmd_arg, &block)
|
319
|
+
@parent = arg_scope
|
320
|
+
@command_arg = cmd_arg
|
321
|
+
self.instance_eval(&block)
|
322
|
+
end
|
323
|
+
|
324
|
+
|
325
|
+
def define_args(&block)
|
326
|
+
pre_def_arg_scope = ArgumentScope.new("Predefined args for #{@command_arg}")
|
327
|
+
pre_def_arg_scope.instance_eval(&block)
|
328
|
+
@parent.predefined_args = pre_def_arg_scope
|
329
|
+
end
|
330
|
+
|
331
|
+
|
332
|
+
def command(key, desc, opts = {}, &block)
|
333
|
+
cmd_arg_scope = ArgumentScope.new("Arguments for #{key} command", @parent)
|
334
|
+
cmd_arg_scope.instance_eval(&block) if block_given?
|
335
|
+
cmd_inst = CommandInstance.new(key, desc, @command_arg, cmd_arg_scope, opts)
|
336
|
+
@command_arg << cmd_inst
|
337
|
+
end
|
338
|
+
|
339
|
+
end
|
340
|
+
|
341
|
+
|
342
|
+
# Represents the collection of possible command-line arguments for a script.
|
343
|
+
class Definition < ArgumentScope
|
344
|
+
|
345
|
+
# @return [String] A title for the script, displayed at the top of the
|
346
|
+
# usage and help outputs.
|
347
|
+
attr_accessor :title
|
348
|
+
# @return [String] A short description of the purpose of the script, for
|
349
|
+
# display when showing the usage help.
|
350
|
+
attr_accessor :purpose
|
351
|
+
# @return [String] A copyright notice, displayed in the usage and help
|
352
|
+
# outputs.
|
353
|
+
attr_accessor :copyright
|
354
|
+
|
355
|
+
|
356
|
+
# Create a new Definition, which is a collection of valid Arguments to
|
357
|
+
# be used when parsing a command-line.
|
358
|
+
def initialize(name = 'ArgParser::Definition')
|
359
|
+
super(name)
|
360
|
+
@require_set = []
|
361
|
+
@title = $0.respond_to?(:titleize) ? $0.titleize : $0
|
362
|
+
yield self if block_given?
|
363
|
+
end
|
364
|
+
|
365
|
+
|
366
|
+
# Collapses an ArgumentScope into this Definition, representing the
|
367
|
+
# collapsed argument possibilities once a command has been identitfied
|
368
|
+
# for a CommandArgument. Think of the original Definition as being a
|
369
|
+
# superposition of possible argument definitions, with one possible
|
370
|
+
# state for each CommandInstance of each commad. Once the actual
|
371
|
+
# CommandInstance is known, we are collapsing the superposition of
|
372
|
+
# possible definitions to a lower dimensionality; only one possible
|
373
|
+
# definition remains once all CommandArgument objects are replaced by
|
374
|
+
# CommandInstances.
|
375
|
+
#
|
376
|
+
# @param cmd_inst [CommandInstance] The instance of a command that has
|
377
|
+
# been specified.
|
378
|
+
# @return [Definition] A new Definition with a set of arguments combined
|
379
|
+
# from this Definition and the selected ArgumentScope for a specific
|
380
|
+
# command instance.
|
381
|
+
def collapse(cmd_inst)
|
382
|
+
new_def = self.clone
|
383
|
+
child = cmd_inst.argument_scope
|
384
|
+
new_args = {}
|
385
|
+
new_short_keys = {}
|
386
|
+
@arguments.each do |key, arg|
|
387
|
+
if arg == cmd_inst.command_arg
|
388
|
+
new_args[key] = cmd_inst
|
389
|
+
child.walk_arguments do |key, arg|
|
390
|
+
new_args[key] = arg
|
391
|
+
new_short_keys[arg.short_key] = arg if arg.short_key
|
392
|
+
end
|
393
|
+
else
|
394
|
+
new_args[key] = arg
|
395
|
+
new_short_keys[arg.short_key] = arg if arg.short_key
|
396
|
+
end
|
397
|
+
end
|
398
|
+
new_children = @children.reject{ |c| c == cmd_inst.argument_scope } &
|
399
|
+
child.instance_variable_get(:@children)
|
400
|
+
new_def.instance_variable_set(:@arguments, new_args)
|
401
|
+
new_def.instance_variable_set(:@short_keys, new_short_keys)
|
402
|
+
new_def.instance_variable_set(:@children, new_children)
|
403
|
+
new_def
|
404
|
+
end
|
405
|
+
|
406
|
+
|
407
|
+
# Lookup a pre-defined argument (created earlier via Argument#register),
|
408
|
+
# and add it to this arguments definition.
|
409
|
+
#
|
410
|
+
# @see Argument#register
|
411
|
+
#
|
412
|
+
# @param lookup_key [String, Symbol] The key under which the pre-defined
|
413
|
+
# argument was registered.
|
414
|
+
# @param desc [String] An optional override for the argument description
|
415
|
+
# for this use of the pre-defined argument.
|
416
|
+
# @param opts [Hash] An options hash for those select properties that
|
417
|
+
# can be overridden on a pre-defined argument.
|
418
|
+
# @option opts [String] :description The argument description for this
|
419
|
+
# use of the pre-defined argument.
|
420
|
+
# @option opts [String] :usage_break The usage break for this use of
|
421
|
+
# the pre-defined argument.
|
422
|
+
# @option opts [Boolean] :required Whether this argument is a required
|
423
|
+
# (i.e. mandatory) argument.
|
424
|
+
# @option opts [String] :default The default value for the argument,
|
425
|
+
# returned in the command-line parse results if no other value is
|
426
|
+
# specified.
|
427
|
+
def predefined_arg(lookup_key, opts = {})
|
428
|
+
# TODO: walk ancestor chain looking at pre-defined arg scopes
|
429
|
+
arg = (self.predefined_args && self.predefined_args.key_used?(lookup_key)) ||
|
430
|
+
Argument.lookup(lookup_key)
|
431
|
+
arg.short_key = opts[:short_key] if opts.has_key?(:short_key)
|
432
|
+
arg.description = opts[:description] if opts.has_key?(:description)
|
433
|
+
arg.usage_break = opts[:usage_break] if opts.has_key?(:usage_break)
|
434
|
+
arg.required = opts[:required] if opts.has_key?(:required)
|
435
|
+
arg.default = opts[:default] if opts.has_key?(:default)
|
436
|
+
arg.on_parse = opts[:on_parse] if opts.has_key?(:on_parse)
|
437
|
+
self << arg
|
438
|
+
end
|
439
|
+
|
440
|
+
|
441
|
+
# Individual arguments are optional, but exactly one of +keys+ arguments
|
442
|
+
# is required.
|
443
|
+
def require_one_of(*keys)
|
444
|
+
@require_set << [:one, keys.map{ |k| self[k] }]
|
445
|
+
end
|
446
|
+
|
447
|
+
|
448
|
+
# Individual arguments are optional, but at least one of +keys+ arguments
|
449
|
+
# is required.
|
450
|
+
def require_any_of(*keys)
|
451
|
+
@require_set << [:any, keys.map{ |k| self[k] }]
|
452
|
+
end
|
453
|
+
|
454
|
+
|
455
|
+
# True if at least one argument is required out of multiple optional args.
|
456
|
+
def requires_some?
|
457
|
+
@require_set.size > 0
|
458
|
+
end
|
459
|
+
|
460
|
+
|
461
|
+
# @return [Parser] a Parser instance that can be used to parse this
|
462
|
+
# command-line Definition.
|
463
|
+
def parser
|
464
|
+
@parser ||= Parser.new(self)
|
465
|
+
end
|
466
|
+
|
467
|
+
|
468
|
+
# Parse the +args+ array of arguments using this command-line definition.
|
469
|
+
#
|
470
|
+
# @param args [Array, String] an array of arguments, or a String representing
|
471
|
+
# the command-line that is to be parsed.
|
472
|
+
# @return [OpenStruct, false] if successful, an OpenStruct object with all
|
473
|
+
# arguments defined as accessors, and the parsed or default values for each
|
474
|
+
# argument as values. If unsuccessful, returns false indicating a parse
|
475
|
+
# failure.
|
476
|
+
# @see Parser#parse, Parser#errors, Parser#show_usage, Parser#show_help
|
477
|
+
def parse(args = ARGV)
|
478
|
+
parser.parse(args)
|
479
|
+
end
|
480
|
+
|
481
|
+
|
482
|
+
# Return an array of parse errors.
|
483
|
+
# @see Parser#errors
|
484
|
+
def errors
|
485
|
+
parser.errors
|
486
|
+
end
|
487
|
+
|
488
|
+
|
489
|
+
# Whether user indicated they would like help on usage.
|
490
|
+
# @see Parser#show_usage
|
491
|
+
def show_usage?
|
492
|
+
parser.show_usage?
|
493
|
+
end
|
494
|
+
|
495
|
+
|
496
|
+
# Whether user indicated they would like help on supported arguments.
|
497
|
+
# @see Parser#show_help
|
498
|
+
def show_help?
|
499
|
+
parser.show_help?
|
500
|
+
end
|
501
|
+
|
502
|
+
|
503
|
+
# Validates the supplied +args+ Hash object, verifying that any argument
|
504
|
+
# set requirements have been satisfied. Returns an array of error
|
505
|
+
# messages for each set requirement that is not satisfied.
|
506
|
+
#
|
507
|
+
# @param args [Hash] a Hash containing the keys and values identified
|
508
|
+
# by the parser.
|
509
|
+
# @return [Array] a list of errors for any argument requirements that
|
510
|
+
# have not been satisfied.
|
511
|
+
def validate_requirements(args)
|
512
|
+
errors = []
|
513
|
+
@require_set.each do |req, set|
|
514
|
+
count = set.count{ |arg| args.has_key?(arg.key) && args[arg.key] }
|
515
|
+
case req
|
516
|
+
when :one
|
517
|
+
if count == 0
|
518
|
+
errors << "No argument has been specified for one of: #{set.join(', ')}"
|
519
|
+
elsif count > 1
|
520
|
+
errors << "Only one argument can been specified from: #{set.join(', ')}"
|
521
|
+
end
|
522
|
+
when :any
|
523
|
+
if count == 0
|
524
|
+
errors << "At least one of the arguments must be specified from: #{set.join(', ')}"
|
525
|
+
end
|
526
|
+
end
|
527
|
+
end
|
528
|
+
errors
|
529
|
+
end
|
530
|
+
|
320
531
|
|
321
532
|
# Generates a usage display string
|
322
533
|
def show_usage(out = STDERR, width = 80)
|
323
534
|
lines = ['']
|
324
|
-
|
325
|
-
|
326
|
-
|
535
|
+
usage_args = []
|
536
|
+
usage_args.concat(positional_args.map(&:to_use))
|
537
|
+
opt_args = size - usage_args.size
|
327
538
|
usage_args << (requires_some? ? 'OPTIONS' : '[OPTIONS]') if opt_args > 0
|
328
539
|
usage_args << rest_args.to_use if rest_args?
|
329
540
|
lines.concat(wrap_text("USAGE: #{RUBY_ENGINE} #{$0} #{usage_args.join(' ')}", width))
|
@@ -348,6 +559,9 @@ module ArgParser
|
|
348
559
|
if purpose
|
349
560
|
lines.concat(wrap_text(purpose, width))
|
350
561
|
lines << ''
|
562
|
+
end
|
563
|
+
if copyright
|
564
|
+
lines.concat(wrap_text("Copyright (c) #{copyright}", width))
|
351
565
|
lines << ''
|
352
566
|
end
|
353
567
|
|
@@ -355,14 +569,15 @@ module ArgParser
|
|
355
569
|
lines << '-----'
|
356
570
|
pos_args = positional_args
|
357
571
|
opt_args = size - pos_args.size
|
358
|
-
usage_args =
|
572
|
+
usage_args = []
|
573
|
+
usage_args.concat(pos_args.map(&:to_use))
|
359
574
|
usage_args << (requires_some? ? 'OPTIONS' : '[OPTIONS]') if opt_args > 0
|
360
575
|
usage_args << rest_args.to_use if rest_args?
|
361
576
|
lines.concat(wrap_text(" #{RUBY_ENGINE} #{$0} #{usage_args.join(' ')}", width))
|
362
577
|
lines << ''
|
363
578
|
|
364
579
|
if positional_args?
|
365
|
-
max = positional_args.map{ |
|
580
|
+
max = positional_args.map{ |arg| arg.to_s.length }.max
|
366
581
|
pos_args = positional_args
|
367
582
|
pos_args << rest_args if rest_args?
|
368
583
|
pos_args.each do |arg|
|
@@ -371,25 +586,48 @@ module ArgParser
|
|
371
586
|
lines << arg.usage_break
|
372
587
|
end
|
373
588
|
desc = arg.description
|
374
|
-
desc
|
589
|
+
desc += "\n[Default: #{arg.default}]" unless arg.default.nil?
|
375
590
|
wrap_text(desc, width - max - 6).each_with_index do |line, i|
|
376
591
|
lines << " %-#{max}s %s" % [[arg.to_s][i], line]
|
377
592
|
end
|
378
593
|
end
|
379
594
|
lines << ''
|
380
595
|
end
|
596
|
+
if command_args?
|
597
|
+
max = command_args.reduce(0) do |max, cmd_arg|
|
598
|
+
m = cmd_arg.commands.map{ |_, arg| arg.to_s.length }.max
|
599
|
+
m > max ? m : max
|
600
|
+
end
|
601
|
+
command_args.each do |cmd_arg|
|
602
|
+
lines << ''
|
603
|
+
lines << "#{cmd_arg.to_use}S"
|
604
|
+
lines << '--------'
|
605
|
+
cmd_arg.commands.each do |_, arg|
|
606
|
+
if arg.usage_break
|
607
|
+
lines << ''
|
608
|
+
lines << arg.usage_break
|
609
|
+
end
|
610
|
+
desc = arg.description
|
611
|
+
wrap_text(desc, width - max - 6).each_with_index do |line, i|
|
612
|
+
lines << " %-#{max}s %s" % [[arg.to_s][i], line]
|
613
|
+
end
|
614
|
+
end
|
615
|
+
lines << ''
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
381
619
|
if non_positional_args?
|
382
620
|
lines << ''
|
383
621
|
lines << 'OPTIONS'
|
384
622
|
lines << '-------'
|
385
|
-
max = non_positional_args.map{ |
|
623
|
+
max = non_positional_args.map{ |arg| arg.to_use.length }.max
|
386
624
|
non_positional_args.each do |arg|
|
387
625
|
if arg.usage_break
|
388
626
|
lines << ''
|
389
627
|
lines << arg.usage_break
|
390
628
|
end
|
391
629
|
desc = arg.description
|
392
|
-
desc
|
630
|
+
desc += "\n[Default: #{arg.default}]" unless arg.default.nil?
|
393
631
|
wrap_text(desc, width - max - 6).each_with_index do |line, i|
|
394
632
|
lines << " %-#{max}s %s" % [[arg.to_use][i], line]
|
395
633
|
end
|
@@ -461,5 +699,6 @@ module ArgParser
|
|
461
699
|
|
462
700
|
end
|
463
701
|
|
702
|
+
|
464
703
|
end
|
465
704
|
|
data/lib/arg-parser/dsl.rb
CHANGED
@@ -41,6 +41,7 @@ module ArgParser
|
|
41
41
|
@args_def ||= ArgParser::Definition.new
|
42
42
|
end
|
43
43
|
|
44
|
+
|
44
45
|
# Returns true if any arguments have been defined
|
45
46
|
def args_defined?
|
46
47
|
@args_def && @args_def.args.size > 0
|
@@ -64,6 +65,20 @@ module ArgParser
|
|
64
65
|
args_def.purpose = desc
|
65
66
|
end
|
66
67
|
|
68
|
+
# Sets the copyright notice to be displayed on the help screen or on
|
69
|
+
# startup.
|
70
|
+
def copyright(txt)
|
71
|
+
args_def.copyright = txt
|
72
|
+
end
|
73
|
+
|
74
|
+
# Define a new command argument.
|
75
|
+
# @see CommandArgument#initialize
|
76
|
+
def command_arg(key, desc, opts = {}, &block)
|
77
|
+
opts.merge!(@arg_opts){ |k, e, n| e || n } if @arg_opts
|
78
|
+
@arg_opts = nil
|
79
|
+
args_def.command_arg(key, desc, opts, &block)
|
80
|
+
end
|
81
|
+
|
67
82
|
# Define a new positional argument.
|
68
83
|
# @see PositionalArgument#initialize
|
69
84
|
def positional_arg(key, desc, opts = {}, &block)
|
@@ -96,6 +111,26 @@ module ArgParser
|
|
96
111
|
args_def.rest_arg(key, desc, opts, &block)
|
97
112
|
end
|
98
113
|
|
114
|
+
|
115
|
+
# Define a set of pre-defined arguments for later use
|
116
|
+
def define_args(label = nil, &block)
|
117
|
+
pre_def_arg_scope = ArgumentScope.new(label || "Predefined args")
|
118
|
+
pre_def_arg_scope.instance_eval(&block)
|
119
|
+
pre_def_arg_scope
|
120
|
+
end
|
121
|
+
|
122
|
+
# Set the ArgumentScope to use for looking up pre-defined arguments
|
123
|
+
def with_predefined_args(scope, &blk)
|
124
|
+
if block_given?
|
125
|
+
pre_defs = args_def.predefined_args
|
126
|
+
args_def.predefined_args = scope
|
127
|
+
yield
|
128
|
+
args_def.predefined_args = pre_defs
|
129
|
+
else
|
130
|
+
args_def.predefined_args = scope
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
99
134
|
# Use a pre-defined argument.
|
100
135
|
# @see Definition#predefined_arg
|
101
136
|
def predefined_arg(lookup_key, desc = nil, opts = {})
|
data/lib/arg-parser/parser.rb
CHANGED
@@ -40,11 +40,27 @@ module ArgParser
|
|
40
40
|
end
|
41
41
|
|
42
42
|
|
43
|
-
# Parse the specified Array
|
44
|
-
# nil.
|
45
|
-
#
|
46
|
-
# not
|
47
|
-
#
|
43
|
+
# Parse the specified Array<String> of +tokens+, or ARGV if +tokens+ is
|
44
|
+
# nil. If the parse is successful, an OpenStruct object is returned with
|
45
|
+
# values for all defined arguments as members of the OpenStruct.
|
46
|
+
# If parsing is not succssful, it may be due to parse errors or because
|
47
|
+
# a help flag was encountered. The +show_usage+ or +show_help+ flags
|
48
|
+
# will be set (the former in the case of parse errors, and the latter if
|
49
|
+
# help was requested), as well as the +errors+ property if there were parse
|
50
|
+
# errors.
|
51
|
+
#
|
52
|
+
# @note After parse has been called, the argument Definition used by the
|
53
|
+
# parser may have been replaced if a command argument value was specified.
|
54
|
+
# This new definition can be used to show context-specific help or usage
|
55
|
+
# details.
|
56
|
+
#
|
57
|
+
# @param tokens [Array<String>] The tokens to be parsed using the
|
58
|
+
# argument definition set for this parser. If not specified, defaults
|
59
|
+
# to ARGV.
|
60
|
+
# @return [False|OpenStruct] false if unable to parse successfully, or
|
61
|
+
# an OpenStruct with accessors for every defined argument if the parse
|
62
|
+
# was successful. Arguments whose values are not specified will contain
|
63
|
+
# the agument default value, or nil if no default is specified.
|
48
64
|
def parse(tokens = ARGV)
|
49
65
|
@show_usage = nil
|
50
66
|
@show_help = nil
|
@@ -73,8 +89,13 @@ module ArgParser
|
|
73
89
|
# Evaluate the list of values in +tokens+, and classify them as either
|
74
90
|
# keyword/value pairs, or positional arguments. Ideally this would be
|
75
91
|
# done without any reference to the defined arguments, but unfortunately
|
76
|
-
# a
|
77
|
-
#
|
92
|
+
# there are a couple of issues that require context to be able to handle
|
93
|
+
# properly:
|
94
|
+
# - a keyword arg cannot be distinguished from a flag arg followed by a
|
95
|
+
# positional arg without the context of what arguments are expected.
|
96
|
+
# - command arguments will normally have their own set of additional
|
97
|
+
# arguments that must be added once we know which command has been
|
98
|
+
# entered.
|
78
99
|
def classify_tokens(tokens)
|
79
100
|
if tokens.is_a?(String)
|
80
101
|
require 'csv'
|
@@ -86,37 +107,68 @@ module ArgParser
|
|
86
107
|
rest_vals = []
|
87
108
|
|
88
109
|
arg = nil
|
110
|
+
pos_args = @definition.positional_args
|
89
111
|
tokens.each_with_index do |token, i|
|
112
|
+
if token.downcase == 'help' && i == 0
|
113
|
+
# Accept 'help' as the first token
|
114
|
+
@show_help = true
|
115
|
+
next
|
116
|
+
end
|
90
117
|
case token
|
91
118
|
when '/?', '-?', '--help'
|
92
119
|
@show_help = true
|
93
120
|
when /^[-\/]([a-z0-9]+)$/i
|
121
|
+
# One or more short keys
|
94
122
|
$1.to_s.each_char do |sk|
|
95
123
|
kw_vals[arg] = nil if arg
|
96
124
|
arg = @definition[sk]
|
97
125
|
if FlagArgument === arg
|
98
126
|
kw_vals[arg] = true
|
99
127
|
arg = nil
|
128
|
+
elsif PositionalArgument == arg
|
129
|
+
pos_args.delete(arg)
|
100
130
|
end
|
101
131
|
end
|
102
|
-
when /^--(no
|
132
|
+
when /^--(no[nt]?-)?(.+)/i
|
133
|
+
# Long key
|
103
134
|
kw_vals[arg] = nil if arg
|
104
135
|
arg = @definition[$2]
|
105
136
|
if FlagArgument === arg || (KeywordArgument === arg && $1)
|
106
137
|
kw_vals[arg] = $1 ? false : true
|
107
138
|
arg = nil
|
139
|
+
elsif PositionalArgument === arg
|
140
|
+
pos_args.delete(arg)
|
108
141
|
end
|
109
142
|
when '--'
|
110
143
|
# All subsequent values are rest args
|
111
144
|
kw_vals[arg] = nil if arg
|
145
|
+
unless rest_vals.empty?
|
146
|
+
self.errors << "Too many positional arguments supplied before -- token"
|
147
|
+
end
|
112
148
|
rest_vals = tokens[(i + 1)..-1]
|
113
149
|
break
|
114
150
|
else
|
115
151
|
if arg
|
116
152
|
kw_vals[arg] = token
|
153
|
+
elsif pos_args.size > 0
|
154
|
+
arg = pos_args.shift
|
155
|
+
if CommandArgument === arg
|
156
|
+
if cmd_inst = arg[token]
|
157
|
+
# Merge command's arg set with the current definition
|
158
|
+
@definition = @definition.collapse(cmd_inst)
|
159
|
+
# Insert command positional arguments
|
160
|
+
pos_args.insert(0, *cmd_inst.argument_scope.positional_args)
|
161
|
+
pos_vals << cmd_inst.command_value
|
162
|
+
else
|
163
|
+
self.errors << "'#{token}' is not a valid value for #{arg}; valid values are: #{
|
164
|
+
arg.commands.keys.join(', ')}"
|
165
|
+
end
|
166
|
+
arg = nil # Commands can't be sensitive
|
167
|
+
else
|
168
|
+
pos_vals << token
|
169
|
+
end
|
117
170
|
else
|
118
|
-
|
119
|
-
arg = @definition.positional_args[i]
|
171
|
+
rest_vals << token
|
120
172
|
end
|
121
173
|
tokens[i] = '******' if arg && arg.sensitive?
|
122
174
|
arg = nil
|
@@ -154,11 +206,14 @@ module ArgParser
|
|
154
206
|
end
|
155
207
|
|
156
208
|
# Process rest values
|
157
|
-
if
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
rest_vals.size == 1 ? '
|
209
|
+
if rest_vals.size > 0
|
210
|
+
if rest_arg = @definition.rest_args
|
211
|
+
result[rest_arg.key] = process_arg_val(rest_arg, rest_vals, result)
|
212
|
+
else
|
213
|
+
self.errors << "#{rest_vals.size} rest #{rest_vals.size == 1 ? 'value' : 'values'} #{
|
214
|
+
rest_vals.size == 1 ? 'was' : 'were'} supplied (#{rest_vals.join(', ')
|
215
|
+
}), but no rest argument is defined"
|
216
|
+
end
|
162
217
|
end
|
163
218
|
|
164
219
|
# Default unspecified arguments
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: arg-parser
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adam Gardiner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-07-04 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |2
|
14
14
|
ArgParser is a simple, yet powerful command-line argument parser, with
|
@@ -19,13 +19,13 @@ executables: []
|
|
19
19
|
extensions: []
|
20
20
|
extra_rdoc_files: []
|
21
21
|
files:
|
22
|
-
- README.md
|
23
22
|
- LICENSE
|
23
|
+
- README.md
|
24
|
+
- lib/arg-parser.rb
|
24
25
|
- lib/arg-parser/argument.rb
|
25
26
|
- lib/arg-parser/definition.rb
|
26
27
|
- lib/arg-parser/dsl.rb
|
27
28
|
- lib/arg-parser/parser.rb
|
28
|
-
- lib/arg-parser.rb
|
29
29
|
- lib/arg_parser.rb
|
30
30
|
homepage: https://github.com/agardiner/arg-parser
|
31
31
|
licenses: []
|
@@ -36,17 +36,17 @@ require_paths:
|
|
36
36
|
- lib
|
37
37
|
required_ruby_version: !ruby/object:Gem::Requirement
|
38
38
|
requirements:
|
39
|
-
- -
|
39
|
+
- - ">="
|
40
40
|
- !ruby/object:Gem::Version
|
41
41
|
version: '0'
|
42
42
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
43
43
|
requirements:
|
44
|
-
- -
|
44
|
+
- - ">="
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: '0'
|
47
47
|
requirements: []
|
48
48
|
rubyforge_project:
|
49
|
-
rubygems_version: 2.
|
49
|
+
rubygems_version: 2.5.2.3
|
50
50
|
signing_key:
|
51
51
|
specification_version: 4
|
52
52
|
summary: ArgParser is a simple, yet powerful, command-line argument (option) parser
|