ArgParser class
A class for taking a list of raw command line arguments and parsing out options and flags from them.
class ArgParser { static const _SOLO_OPT = const RegExp(r'^-([a-zA-Z0-9])$'); static const _ABBR_OPT = const RegExp(r'^-([a-zA-Z0-9]+)(.*)$'); static const _LONG_OPT = const RegExp(r'^--([a-zA-Z\-_0-9]+)(=(.*))?$'); final Map<String, _Option> _options; /** * The names of the options, in the order that they were added. This way we * can generate usage information in the same order. */ // TODO(rnystrom): Use an ordered map type, if one appears. final List<String> _optionNames; /** The current argument list being parsed. Set by [parse()]. */ List<String> _args; /** Index of the current argument being parsed in [_args]. */ int _current; /** Creates a new ArgParser. */ ArgParser() : _options = <String, _Option>{}, _optionNames = <String>[]; /** * 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, 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(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, defaultsTo, void callback(value), {bool isFlag, bool negatable: false, bool allowMultiple: false}) { // Make sure the name isn't in use. if (_options.containsKey(name)) { throw new ArgumentError('Duplicate option "$name".'); } // Make sure the abbreviation isn't too long or in use. if (abbr != null) { if (abbr.length > 1) { throw new ArgumentError( 'Abbreviation "$abbr" is longer than one character.'); } var existing = _findByAbbr(abbr); if (existing != null) { throw new ArgumentError( 'Abbreviation "$abbr" is already used by "${existing.name}".'); } } _options[name] = new _Option(name, abbr, help, allowed, allowedHelp, defaultsTo, callback, isFlag: isFlag, negatable: negatable, allowMultiple: allowMultiple); _optionNames.add(name); } /** * 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) { _args = args; _current = 0; var results = {}; // Initialize flags to their defaults. _options.forEach((name, option) { if (option.allowMultiple) { results[name] = []; } else { results[name] = option.defaultValue; } }); // Parse the args. for (_current = 0; _current < args.length; _current++) { var arg = args[_current]; if (arg == '--') { // Reached the argument terminator, so stop here. _current++; break; } // Try to parse the current argument as an option. Note that the order // here matters. if (_parseSoloOption(results)) continue; if (_parseAbbreviation(results)) continue; if (_parseLongOption(results)) continue; // If we got here, the argument doesn't look like an option, so stop. break; } // Set unspecified multivalued arguments to their default value, // if any, and invoke the callbacks. for (var name in _optionNames) { var option = _options[name]; if (option.allowMultiple && results[name].length == 0 && option.defaultValue != null) { results[name].add(option.defaultValue); } if (option.callback != null) option.callback(results[name]); } // Add in the leftover arguments we didn't parse. return new ArgResults(results, _args.getRange(_current, _args.length - _current)); } /** * Generates a string displaying usage information for the defined options. * This is basically the help text shown on the command line. */ String getUsage() { return new _Usage(this).generate(); } /** * Called during parsing to validate the arguments. Throws a * [FormatException] if [condition] is `false`. */ _validate(bool condition, String message) { if (!condition) throw new FormatException(message); } /** Validates and stores [value] as the value for [option]. */ _setOption(Map results, _Option option, value) { // See if it's one of the allowed values. if (option.allowed != null) { _validate(option.allowed.some((allow) => allow == value), '"$value" is not an allowed value for option "${option.name}".'); } if (option.allowMultiple) { results[option.name].add(value); } else { results[option.name] = value; } } /** * Pulls the value for [option] from the next argument in [_args] (where the * current option is at index [_current]. Validates that there is a valid * value there. */ void _readNextArgAsValue(Map results, _Option option) { _current++; // Take the option argument from the next command line arg. _validate(_current < _args.length, 'Missing argument for "${option.name}".'); // Make sure it isn't an option itself. _validate(!_ABBR_OPT.hasMatch(_args[_current]) && !_LONG_OPT.hasMatch(_args[_current]), 'Missing argument for "${option.name}".'); _setOption(results, option, _args[_current]); } /** * Tries to parse the current argument as a "solo" option, which is a single * hyphen followed by a single letter. We treat this differently than * collapsed abbreviations (like "-abc") to handle the possible value that * may follow it. */ bool _parseSoloOption(Map results) { var soloOpt = _SOLO_OPT.firstMatch(_args[_current]); if (soloOpt == null) return false; var option = _findByAbbr(soloOpt[1]); _validate(option != null, 'Could not find an option or flag "-${soloOpt[1]}".'); if (option.isFlag) { _setOption(results, option, true); } else { _readNextArgAsValue(results, option); } return true; } /** * Tries to parse the current argument as a series of collapsed abbreviations * (like "-abc") or a single abbreviation with the value directly attached * to it (like "-mrelease"). */ bool _parseAbbreviation(Map results) { var abbrOpt = _ABBR_OPT.firstMatch(_args[_current]); if (abbrOpt == null) return false; // If the first character is the abbreviation for a non-flag option, then // the rest is the value. var c = abbrOpt[1].substring(0, 1); var first = _findByAbbr(c); if (first == null) { _validate(false, 'Could not find an option with short name "-$c".'); } else if (!first.isFlag) { // The first character is a non-flag option, so the rest must be the // value. var value = '${abbrOpt[1].substring(1)}${abbrOpt[2]}'; _setOption(results, first, value); } else { // If we got some non-flag characters, then it must be a value, but // if we got here, it's a flag, which is wrong. _validate(abbrOpt[2] == '', 'Option "-$c" is a flag and cannot handle value ' '"${abbrOpt[1].substring(1)}${abbrOpt[2]}".'); // Not an option, so all characters should be flags. for (var i = 0; i < abbrOpt[1].length; i++) { var c = abbrOpt[1].substring(i, i + 1); var option = _findByAbbr(c); _validate(option != null, 'Could not find an option with short name "-$c".'); // In a list of short options, only the first can be a non-flag. If // we get here we've checked that already. _validate(option.isFlag, 'Option "-$c" must be a flag to be in a collapsed "-".'); _setOption(results, option, true); } } return true; } /** * Tries to parse the current argument as a long-form named option, which * may include a value like "--mode=release" or "--mode release". */ bool _parseLongOption(Map results) { var longOpt = _LONG_OPT.firstMatch(_args[_current]); if (longOpt == null) return false; var name = longOpt[1]; var option = _options[name]; if (option != null) { if (option.isFlag) { _validate(longOpt[3] == null, 'Flag option "$name" should not be given a value.'); _setOption(results, option, true); } else if (longOpt[3] != null) { // We have a value like --foo=bar. _setOption(results, option, longOpt[3]); } else { // Option like --foo, so look for the value as the next arg. _readNextArgAsValue(results, option); } } else if (name.startsWith('no-')) { // See if it's a negated flag. name = name.substring('no-'.length); option = _options[name]; _validate(option != null, 'Could not find an option named "$name".'); _validate(option.isFlag, 'Cannot negate non-flag option "$name".'); _validate(option.negatable, 'Cannot negate option "$name".'); _setOption(results, option, false); } else { _validate(option != null, 'Could not find an option named "$name".'); } return true; } /** * Finds the option whose abbreviation is [abbr], or `null` if no option has * that abbreviation. */ _Option _findByAbbr(String abbr) { for (var option in _options.getValues()) { if (option.abbreviation == abbr) return option; } return null; } /** * Get the default value for an option. Useful after parsing to test * if the user specified something other than the default. */ getDefault(String option) { if (!_options.containsKey(option)) { throw new ArgumentError('No option named $option'); } return _options[option].defaultValue; } }
Constructors
new ArgParser() #
Creates a new ArgParser.
ArgParser() : _options = <String, _Option>{}, _optionNames = <String>[];
Methods
void addFlag(String name, [String abbr, String help, bool defaultsTo = false, bool negatable = true, void callback(bool value)]) #
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, isFlag: true, negatable: negatable); }
void addOption(String name, [String abbr, String help, List<String> allowed, Map<String, String> allowedHelp, String defaultsTo, void callback(value), bool allowMultiple = false]) #
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(value), bool allowMultiple: false}) { _addOption(name, abbr, help, allowed, allowedHelp, defaultsTo, callback, isFlag: false, allowMultiple: allowMultiple); }
getDefault(String option) #
Get the default value for an option. Useful after parsing to test if the user specified something other than the default.
getDefault(String option) { if (!_options.containsKey(option)) { throw new ArgumentError('No option named $option'); } return _options[option].defaultValue; }
String getUsage() #
Generates a string displaying usage information for the defined options. This is basically the help text shown on the command line.
String getUsage() { return new _Usage(this).generate(); }
ArgResults parse(List<String> args) #
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) { _args = args; _current = 0; var results = {}; // Initialize flags to their defaults. _options.forEach((name, option) { if (option.allowMultiple) { results[name] = []; } else { results[name] = option.defaultValue; } }); // Parse the args. for (_current = 0; _current < args.length; _current++) { var arg = args[_current]; if (arg == '--') { // Reached the argument terminator, so stop here. _current++; break; } // Try to parse the current argument as an option. Note that the order // here matters. if (_parseSoloOption(results)) continue; if (_parseAbbreviation(results)) continue; if (_parseLongOption(results)) continue; // If we got here, the argument doesn't look like an option, so stop. break; } // Set unspecified multivalued arguments to their default value, // if any, and invoke the callbacks. for (var name in _optionNames) { var option = _options[name]; if (option.allowMultiple && results[name].length == 0 && option.defaultValue != null) { results[name].add(option.defaultValue); } if (option.callback != null) option.callback(results[name]); } // Add in the leftover arguments we didn't parse. return new ArgResults(results, _args.getRange(_current, _args.length - _current)); }