ArgParser
ArgParser is a small library for parsing command-line arguments (or indeed any string or Array of text). It provides a simple DSL for defining the possible arguments, which may be one of the following:
- Positional arguments, where values are specified without any keyword, and their meaning is based on the order in which they appear in the command-line.
- Keyword arguments, which are identified by a long- or short-key preceding the value.
- Flag arguments, which are essentially boolean keyword arguments. The presence of the key implies a true value.
- A Rest argument, which is an argument definition that takes 0 or more trailing positional arguments.
Usage
ArgParser is supplied as a gem, and has no dependencies. To use it, simply:
gem install arg-parser
ArgParser provides a DSL module that can be included into any class to provide argument parsing.
require 'arg-parser'
class MyClass
include ArgParser::DSL
purpose <<-EOT
This is where you specify the purpose of your program. It will be displayed in the
generated help screen if you pass /? or --help on the command-line.
EOT
positional_arg :my_positional_arg, 'This is a positional arg'
keyword_arg :my_keyword_arg, 'This is a keyword arg; if not specified, returns value1',
default: 'value1'
# An optional value keyword argument. If not specified, returns false. If specified
# without a value, returns 'maybe'. If specified with a value, returns the value.
keyword_arg :val_opt, 'This is a keyword arg with optional value',
value_optional: 'maybe'
flag_arg :flag, 'This is a flag argument'
rest_arg :files, 'This is where we specify that remaining args will be collected in an array'
def run
if opts = parse_arguments
# Do something with opts.my_positional_arg, opts.my_keyword_arg,
# opts.flag, and opts.files
# ...
else
# False is returned if argument parsing was not completed
# This may be due to an error or because the help command
# was used (by specifying --help or /?). The #show_help?
# method returns true if help was requested, otherwise if
# a parse error was encountered, #show_usage? is true and
# parse errors are in #parse_errors
show_help? ? show_help : show_usage
end
end
end
MyClass.new.run
Functionality
ArgParser provides a fairly broad range of functionality for argument parsing. Following is a non-exhaustive list of features:
- Built-in usage and help display
- Multiple different types of arguments can be defined:
- Command arguments indicate an argument that can be one of a limited selection of pre-defined argument values. Each command argument must define its commands, as well as any command-specific arguments.
- Positional arguments are arguments that are normally supplied without any key, and are matched to an argument definition based on the order in which they are encountered. Positional arguments can also be supplied using the key if preferred. Generally only a small number of positional arguments would be defined for those parameters required on every run.
- Keyword arguments are arguments that must be specified by using a key to indicate which argument the subsequent value is for.
- Flag arguments are boolean arguments that normally default to nil, but can be set to true just by specifying the argument key. It is also possible to define flag arguments with a default value of true; the argument can then be set to false by speciying the argument name with --no- (or --not- or --non- when those are more understandable).
- Rest arguments use a single argument key to collect all remaining values from the command-line, and return these as an array in the resulting argument results.
- Parsed results are returned in an OpenStruct, with every defined argument having a value set (whether
or not it was encountered on the command-line). Use the
default: <value>
option to set default values for non-specified arguments where you don't want nil returned in the parse results. Note that in the case of command arguments with command-specific arguments, only the arguments from the selected command appear in the results. - Mandatory vs Optional: All arguments your program accepts can be defined as optional or mandatory.
By default, positional arguments are considered mandatory, and all others default to optional. To change
the default, simply specify
required: true
orrequired: false
when defining the argument. - Keyword arguments can also accept an optional value, using the
value_optional
option. If a keyword argument is supplied without a value, it returns the value of thevalue_optional
option. - Short-keys and long-keys: Arguments are defined with a long_key name which will be used to access the
parsed value in the results. However, arguments can also define a single letter or digit short-key form
which can be used as an alternate means for indicating a value. To define a short key, simply pass
short_key: '<letter_or_digit>'
when defining an argument. - Requiring from a set of arguments: If you need to ensure that only one of several mutually exclusive
arguments is specified, use
require_one_of <arg1>, <arg2>, ..., <argN>
. If you need to ensure that at least one argument is specified from a list of arguments, userequire_any_of <arg1>, <arg2>, ..., <argN>
- Validation: Arguments can define validation requirements that must be satisfied. This can take several
forms:
- List of values: Pass an array containing the allowed values the argument can take.
validation: %w{one two three}
- Regular expression: Pass a regular expression that the argument value must satisfy.
validation: /.*\.rb$/
- Proc: Pass a proc that will be called to validate the supplied argument value. If the proc returns
a non-falsey value, the argument is accepted, otherwise it is rejected.
validation: lambda{ |val, arg, hsh| val.upcase == 'TRUE' }
- List of values: Pass an array containing the allowed values the argument can take.
- On-parse handler: A proc can be passed that will be called when the argument value is encountered
during parsing. The return value of the proc will be used as the argument result.
on_parse: lambda{ |val, arg, hsh| val.split(',') }
- On-parse handler reuse: Common parse handlers can be registered and used on multiple arguments. Handlers are registered for reuse via the DSL method #register_parse_handler. To use a registered parse handler for a particular argument, just pass the key under which the handler is registered.
- Pre-defined arguments: Arguments can be registered under a key, and then re-used across multiple definitions via the #predefined_arg DSL method. Common arguments would typically be defined in a shared file that was included into each job. See Argument.register and DSL.predefined_arg.
Commands
Often command-line utilities can perform several actions, and we want to define an argument that will
be used to indicate to the program which action it should perform. This can be done very easily using
positional arguments, and setting a list of allowed values in the argument validation, e.g.
positional_arg :command, 'The command to perform', validation: %w{list export import}
However, it is often the case that the different commands require different additional arguments to be specified, and this makes it necessary to make any argument not required by all commands into an optional argument. However, now the program has to ensure that it checks for those optional arguments that are required for the command to actually be present.
In this circumstance, it is better to instead make use of command arguments. These are arguments that look like positional arguments that accept only a specific set of values, however, each command can then define its own set of additional arguments (of any type). During parsing, when a command argument is found, the argument definition is updated on-the-fly to include any additional arguments defined by the now-matched command. Additionally, the usage and help displays are also updated to reflect the arguments defined by the specified command.
Defining command arguments is a little more complex, due to the need to associate command arguments with the commands.
require 'arg-parser'
class MyClass
include ArgParser::DSL
purpose <<-EOT
This is where you specify the purpose of your program. It will be displayed in the
generated help screen if you pass /? or --help on the command-line.
EOT
command_arg :command, 'The command to perform' do
# We often want the same arguments to appear in several but not all commands.
# Rather than re-defining them for each command, we can first define them, and
# then reference them in the commands that need them.
define_args do
positional_arg :source, 'The source to use'
positional_arg :target, 'The target to use'
positional_arg :local_dir, 'The path to a local directory',
default: 'extracts'
end
command :list, 'List available items for transfer' do
predefined_arg :source
end
command :export, 'Export items to a local directory' do
predefined_arg :source
predefined_arg :local_dir
end
command :import, 'Import items from a local directory' do
predefined_arg :local_dir
predefined_arg :target
end
end
positional_arg :user_id, 'The user id for connecting to the source/target'
positional_arg :password, 'The password for connecting to the source/target',
sensitive: true
...
def run
if opts = parse_arguments
case opts.command
when 'list'
do_list(opts.source)
when 'export'
do_export(opts.source, opts.local_dir)
when 'import'
do_import(opts.local_dir, opts.target)
end
else
show_help? ? show_help : show_usage
end
end
end
MyClass.new.run
Pre-defined Arguments
We saw above in the case of commands that it can be useful to pre-define an argument with all its parameters once, and then reference that argument in multiple separate commands. This allows us to follow DRY principles when defining an argument, making maintenance simpler and reducing the possibility of mis-configuring one or more instances of the same argument.
This principle does not only apply to arguments for use within a single utility or set of commands. Arguments can also be pre-defined in an argument scope, and then referenced in multiple utilities. Thus if there are common arguments needed in many utilities that share some common code, you can also define these common arguments in the shared code, and then include the pre-defined arguments into each utility that shares that functionality.
require 'arg-parser'
class MyLibrary
include ArgParser::DSL
LIBRARY_ARGS = define_args('Database Args') do
positional_arg :database, 'The name of the database to connect to',
on_parse: lambda{ |val, arg, hsh| # Lookup database connection details and return as hash }
positional_arg :user_id, 'The user id for connecting to the database'
positional_arg :password, 'The password for connecting to the database',
sensitive: true
end
end
class Utility
include 'arg-parser'
with_predefined_args MyLibrary::LIBRARY_ARGS do
predefined_arg :database
predefined_arg :user_id
predefined_arg :password
end
keyword_arg :log_level, 'Set the log level for database interactions'
...
def run
if opts = parse_arguments
# Connect to database, set log level etc
else
show_help? ? show_help : show_usage
end
end
end
MyClass.new.run