blob: d36be633e2d7e1e1a038b546ac71dca3ca65f05c [file] [log] [blame]
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// This library lets you define parsers for parsing raw command-line arguments
/// into a set of options and values using [GNU][] and [POSIX][] style options.
///
/// ## Defining options ##
///
/// To use this library, you create an [ArgParser] object which will contain
/// the set of options you support:
///
/// var parser = new ArgParser();
///
/// Then you define a set of options on that parser using [addOption()] and
/// [addFlag()]. The minimal way to create an option is:
///
/// parser.addOption('name');
///
/// This creates an option named "name". Options must be given a value on the
/// command line. If you have a simple on/off flag, you can instead use:
///
/// parser.addFlag('name');
///
/// Flag options will, by default, accept a 'no-' prefix to negate the option.
/// This can be disabled like so:
///
/// parser.addFlag('name', negatable: false);
///
/// (From here on out "option" will refer to both "regular" options and flags.
/// In cases where the distinction matters, we'll use "non-flag option".)
///
/// Options may have an optional single-character abbreviation:
///
/// parser.addOption('mode', abbr: 'm');
/// parser.addFlag('verbose', abbr: 'v');
///
/// They may also specify a default value. The default value will be used if the
/// option isn't provided:
///
/// parser.addOption('mode', defaultsTo: 'debug');
/// parser.addFlag('verbose', defaultsTo: false);
///
/// The default value for non-flag options can be any [String]. For flags, it
/// must be a [bool].
///
/// To validate non-flag options, you may provide an allowed set of values. When
/// you do, it will throw a [FormatException] when you parse the arguments if
/// the value for an option is not in the allowed set:
///
/// parser.addOption('mode', allowed: ['debug', 'release']);
///
/// You can provide a callback when you define an option. When you later parse
/// a set of arguments, the callback for that option will be invoked with the
/// value provided for it:
///
/// parser.addOption('mode', callback: (mode) => print('Got mode $mode));
/// parser.addFlag('verbose', callback: (verbose) {
/// if (verbose) print('Verbose');
/// });
///
/// The callback for each option will *always* be called when you parse a set of
/// arguments. If the option isn't provided in the args, the callback will be
/// passed the default value, or `null` if there is none set.
///
/// ## Parsing arguments ##
///
/// Once you have an [ArgParser] set up with some options and flags, you use it
/// by calling [ArgParser.parse()] with a set of arguments:
///
/// var results = parser.parse(['some', 'command', 'line', 'args']);
///
/// These will usually come from `new Options().arguments`, but you can pass in
/// any list of strings. It returns an instance of [ArgResults]. This is a
/// map-like object that will return the value of any parsed option.
///
/// var parser = new ArgParser();
/// parser.addOption('mode');
/// parser.addFlag('verbose', defaultsTo: true);
/// var results = parser.parse('['--mode', 'debug', 'something', 'else']);
///
/// print(results['mode']); // debug
/// print(results['verbose']); // true
///
/// The [parse()] method will stop as soon as it reaches `--` or anything that
/// it doesn't recognize as an option, flag, or option value. If there are still
/// arguments left, they will be provided to you in
/// [ArgResults.rest].
///
/// print(results.rest); // ['something', 'else']
///
/// ## Specifying options ##
///
/// To actually pass in options and flags on the command line, use GNU or POSIX
/// style. If you define an option like:
///
/// parser.addOption('name', abbr: 'n');
///
/// Then a value for it can be specified on the command line using any of:
///
/// --name=somevalue
/// --name somevalue
/// -nsomevalue
/// -n somevalue
///
/// Given this flag:
///
/// parser.addFlag('name', abbr: 'n');
///
/// You can set it on using one of:
///
/// --name
/// -n
///
/// Or set it off using:
///
/// --no-name
///
/// Multiple flag abbreviation can also be collapsed into a single argument. If
/// you define:
///
/// parser.addFlag('verbose', abbr: 'v');
/// parser.addFlag('french', abbr: 'f');
/// parser.addFlag('iambic-pentameter', abbr: 'i');
///
/// Then all three flags could be set using:
///
/// -vfi
///
/// By default, an option has only a single value, with later option values
/// overriding earlier ones; for example:
///
/// var parser = new ArgParser();
/// parser.addOption('mode');
/// var results = parser.parse(['--mode', 'on', '--mode', 'off']);
/// print(results['mode']); // prints 'off'
///
/// If you need multiple values, set the `allowMultiple` flag. In that
/// case the option can occur multiple times and when parsing arguments a
/// List of values will be returned:
///
/// var parser = new ArgParser();
/// parser.addOption('mode', allowMultiple: true);
/// var results = parser.parse(['--mode', 'on', '--mode', 'off']);
/// print(results['mode']); // prints '[on, off]'
///
/// ## Defining commands ##
///
/// In addition to *options*, you can also define *commands*. A command is a
/// named argument that has its own set of options. For example, when you run:
///
/// $ git commit -a
///
/// The executable is `git`, the command is `commit`, and the `-a` option is an
/// option passed to the command. You can add a command like so:
///
/// var parser = new ArgParser();
/// var command = parser.addCommand("commit");
/// command.addFlag('all', abbr: 'a');
///
/// It returns another [ArgParser] which you can use to define options and
/// subcommands on that command. When an argument list is parsed, you can then
/// determine which command was entered and what options were provided for it.
///
/// var results = parser.parse(['commit', '-a']);
/// print(results.command.name); // "commit"
/// print(results.command['a']); // true
///
/// ## Displaying usage ##
///
/// This library can also be used to automatically generate nice usage help
/// text like you get when you run a program with `--help`. To use this, you
/// will also want to provide some help text when you create your options. To
/// define help text for the entire option, do:
///
/// parser.addOption('mode', help: 'The compiler configuration',
/// allowed: ['debug', 'release']);
/// parser.addFlag('verbose', help: 'Show additional diagnostic info');
///
/// For non-flag options, you can also provide detailed help for each expected
/// value using a map:
///
/// parser.addOption('arch', help: 'The architecture to compile for',
/// allowedHelp: {
/// 'ia32': 'Intel x86',
/// 'arm': 'ARM Holding 32-bit chip'
/// });
///
/// If you define a set of options like the above, then calling this:
///
/// print(parser.getUsage());
///
/// Will display something like:
///
/// --mode The compiler configuration
/// [debug, release]
///
/// --[no-]verbose Show additional diagnostic info
/// --arch The architecture to compile for
///
/// [arm] ARM Holding 32-bit chip
/// [ia32] Intel x86
///
/// To assist the formatting of the usage help, single line help text will
/// be followed by a single new line. Options with multi-line help text
/// will be followed by two new lines. This provides spatial diversity between
/// options.
///
/// [posix]: http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap12.html#tag_12_02
/// [gnu]: http://www.gnu.org/prep/standards/standards.html#Command_002dLine-Interfaces
library args;
import 'src/parser.dart';
import 'src/usage.dart';
/// A class for taking a list of raw command line arguments and parsing out
/// options and flags from them.
class ArgParser {
/// The options that have been defined for this parser.
final Map<String, Option> options = <String, Option>{};
/// The commands that have been defined for this parser.
final Map<String, ArgParser> commands = <String, ArgParser>{};
/// Creates a new ArgParser.
ArgParser();
/// Defines a command. A command is a named argument which may in turn
/// define its own options and subcommands. Returns an [ArgParser] that can
/// be used to define the command's options.
ArgParser addCommand(String name) {
// Make sure the name isn't in use.
if (commands.containsKey(name)) {
throw ArgumentError('Duplicate command "$name".');
}
var command = ArgParser();
commands[name] = command;
return command;
}
/// Defines a flag. Throws an [ArgumentError] if:
///
/// * There is already an option named [name].
/// * There is already an option using abbreviation [abbr].
void addFlag(String name,
{String abbr,
String help,
bool defaultsTo = false,
bool negatable = true,
void callback(bool value)}) {
_addOption(name, abbr, help, null, null, defaultsTo,
callback == null ? null : (value) => callback(value as bool),
isFlag: true, negatable: negatable);
}
/// Defines a value-taking option. Throws an [ArgumentError] if:
///
/// * There is already an option with name [name].
/// * There is already an option using abbreviation [abbr].
void addOption(String name,
{String abbr,
String help,
List<String> allowed,
Map<String, String> allowedHelp,
String defaultsTo,
void callback(dynamic value),
bool allowMultiple = false}) {
_addOption(name, abbr, help, allowed, allowedHelp, defaultsTo, callback,
isFlag: false, allowMultiple: allowMultiple);
}
void _addOption(
String name,
String abbr,
String help,
List<String> allowed,
Map<String, String> allowedHelp,
dynamic defaultsTo,
void callback(dynamic value),
{bool isFlag,
bool negatable = false,
bool allowMultiple = false}) {
// Make sure the name isn't in use.
if (options.containsKey(name)) {
throw ArgumentError('Duplicate option "$name".');
}
// Make sure the abbreviation isn't too long or in use.
if (abbr != null) {
if (abbr.length > 1) {
throw ArgumentError(
'Abbreviation "$abbr" is longer than one character.');
}
var existing = findByAbbreviation(abbr);
if (existing != null) {
throw ArgumentError(
'Abbreviation "$abbr" is already used by "${existing.name}".');
}
}
options[name] = Option(
name, abbr, help, allowed, allowedHelp, defaultsTo, callback,
isFlag: isFlag, negatable: negatable, allowMultiple: allowMultiple);
}
/// Parses [args], a list of command-line arguments, matches them against the
/// flags and options defined by this parser, and returns the result.
ArgResults parse(List<String> args) =>
Parser(null, this, args.toList()).parse();
/// Generates a string displaying usage information for the defined options.
/// This is basically the help text shown on the command line.
String getUsage() => Usage(this).generate();
/// Get the default value for an option. Useful after parsing to test
/// if the user specified something other than the default.
dynamic getDefault(String option) {
if (!options.containsKey(option)) {
throw ArgumentError('No option named $option');
}
return options[option].defaultValue;
}
/// Finds the option whose abbreviation is [abbr], or `null` if no option has
/// that abbreviation.
Option findByAbbreviation(String abbr) {
return options.values.firstWhere((option) => option.abbreviation == abbr,
orElse: () => null);
}
}
/// A command-line option. Includes both flags and options which take a value.
class Option {
final String name;
final String abbreviation;
final List allowed;
final dynamic defaultValue;
final Function callback;
final String help;
final Map<String, String> allowedHelp;
final bool isFlag;
final bool negatable;
final bool allowMultiple;
Option(this.name, this.abbreviation, this.help, this.allowed,
this.allowedHelp, this.defaultValue, this.callback,
{this.isFlag, this.negatable, this.allowMultiple = false});
}
/// The results of parsing a series of command line arguments using
/// [ArgParser.parse()]. Includes the parsed options and any remaining unparsed
/// command line arguments.
class ArgResults {
final Map<String, dynamic> _options;
/// If these are the results for parsing a command's options, this will be
/// the name of the command. For top-level results, this returns `null`.
final String name;
/// The command that was selected, or `null` if none was. This will contain
/// the options that were selected for that command.
final ArgResults command;
/// The remaining command-line arguments that were not parsed as options or
/// flags. If `--` was used to separate the options from the remaining
/// arguments, it will not be included in this list.
final List<String> rest;
/// Creates a new [ArgResults].
ArgResults(this._options, this.name, this.command, this.rest);
/// Gets the parsed command-line option named [name].
dynamic operator [](String name) {
if (!_options.containsKey(name)) {
throw ArgumentError('Could not find an option named "$name".');
}
return _options[name];
}
List<String> get options => _options.keys.toList();
}