diff --git a/CHANGELOG.md b/CHANGELOG.md index a205299..6e78848 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Added source argument when throwing a `ArgParserException`. * Fix inconsistent `FormatException` messages +* Add function to retrieve actual input values for command options by name. * Require Dart 3.3 ## 2.5.0 diff --git a/lib/src/arg_results.dart b/lib/src/arg_results.dart index 72c4410..d311552 100644 --- a/lib/src/arg_results.dart +++ b/lib/src/arg_results.dart @@ -13,11 +13,12 @@ import 'arg_parser.dart'; ArgResults newArgResults( ArgParser parser, Map parsed, + Map actual, String? name, ArgResults? command, List rest, List arguments) { - return ArgResults._(parser, parsed, name, command, rest, arguments); + return ArgResults._(parser, parsed, actual, name, command, rest, arguments); } /// The results of parsing a series of command line arguments using @@ -32,6 +33,9 @@ class ArgResults { /// The option values that were parsed from arguments. final Map _parsed; + /// The actual values that were parsed from arguments. + final Map _actual; + /// The name of the command for which these options are parsed, or `null` if /// these are the top-level results. final String? name; @@ -52,8 +56,8 @@ class ArgResults { /// The original arguments that were parsed. final List arguments; - ArgResults._(this._parser, this._parsed, this.name, this.command, - List rest, List arguments) + ArgResults._(this._parser, this._parsed, this._actual, this.name, + this.command, List rest, List arguments) : rest = UnmodifiableListView(rest), arguments = UnmodifiableListView(arguments); @@ -134,6 +138,18 @@ class ArgResults { return result; } + /// Returns the actual value of the command-line option named [name]. + /// Returns `null` if the option was not provided. + /// + /// [name] must be a valid option name in the parser. + String? actual(String name) { + if (_actual.containsKey(name)) { + return _actual[name]; + } else { + return null; + } + } + /// Returns `true` if the option with [name] was parsed from an actual /// argument. /// diff --git a/lib/src/parser.dart b/lib/src/parser.dart index 660e56d..207a600 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -34,6 +34,9 @@ class Parser { /// The accumulated parsed options. final Map _results = {}; + /// The actual inputs that were parsed. + final Map _actualStores = {}; + Parser(this._commandName, this._grammar, this._args, [this._parent, List? rest]) : _rest = [...?rest]; @@ -45,8 +48,8 @@ class Parser { ArgResults parse() { var arguments = _args.toList(); if (_grammar.allowsAnything) { - return newArgResults( - _grammar, const {}, _commandName, null, arguments, arguments); + return newArgResults(_grammar, const {}, const {}, _commandName, null, + arguments, arguments); } ArgResults? commandResults; @@ -116,8 +119,8 @@ class Parser { // 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); + return newArgResults(_grammar, _results, _actualStores, _commandName, + commandResults, _rest, arguments); } /// Pulls the value for [option] from the second argument in [_args]. @@ -127,7 +130,7 @@ class Parser { // Take the option argument from the next command line arg. _validate(_args.isNotEmpty, 'Missing argument for "$arg".', arg); - _setOption(_results, option, _current, arg); + _setOption(_results, option, _current, _actualStores, arg); _args.removeFirst(); } @@ -158,7 +161,7 @@ class Parser { _args.removeFirst(); if (option.isFlag) { - _setFlag(_results, option, true); + _setFlag(_results, option, true, _actualStores, '-$opt'); } else { _readNextArgAsValue(option, '-$opt'); } @@ -207,7 +210,7 @@ class Parser { // 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'); + _setOption(_results, first, value, _actualStores, '-$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. @@ -246,7 +249,7 @@ class Parser { _validate(option.isFlag, 'Option "-$c" must be a flag to be in a collapsed "-".', '-$c'); - _setFlag(_results, option, true); + _setFlag(_results, option, true, _actualStores, '-$c'); } /// Tries to parse the current argument as a long-form named option, which @@ -279,10 +282,10 @@ class Parser { _validate(value == null, 'Flag option "--$name" should not be given a value.', '--$name'); - _setFlag(_results, option, true); + _setFlag(_results, option, true, _actualStores, '--$name'); } else if (value != null) { // We have a value like --foo=bar. - _setOption(_results, option, value, '--$name'); + _setOption(_results, option, value, _actualStores, '--$name'); } else { // Option like --foo, so look for the value as the next arg. _readNextArgAsValue(option, '--$name'); @@ -304,7 +307,7 @@ class Parser { _validate( option.negatable!, 'Cannot negate option "--$name".', '--$name'); - _setFlag(_results, option, false); + _setFlag(_results, option, false, _actualStores, '--$name'); } else { // Walk up to the parent command if possible. _validate(_parent != null, 'Could not find an option named "--$name".', @@ -327,8 +330,10 @@ class Parser { /// 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) { + void _setOption(Map results, Option option, String value, + Map actualStores, String arg) { assert(!option.isFlag); + actualStores[option.name] = arg; if (!option.isMultiple) { _validateAllowed(option, value, arg); @@ -351,9 +356,11 @@ class Parser { /// Validates and stores [value] as the value for [option], which must be a /// flag. - void _setFlag(Map results, Option option, bool value) { + void _setFlag(Map results, Option option, bool value, + Map actualStores, String source) { assert(option.isFlag); results[option.name] = value; + actualStores[option.name] = source; } /// Validates that [value] is allowed as a value of [option]. diff --git a/test/parse_test.dart b/test/parse_test.dart index 9501b5d..eeadd98 100644 --- a/test/parse_test.dart +++ b/test/parse_test.dart @@ -730,6 +730,52 @@ void main() { }); }); + group('actual()', () { + test('returns value if present', () { + var parser = ArgParser(); + parser.addFlag('verbose'); + parser.addOption('mode'); + parser.addMultiOption('define'); + + var args = parser.parse(['--verbose', '--mode=release', '--define=1']); + expect(args.actual('verbose'), '--verbose'); + expect(args.actual('mode'), '--mode'); + expect(args.actual('define'), '--define'); + }); + + test('returns null if missing', () { + var parser = ArgParser(); + parser.addFlag('a', defaultsTo: true); + parser.addOption('b', defaultsTo: 'c'); + parser.addMultiOption('d', defaultsTo: ['e']); + + var args = parser.parse([]); + expect(args.actual('a'), isNull); + expect(args.actual('b'), isNull); + expect(args.actual('d'), isNull); + }); + + test('can match by alias', () { + var parser = ArgParser()..addFlag('verbose', abbr: 'v'); + var results = parser.parse(['-v']); + expect(results.actual('verbose'), '-v'); + }); + + test('can be negated by alias', () { + var parser = ArgParser() + ..addFlag('a', aliases: ['b'], defaultsTo: true, negatable: true); + var results = parser.parse(['--no-b']); + expect(results.actual('a'), '--no-b'); + }); + + // abbr test + test('can match by abbreviation', () { + var parser = ArgParser()..addFlag('a', abbr: 'b'); + var results = parser.parse(['-b']); + expect(results.actual('a'), '-b'); + }); + }); + group('remaining args', () { test('stops parsing args when a non-option-like arg is encountered', () { var parser = ArgParser();