Added mandatory (or required) option (#177)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d09d006..edac237 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
## 2.1.0-dev
+* Add a `mandatory` argument to require the presence of an option.
* Add `aliases` named argument to `addFlag`, `addOption`, and `addMultiOption`,
as well as a public `findByNameOrAlias` method on `ArgParser`. This allows
you to provide aliases for an argument name, which eases the transition from
diff --git a/README.md b/README.md
index 7d5fe5e..dde52cc 100644
--- a/README.md
+++ b/README.md
@@ -74,6 +74,13 @@
If an option isn't provided in the args, its callback is passed the default
value, or `null` if no default value is set.
+If an option is `mandatory` but not provided, the parser throws an
+[`ArgParserException`][ArgParserException].
+
+```dart
+parser.addOption('mode', mandatory: true);
+```
+
## Parsing arguments
Once you have an [ArgParser][] set up with some options and flags, you use it by
diff --git a/example/test_runner.dart b/example/test_runner.dart
index f600915..9a231e8 100644
--- a/example/test_runner.dart
+++ b/example/test_runner.dart
@@ -164,6 +164,7 @@
parser.addOption('dart', help: 'Path to dart executable');
parser.addOption('drt', help: 'Path to content shell executable');
parser.addOption('dartium', help: 'Path to Dartium Chrome executable');
+ parser.addOption('mandatory', help: 'A mandatory option', mandatory: true);
print(parser.usage);
}
diff --git a/lib/src/allow_anything_parser.dart b/lib/src/allow_anything_parser.dart
index becff98..46dfc94 100644
--- a/lib/src/allow_anything_parser.dart
+++ b/lib/src/allow_anything_parser.dart
@@ -52,6 +52,7 @@
void Function(String?)? callback,
bool allowMultiple = false,
bool? splitCommas,
+ bool mandatory = false,
bool hide = false,
List<String> aliases = const []}) {
throw UnsupportedError(
diff --git a/lib/src/arg_parser.dart b/lib/src/arg_parser.dart
index 87f7a02..42af6bf 100644
--- a/lib/src/arg_parser.dart
+++ b/lib/src/arg_parser.dart
@@ -198,11 +198,12 @@
Map<String, String>? allowedHelp,
String? defaultsTo,
void Function(String?)? callback,
+ bool mandatory = false,
bool hide = false,
List<String> aliases = const []}) {
_addOption(name, abbr, help, valueHelp, allowed, allowedHelp, defaultsTo,
callback, OptionType.single,
- hide: hide, aliases: aliases);
+ mandatory: mandatory, hide: hide, aliases: aliases);
}
/// Defines an option that takes multiple values.
@@ -283,6 +284,7 @@
OptionType type,
{bool negatable = false,
bool? splitCommas,
+ bool mandatory = false,
bool hide = false,
List<String> aliases = const []}) {
var allNames = [name, ...aliases];
@@ -299,10 +301,17 @@
}
}
+ // Make sure the option is not mandatory with a default value.
+ if (mandatory && defaultsTo != null) {
+ throw ArgumentError(
+ 'The option $name cannot be mandatory and have a default value.');
+ }
+
var option = newOption(name, abbr, help, valueHelp, allowed, allowedHelp,
defaultsTo, callback, type,
negatable: negatable,
splitCommas: splitCommas,
+ mandatory: mandatory,
hide: hide,
aliases: aliases);
_options[name] = option;
diff --git a/lib/src/option.dart b/lib/src/option.dart
index 7a91a4b..a7c6ab8 100644
--- a/lib/src/option.dart
+++ b/lib/src/option.dart
@@ -18,12 +18,14 @@
OptionType type,
{bool? negatable,
bool? splitCommas,
+ bool mandatory = false,
bool hide = false,
List<String> aliases = const []}) {
return Option._(name, abbr, help, valueHelp, allowed, allowedHelp, defaultsTo,
callback, type,
negatable: negatable,
splitCommas: splitCommas,
+ mandatory: mandatory,
hide: hide,
aliases: aliases);
}
@@ -74,6 +76,9 @@
/// addition to `--option a --option b`.
final bool splitCommas;
+ /// Whether this option must be provided for correct usage.
+ final bool mandatory;
+
/// Whether this option should be hidden from usage documentation.
final bool hide;
@@ -101,6 +106,7 @@
OptionType type,
{this.negatable,
bool? splitCommas,
+ this.mandatory = false,
this.hide = false,
this.aliases = const []})
: allowed = allowed == null ? null : List.unmodifiable(allowed),
diff --git a/lib/src/parser.dart b/lib/src/parser.dart
index 9cfaa60..25497be 100644
--- a/lib/src/parser.dart
+++ b/lib/src/parser.dart
@@ -92,10 +92,19 @@
rest.add(args.removeFirst());
}
- // Invoke the callbacks.
+ // Check if mandatory and invoke existing callbacks.
grammar.options.forEach((name, option) {
+ var parsedOption = results[name];
+
+ // Check if an option was mandatory and exist
+ // if not throw an exception
+ if (option.mandatory && parsedOption == null) {
+ throw ArgParserException('Option $name is mandatory.', [name]);
+ }
+
var callback = option.callback;
- if (callback != null) callback(option.valueOrDefault(results[name]));
+ if (callback == null) return;
+ callback(option.valueOrDefault(parsedOption));
});
// Add in the leftover arguments we didn't parse to the innermost command.
diff --git a/lib/src/usage.dart b/lib/src/usage.dart
index 6e44244..8b788c5 100644
--- a/lib/src/usage.dart
+++ b/lib/src/usage.dart
@@ -85,7 +85,7 @@
void _writeOption(Option option) {
_write(0, _abbreviation(option));
- _write(1, _longOption(option));
+ _write(1, '${_longOption(option)}${_mandatoryOption(option)}');
if (option.help != null) _write(2, option.help!);
@@ -131,6 +131,10 @@
return result;
}
+ String _mandatoryOption(Option option) {
+ return option.mandatory ? ' (mandatory)' : '';
+ }
+
String _allowedTitle(Option option, String allowed) {
var isDefault = option.defaultsTo is List
? option.defaultsTo.contains(allowed)
@@ -149,7 +153,7 @@
abbr = math.max(abbr, _abbreviation(option).length);
// Make room for the option.
- title = math.max(title, _longOption(option).length);
+ title = math.max(title, _longOption(option).length + _mandatoryOption(option).length);
// Make room for the allowed help.
if (option.allowedHelp != null) {
diff --git a/test/parse_test.dart b/test/parse_test.dart
index c90219e..4714d16 100644
--- a/test/parse_test.dart
+++ b/test/parse_test.dart
@@ -509,6 +509,28 @@
var results = parser.parse(['--b=1']);
expect(results['a'], '1');
});
+
+ group('mandatory', () {
+ test('throw if no args', () {
+ var parser = ArgParser();
+ parser.addOption('username', mandatory: true);
+ throwsFormat(parser, []);
+ });
+
+ test('throw if no mandatory args', () {
+ var parser = ArgParser();
+ parser.addOption('test');
+ parser.addOption('username', mandatory: true);
+ throwsFormat(parser, ['--test', 'test']);
+ });
+
+ test('parse successfully', () {
+ var parser = ArgParser();
+ parser.addOption('test', mandatory: true);
+ var results = parser.parse(['--test', 'test']);
+ expect(results['test'], equals('test'));
+ });
+ });
});
group('remaining args', () {
diff --git a/test/usage_test.dart b/test/usage_test.dart
index 0ba33f0..dbcef78 100644
--- a/test/usage_test.dart
+++ b/test/usage_test.dart
@@ -5,6 +5,8 @@
import 'package:args/args.dart';
import 'package:test/test.dart';
+import 'test_utils.dart';
+
void main() {
group('ArgParser.usage', () {
test('negatable flags show "no-" in title', () {
@@ -404,6 +406,22 @@
''');
});
+ test('display "mandatory" after a mandatory option', () {
+ var parser = ArgParser();
+ parser.addOption('test', mandatory: true);
+ validateUsage(parser, '''
+ --test (mandatory)
+ ''');
+ });
+
+ test('throw argument error if option is mandatory with a default value', () {
+ var parser = ArgParser();
+ expect(
+ () => parser.addOption('test', mandatory: true, defaultsTo: 'test'),
+ throwsArgumentError
+ );
+ });
+
group('separators', () {
test("separates options where it's placed", () {
var parser = ArgParser();