| // 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. | |
| import 'dart:collection'; | |
| import 'arg_parser.dart'; | |
| import 'arg_parser_exception.dart'; | |
| import 'arg_results.dart'; | |
| import 'option.dart'; | |
| /// The actual argument parsing class. | |
| /// | |
| /// Unlike [ArgParser] which is really more an "arg grammar", this is the class | |
| /// that does the parsing and holds the mutable state required during a parse. | |
| class Parser { | |
| /// If parser is parsing a command's options, this will be the name of the | |
| /// command. For top-level results, this returns `null`. | |
| final String? _commandName; | |
| /// The parser for the supercommand of this command parser, or `null` if this | |
| /// is the top-level parser. | |
| final Parser? _parent; | |
| /// The grammar being parsed. | |
| final ArgParser _grammar; | |
| /// The arguments being parsed. | |
| final Queue<String> _args; | |
| /// The remaining non-option, non-command arguments. | |
| final List<String> _rest; | |
| /// The accumulated parsed options. | |
| final Map<String, dynamic> _results = <String, dynamic>{}; | |
| Parser(this._commandName, this._grammar, this._args, | |
| [this._parent, List<String>? rest]) | |
| : _rest = [...?rest]; | |
| /// The current argument being parsed. | |
| String get _current => _args.first; | |
| /// Parses the arguments. This can only be called once. | |
| ArgResults parse() { | |
| var arguments = _args.toList(); | |
| if (_grammar.allowsAnything) { | |
| return newArgResults( | |
| _grammar, const {}, _commandName, null, arguments, arguments); | |
| } | |
| ({String name, ArgParser parser})? command; | |
| // Parse the args. | |
| while (_args.isNotEmpty) { | |
| if (_current == '--') { | |
| // Reached the argument terminator, so stop here. | |
| _args.removeFirst(); | |
| break; | |
| } | |
| // Try to parse the current argument as a command. This happens before | |
| // options so that commands can have option-like names. | |
| // | |
| // Otherwise, if there is a default command then select it before parsing | |
| // any arguments. We make exception for situations when help flag is | |
| // passed because we want `program command -h` to display help for | |
| // `command` rather than display help for the default subcommand of the | |
| // `command`. | |
| if (_grammar.commands[_current] case final parser?) { | |
| command = (name: _args.removeFirst(), parser: parser); | |
| break; | |
| } else if (_grammar.defaultCommand case final defaultCommand? | |
| when !(_current == '-h' || _current == '--help')) { | |
| command = | |
| (name: defaultCommand, parser: _grammar.commands[defaultCommand]!); | |
| break; | |
| } | |
| // Try to parse the current argument as an option. Note that the order | |
| // here matters. | |
| if (_parseSoloOption()) continue; | |
| if (_parseAbbreviation(this)) continue; | |
| if (_parseLongOption()) continue; | |
| // This argument is neither option nor command, so stop parsing unless | |
| // the [allowTrailingOptions] option is set. | |
| if (!_grammar.allowTrailingOptions) break; | |
| _rest.add(_args.removeFirst()); | |
| } | |
| // If there is a default command and we did not select any other commands | |
| // and we don't have any trailing arguments then select the default | |
| // command unless user requested help. | |
| if (command == null && _rest.isEmpty && !_results.containsKey('help')) { | |
| if (_grammar.defaultCommand case final defaultCommand?) { | |
| command = | |
| (name: defaultCommand, parser: _grammar.commands[defaultCommand]!); | |
| } | |
| } | |
| ArgResults? commandResults; | |
| if (command != null) { | |
| _validate(_rest.isEmpty, 'Cannot specify arguments before a command.', | |
| command.name); | |
| var commandParser = | |
| Parser(command.name, command.parser, _args, this, _rest); | |
| try { | |
| commandResults = commandParser.parse(); | |
| } on ArgParserException catch (error) { | |
| throw ArgParserException( | |
| error.message, | |
| [command.name, ...error.commands], | |
| error.argumentName, | |
| error.source, | |
| error.offset); | |
| } | |
| // All remaining arguments were passed to command so clear them here. | |
| _rest.clear(); | |
| } | |
| // Check if mandatory and invoke existing callbacks. | |
| final ignoreMandatoryOptions = | |
| _results.containsKey('help') || command?.name == 'help'; | |
| _grammar.options.forEach((name, option) { | |
| var parsedOption = _results[name]; | |
| // Check if an option is mandatory and was passed; if not, throw an | |
| // exception. | |
| if (!ignoreMandatoryOptions && option.mandatory && parsedOption == null) { | |
| throw ArgParserException('Option $name is mandatory.', null, name); | |
| } | |
| var callback = option.callback; | |
| if (callback == null) return; | |
| // ignore: avoid_dynamic_calls | |
| callback(option.valueOrDefault(parsedOption)); | |
| }); | |
| // Add in the leftover arguments we didn't parse to the innermost command. | |
| _rest.addAll(_args); | |
| _args.clear(); | |
| return newArgResults( | |
| _grammar, _results, _commandName, commandResults, _rest, arguments); | |
| } | |
| /// Pulls the value for [option] from the second argument in [_args]. | |
| /// | |
| /// Validates that there is a valid value there. | |
| void _readNextArgAsValue(Option option, String arg) { | |
| // Take the option argument from the next command line arg. | |
| _validate(_args.isNotEmpty, 'Missing argument for "$arg".', arg); | |
| _setOption(_results, option, _current, arg); | |
| _args.removeFirst(); | |
| } | |
| /// 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() { | |
| // Hand coded regexp: r'^-([a-zA-Z0-9])$' | |
| // Length must be two, hyphen followed by any letter/digit. | |
| if (_current.length != 2) return false; | |
| if (!_current.startsWith('-')) return false; | |
| var opt = _current[1]; | |
| if (!_isLetterOrDigit(opt.codeUnitAt(0))) return false; | |
| return _handleSoloOption(opt); | |
| } | |
| bool _handleSoloOption(String opt) { | |
| var option = _grammar.findByAbbreviation(opt); | |
| if (option == null) { | |
| // Walk up to the parent command if possible. | |
| _validate(_parent != null, 'Could not find an option or flag "-$opt".', | |
| '-$opt'); | |
| return _parent!._handleSoloOption(opt); | |
| } | |
| _args.removeFirst(); | |
| if (option.isFlag) { | |
| _setFlag(_results, option, true); | |
| } else { | |
| _readNextArgAsValue(option, '-$opt'); | |
| } | |
| 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(Parser innermostCommand) { | |
| // Hand coded regexp: r'^-([a-zA-Z0-9]+)(.*)$' | |
| // Hyphen then at least one letter/digit then zero or more | |
| // anything-but-newlines. | |
| if (_current.length < 2) return false; | |
| if (!_current.startsWith('-')) return false; | |
| // Find where we go from letters/digits to rest. | |
| var index = 1; | |
| while (index < _current.length && | |
| _isLetterOrDigit(_current.codeUnitAt(index))) { | |
| ++index; | |
| } | |
| // Must be at least one letter/digit. | |
| if (index == 1) return false; | |
| // If the first character is the abbreviation for a non-flag option, then | |
| // the rest is the value. | |
| var lettersAndDigits = _current.substring(1, index); | |
| var rest = _current.substring(index); | |
| if (rest.contains('\n') || rest.contains('\r')) return false; | |
| return _handleAbbreviation(lettersAndDigits, rest, innermostCommand); | |
| } | |
| bool _handleAbbreviation( | |
| String lettersAndDigits, String rest, Parser innermostCommand) { | |
| var c = lettersAndDigits.substring(0, 1); | |
| var first = _grammar.findByAbbreviation(c); | |
| if (first == null) { | |
| // Walk up to the parent command if possible. | |
| _validate(_parent != null, | |
| 'Could not find an option with short name "-$c".', '-$c'); | |
| return _parent! | |
| ._handleAbbreviation(lettersAndDigits, rest, innermostCommand); | |
| } else if (!first.isFlag) { | |
| // The first character is a non-flag option, so the rest must be the | |
| // value. | |
| var value = '${lettersAndDigits.substring(1)}$rest'; | |
| _setOption(_results, first, value, '-$c'); | |
| } 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( | |
| rest == '', | |
| 'Option "-$c" is a flag and cannot handle value ' | |
| '"${lettersAndDigits.substring(1)}$rest".', | |
| '-$c'); | |
| // Not an option, so all characters should be flags. | |
| // We use "innermostCommand" here so that if a parent command parses the | |
| // *first* letter, subcommands can still be found to parse the other | |
| // letters. | |
| for (var i = 0; i < lettersAndDigits.length; i++) { | |
| var c = lettersAndDigits.substring(i, i + 1); | |
| innermostCommand._parseShortFlag(c); | |
| } | |
| } | |
| _args.removeFirst(); | |
| return true; | |
| } | |
| void _parseShortFlag(String c) { | |
| var option = _grammar.findByAbbreviation(c); | |
| if (option == null) { | |
| // Walk up to the parent command if possible. | |
| _validate(_parent != null, | |
| 'Could not find an option with short name "-$c".', '-$c'); | |
| _parent!._parseShortFlag(c); | |
| return; | |
| } | |
| // 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 "-".', '-$c'); | |
| _setFlag(_results, option, 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() { | |
| // Hand coded regexp: r'^--([a-zA-Z\-_0-9]+)(=(.*))?$' | |
| // Two hyphens then at least one letter/digit/hyphen, optionally an equal | |
| // sign followed by zero or more anything-but-newlines. | |
| if (!_current.startsWith('--')) return false; | |
| var index = _current.indexOf('='); | |
| var name = | |
| index == -1 ? _current.substring(2) : _current.substring(2, index); | |
| for (var i = 0; i != name.length; ++i) { | |
| if (!_isLetterDigitHyphenOrUnderscore(name.codeUnitAt(i))) return false; | |
| } | |
| var value = index == -1 ? null : _current.substring(index + 1); | |
| if (value != null && (value.contains('\n') || value.contains('\r'))) { | |
| return false; | |
| } | |
| return _handleLongOption(name, value); | |
| } | |
| bool _handleLongOption(String name, String? value) { | |
| var option = _grammar.findByNameOrAlias(name); | |
| if (option != null) { | |
| _args.removeFirst(); | |
| if (option.isFlag) { | |
| _validate(value == null, | |
| 'Flag option "--$name" should not be given a value.', '--$name'); | |
| _setFlag(_results, option, true); | |
| } else if (value != null) { | |
| // We have a value like --foo=bar. | |
| _setOption(_results, option, value, '--$name'); | |
| } else { | |
| // Option like --foo, so look for the value as the next arg. | |
| _readNextArgAsValue(option, '--$name'); | |
| } | |
| } else if (name.startsWith('no-')) { | |
| // See if it's a negated flag. | |
| var positiveName = name.substring('no-'.length); | |
| option = _grammar.findByNameOrAlias(positiveName); | |
| if (option == null) { | |
| // Walk up to the parent command if possible. | |
| _validate(_parent != null, 'Could not find an option named "--$name".', | |
| '--$name'); | |
| return _parent!._handleLongOption(name, value); | |
| } | |
| _args.removeFirst(); | |
| _validate( | |
| option.isFlag, 'Cannot negate non-flag option "--$name".', '--$name'); | |
| _validate( | |
| option.negatable!, 'Cannot negate option "--$name".', '--$name'); | |
| _setFlag(_results, option, false); | |
| } else { | |
| // Walk up to the parent command if possible. | |
| _validate(_parent != null, 'Could not find an option named "--$name".', | |
| '--$name'); | |
| return _parent!._handleLongOption(name, value); | |
| } | |
| return true; | |
| } | |
| /// Called during parsing to validate the arguments. | |
| /// | |
| /// Throws an [ArgParserException] if [condition] is `false`. | |
| void _validate(bool condition, String message, | |
| [String? args, List<String>? source, int? offset]) { | |
| if (!condition) { | |
| throw ArgParserException(message, null, args, source, offset); | |
| } | |
| } | |
| /// Validates and stores [value] as the value for [option], which must not be | |
| /// a flag. | |
| void _setOption(Map results, Option option, String value, String arg) { | |
| assert(!option.isFlag); | |
| if (!option.isMultiple) { | |
| _validateAllowed(option, value, arg); | |
| results[option.name] = value; | |
| return; | |
| } | |
| var list = results.putIfAbsent(option.name, () => <String>[]) as List; | |
| if (option.splitCommas) { | |
| for (var element in value.split(',')) { | |
| _validateAllowed(option, element, arg); | |
| list.add(element); | |
| } | |
| } else { | |
| _validateAllowed(option, value, arg); | |
| list.add(value); | |
| } | |
| } | |
| /// Validates and stores [value] as the value for [option], which must be a | |
| /// flag. | |
| void _setFlag(Map results, Option option, bool value) { | |
| assert(option.isFlag); | |
| results[option.name] = value; | |
| } | |
| /// Validates that [value] is allowed as a value of [option]. | |
| void _validateAllowed(Option option, String value, String arg) { | |
| if (option.allowed == null) return; | |
| _validate(option.allowed!.contains(value), | |
| '"$value" is not an allowed value for option "$arg".', arg); | |
| } | |
| } | |
| bool _isLetterOrDigit(int codeUnit) => | |
| // Uppercase letters. | |
| (codeUnit >= 65 && codeUnit <= 90) || | |
| // Lowercase letters. | |
| (codeUnit >= 97 && codeUnit <= 122) || | |
| // Digits. | |
| (codeUnit >= 48 && codeUnit <= 57); | |
| bool _isLetterDigitHyphenOrUnderscore(int codeUnit) => | |
| _isLetterOrDigit(codeUnit) || | |
| // Hyphen. | |
| codeUnit == 45 || | |
| // Underscore. | |
| codeUnit == 95; |