arg-parser 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|