Dart API ReferenceargsArgParser

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));
}