Add argument name when throwing a `ArgParserException`. (#283)
Add an `argumentName` field which tracks the argument that was being
parse when the exception is thrown.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e572ec..a205299 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,7 @@
-## 2.5.1-wip
+## 2.6.0-wip
+* Added source argument when throwing a `ArgParserException`.
+* Fix inconsistent `FormatException` messages
* Require Dart 3.3
## 2.5.0
diff --git a/lib/src/arg_parser_exception.dart b/lib/src/arg_parser_exception.dart
index d727d70..fbee82b 100644
--- a/lib/src/arg_parser_exception.dart
+++ b/lib/src/arg_parser_exception.dart
@@ -9,6 +9,14 @@
/// This will be empty if the error was on the root parser.
final List<String> commands;
- ArgParserException(super.message, [Iterable<String>? commands])
+ /// The name of the argument that was being parsed when the error was
+ /// discovered.
+ final String? argumentName;
+
+ ArgParserException(super.message,
+ [Iterable<String>? commands,
+ this.argumentName,
+ super.source,
+ super.offset])
: commands = commands == null ? const [] : List.unmodifiable(commands);
}
diff --git a/lib/src/arg_results.dart b/lib/src/arg_results.dart
index 9fa87cd..72c4410 100644
--- a/lib/src/arg_results.dart
+++ b/lib/src/arg_results.dart
@@ -66,7 +66,7 @@
/// > flags, [option] for options, and [multiOption] for multi-options.
dynamic operator [](String name) {
if (!_parser.options.containsKey(name)) {
- throw ArgumentError('Could not find an option named "$name".');
+ throw ArgumentError('Could not find an option named "--$name".');
}
final option = _parser.options[name]!;
@@ -83,7 +83,7 @@
bool flag(String name) {
var option = _parser.options[name];
if (option == null) {
- throw ArgumentError('Could not find an option named "$name".');
+ throw ArgumentError('Could not find an option named "--$name".');
}
if (!option.isFlag) {
throw ArgumentError('"$name" is not a flag.');
@@ -97,7 +97,7 @@
String? option(String name) {
var option = _parser.options[name];
if (option == null) {
- throw ArgumentError('Could not find an option named "$name".');
+ throw ArgumentError('Could not find an option named "--$name".');
}
if (!option.isSingle) {
throw ArgumentError('"$name" is a multi-option.');
@@ -111,7 +111,7 @@
List<String> multiOption(String name) {
var option = _parser.options[name];
if (option == null) {
- throw ArgumentError('Could not find an option named "$name".');
+ throw ArgumentError('Could not find an option named "--$name".');
}
if (!option.isMultiple) {
throw ArgumentError('"$name" is not a multi-option.');
@@ -143,7 +143,7 @@
/// [name] must be a valid option name in the parser.
bool wasParsed(String name) {
if (!_parser.options.containsKey(name)) {
- throw ArgumentError('Could not find an option named "$name".');
+ throw ArgumentError('Could not find an option named "--$name".');
}
return _parsed.containsKey(name);
diff --git a/lib/src/parser.dart b/lib/src/parser.dart
index 3c5dfed..660e56d 100644
--- a/lib/src/parser.dart
+++ b/lib/src/parser.dart
@@ -63,7 +63,8 @@
// options so that commands can have option-like names.
var command = _grammar.commands[_current];
if (command != null) {
- _validate(_rest.isEmpty, 'Cannot specify arguments before a command.');
+ _validate(_rest.isEmpty, 'Cannot specify arguments before a command.',
+ _current);
var commandName = _args.removeFirst();
var commandParser = Parser(commandName, command, _args, this, _rest);
@@ -71,7 +72,11 @@
commandResults = commandParser.parse();
} on ArgParserException catch (error) {
throw ArgParserException(
- error.message, [commandName, ...error.commands]);
+ error.message,
+ [commandName, ...error.commands],
+ error.argumentName,
+ error.source,
+ error.offset);
}
// All remaining arguments were passed to command so clear them here.
@@ -101,7 +106,7 @@
// Check if an option is mandatory and was passed; if not, throw an
// exception.
if (option.mandatory && parsedOption == null) {
- throw ArgParserException('Option $name is mandatory.');
+ throw ArgParserException('Option $name is mandatory.', null, name);
}
// ignore: avoid_dynamic_calls
@@ -118,11 +123,11 @@
/// Pulls the value for [option] from the second argument in [_args].
///
/// Validates that there is a valid value there.
- void _readNextArgAsValue(Option option) {
+ void _readNextArgAsValue(Option option, String arg) {
// Take the option argument from the next command line arg.
- _validate(_args.isNotEmpty, 'Missing argument for "${option.name}".');
+ _validate(_args.isNotEmpty, 'Missing argument for "$arg".', arg);
- _setOption(_results, option, _current);
+ _setOption(_results, option, _current, arg);
_args.removeFirst();
}
@@ -145,7 +150,8 @@
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".');
+ _validate(_parent != null, 'Could not find an option or flag "-$opt".',
+ '-$opt');
return _parent!._handleSoloOption(opt);
}
@@ -154,7 +160,7 @@
if (option.isFlag) {
_setFlag(_results, option, true);
} else {
- _readNextArgAsValue(option);
+ _readNextArgAsValue(option, '-$opt');
}
return true;
@@ -193,22 +199,23 @@
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".');
+ _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);
+ _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".');
+ '"${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
@@ -228,16 +235,16 @@
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".');
+ _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 "-".');
+ _validate(option.isFlag,
+ 'Option "-$c" must be a flag to be in a collapsed "-".', '-$c');
_setFlag(_results, option, true);
}
@@ -269,16 +276,16 @@
if (option != null) {
_args.removeFirst();
if (option.isFlag) {
- _validate(
- value == null, 'Flag option "$name" should not be given a value.');
+ _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);
+ _setOption(_results, option, value, '--$name');
} else {
// Option like --foo, so look for the value as the next arg.
- _readNextArgAsValue(option);
+ _readNextArgAsValue(option, '--$name');
}
} else if (name.startsWith('no-')) {
// See if it's a negated flag.
@@ -286,18 +293,22 @@
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".');
+ _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".');
- _validate(option.negatable!, 'Cannot negate option "$name".');
+ _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".');
+ _validate(_parent != null, 'Could not find an option named "--$name".',
+ '--$name');
return _parent!._handleLongOption(name, value);
}
@@ -307,17 +318,20 @@
/// Called during parsing to validate the arguments.
///
/// Throws an [ArgParserException] if [condition] is `false`.
- void _validate(bool condition, String message) {
- if (!condition) throw ArgParserException(message);
+ 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) {
+ void _setOption(Map results, Option option, String value, String arg) {
assert(!option.isFlag);
if (!option.isMultiple) {
- _validateAllowed(option, value);
+ _validateAllowed(option, value, arg);
results[option.name] = value;
return;
}
@@ -326,11 +340,11 @@
if (option.splitCommas) {
for (var element in value.split(',')) {
- _validateAllowed(option, element);
+ _validateAllowed(option, element, arg);
list.add(element);
}
} else {
- _validateAllowed(option, value);
+ _validateAllowed(option, value, arg);
list.add(value);
}
}
@@ -343,11 +357,11 @@
}
/// Validates that [value] is allowed as a value of [option].
- void _validateAllowed(Option option, String value) {
+ 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 "${option.name}".');
+ '"$value" is not an allowed value for option "$arg".', arg);
}
}
diff --git a/pubspec.yaml b/pubspec.yaml
index 2b70992..8e59181 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: args
-version: 2.5.1-wip
+version: 2.6.0-wip
description: >-
Library for defining parsers for parsing raw command-line arguments into a set
of options and values using GNU and POSIX style options.
diff --git a/test/command_runner_test.dart b/test/command_runner_test.dart
index cc80c6b..b9fde8a 100644
--- a/test/command_runner_test.dart
+++ b/test/command_runner_test.dart
@@ -583,7 +583,7 @@
expect(
runner.run(['--asdf']),
throwsUsageException(
- 'Could not find an option named "asdf".', _defaultUsage));
+ 'Could not find an option named "--asdf".', _defaultUsage));
});
test('for a command throws the command usage', () {
@@ -591,7 +591,7 @@
runner.addCommand(command);
expect(runner.run(['foo', '--asdf']),
- throwsUsageException('Could not find an option named "asdf".', '''
+ throwsUsageException('Could not find an option named "--asdf".', '''
Usage: test foo [arguments]
-h, --help Print this usage information.
@@ -616,7 +616,7 @@
test('includes the footer in usage errors', () {
expect(
runner.run(['--bad']),
- throwsUsageException('Could not find an option named "bad".',
+ throwsUsageException('Could not find an option named "--bad".',
'$_defaultUsage\nAlso, footer!'));
});
});
@@ -652,7 +652,7 @@
test('includes the footer in usage errors', () {
expect(runner.run(['--bad']),
- throwsUsageException('Could not find an option named "bad".', '''
+ throwsUsageException('Could not find an option named "--bad".', '''
Usage: test <command> [arguments]
Global options:
@@ -679,7 +679,7 @@
expect(
runner.run(['--bad']),
throwsUsageException(
- 'Could not find an option named "bad".', _defaultUsage));
+ 'Could not find an option named "--bad".', _defaultUsage));
});
test("a top-level command doesn't exist", () {
diff --git a/test/parse_test.dart b/test/parse_test.dart
index b2dda44..9501b5d 100644
--- a/test/parse_test.dart
+++ b/test/parse_test.dart
@@ -766,5 +766,54 @@
expect(results.rest, equals(['stop', '--', 'arg']));
});
});
+
+ group('ArgParser Exception Tests', () {
+ test('throws exception for unknown option', () {
+ var parser = ArgParser();
+ throwsArgParserException(parser, ['--verbose'],
+ 'Could not find an option named "--verbose".', [], '--verbose');
+ throwsArgParserException(
+ parser, ['-v'], 'Could not find an option or flag "-v".', [], '-v');
+ });
+
+ test('throws exception for flag with value', () {
+ var parser = ArgParser();
+ parser.addFlag('flag', abbr: 'f');
+ throwsArgParserException(parser, ['--flag=1'],
+ 'Flag option "--flag" should not be given a value.', [], '--flag');
+ throwsArgParserException(parser, ['-f=1'],
+ 'Option "-f" is a flag and cannot handle value "=1".', [], '-f');
+ });
+
+ test('throws exception after parsing multiple options', () {
+ var parser = ArgParser();
+ parser.addOption('first');
+ parser.addOption('second');
+ throwsArgParserException(
+ parser,
+ ['--first', '1', '--second', '2', '--verbose', '3'],
+ 'Could not find an option named "--verbose".',
+ [],
+ '--verbose');
+ });
+
+ test('throws exception for option with invalid value', () {
+ var parser = ArgParser();
+ parser.addOption('first', allowed: ['a', 'b']);
+ throwsArgParserException(parser, ['--first', 'c'],
+ '"c" is not an allowed value for option "--first".', [], '--first');
+ });
+
+ test('throws exception after parsing command', () {
+ var parser = ArgParser();
+ parser.addCommand('command', ArgParser());
+ throwsArgParserException(
+ parser,
+ ['command', '--verbose'],
+ 'Could not find an option named "--verbose".',
+ ['command'],
+ '--verbose');
+ });
+ });
});
}
diff --git a/test/test_utils.dart b/test/test_utils.dart
index f19da6d..f7d8b8a 100644
--- a/test/test_utils.dart
+++ b/test/test_utils.dart
@@ -343,8 +343,23 @@
expect(function, throwsArgumentError, reason: reason);
}
-void throwsFormat(ArgParser parser, List<String> args) {
- expect(() => parser.parse(args), throwsFormatException);
+void throwsFormat(ArgParser parser, List<String> args, {String? reason}) {
+ expect(() => parser.parse(args), throwsA(isA<FormatException>()),
+ reason: reason);
+}
+
+void throwsArgParserException(ArgParser parser, List<String> args,
+ String message, List<String> commands, String arg) {
+ try {
+ parser.parse(args);
+ fail('Expected an ArgParserException');
+ } on ArgParserException catch (e) {
+ expect(e.message, message);
+ expect(e.commands, commands);
+ expect(e.argumentName, arg);
+ } catch (e) {
+ fail('Expected an ArgParserException, but got $e');
+ }
}
Matcher throwsUsageException(Object? message, Object? usage) =>
diff --git a/test/trailing_options_test.dart b/test/trailing_options_test.dart
index 505b6b1..db502d1 100644
--- a/test/trailing_options_test.dart
+++ b/test/trailing_options_test.dart
@@ -5,6 +5,8 @@
import 'package:args/args.dart';
import 'package:test/test.dart';
+import 'test_utils.dart';
+
void main() {
test('allowTrailingOptions defaults to true', () {
var parser = ArgParser();
@@ -17,9 +19,8 @@
parser = ArgParser(allowTrailingOptions: true);
});
- void expectThrows(List<String> args) {
- expect(() => parser.parse(args), throwsFormatException,
- reason: 'with allowTrailingOptions: true');
+ void expectThrows(List<String> args, String arg) {
+ throwsFormat(parser, args, reason: 'with allowTrailingOptions: true');
}
test('collects non-options in rest', () {
@@ -56,7 +57,7 @@
test('throws on a trailing option missing its value', () {
parser.addOption('opt');
- expectThrows(['arg', '--opt']);
+ expectThrows(['arg', '--opt'], '--opt');
});
test('parses a trailing option', () {
@@ -67,16 +68,16 @@
});
test('throws on a trailing unknown flag', () {
- expectThrows(['arg', '--xflag']);
+ expectThrows(['arg', '--xflag'], '--xflag');
});
test('throws on a trailing unknown option and value', () {
- expectThrows(['arg', '--xopt', 'v']);
+ expectThrows(['arg', '--xopt', 'v'], '--xopt');
});
test('throws on a command', () {
parser.addCommand('com');
- expectThrows(['arg', 'com']);
+ expectThrows(['arg', 'com'], 'com');
});
});