Add a separate addMultiOption() method
This allows us to be more specific in our types and to keep flags that
are specific to multi-options in the place where they make sense.
Closes #90
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7cc69c0..20eba1f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,12 @@
`Option.abbr` and `Option.defaultsTo`. This makes all of `Option`'s fields
match the corresponding parameters to `ArgParser.addOption()`.
+* Deprecated the `allowMultiple` and `splitCommas` arguments to
+ `ArgParser.addOption()` in favor of a separate `ArgParser.addMultiOption()`
+ method. This allows us to provide more accurate type information, and to avoid
+ adding flags that only make sense for multi-options in places where they might
+ be usable for single-value options.
+
## 1.3.0
* Type `Command.run()`'s return value as `FutureOr<T>`.
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 092773b..32747ee 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,5 +1,11 @@
analyzer:
- strong-mode: true
+ strong-mode: true
+
+ errors:
+ # TODO(nweiz): Remove this ignore when sdk#30084 is fixed or when args no
+ # longer refers to its own deprecated members.
+ deprecated_member_use: ignore
+
linter:
rules:
# Errors
diff --git a/lib/src/allow_anything_parser.dart b/lib/src/allow_anything_parser.dart
index 58e4812..248a39a 100644
--- a/lib/src/allow_anything_parser.dart
+++ b/lib/src/allow_anything_parser.dart
@@ -45,6 +45,20 @@
"ArgParser.allowAnything().addOption() isn't supported.");
}
+ void addMultiOption(String name,
+ {String abbr,
+ String help,
+ String valueHelp,
+ Iterable<String> allowed,
+ Map<String, String> allowedHelp,
+ Iterable<String> defaultsTo,
+ void callback(List<String> values),
+ bool splitCommas: true,
+ bool hide: false}) {
+ throw new UnsupportedError(
+ "ArgParser.allowAnything().addMultiOption() isn't supported.");
+ }
+
void addSeparator(String text) {
throw new UnsupportedError(
"ArgParser.allowAnything().addSeparator() isn't supported.");
diff --git a/lib/src/arg_parser.dart b/lib/src/arg_parser.dart
index c115197..bcdb8df 100644
--- a/lib/src/arg_parser.dart
+++ b/lib/src/arg_parser.dart
@@ -156,14 +156,8 @@
/// that are often surprising, and its use is discouraged in favor of reading
/// values from the [ArgResult].
///
- /// If [allowMultiple] is `true`, the user may pass this option multiple times
- /// and its value will be a `List<String>` rather than a `String`. The default
- /// value will be `[]` rather than `null`, or `[defaultsTo]` if [defaultsTo]
- /// is passed.
- ///
- /// If [splitCommas] is `true`, multiple values may be passed by writing
- /// `--option a,b` in addition to `--option a --option b`. It defaults to
- /// `true` if [allowMultiple] is `true` and `false` otherwise.
+ /// The [allowMultiple] and [splitCommas] options are deprecated; the
+ /// [addMultiOption] method should be used instead.
///
/// If [hide] is `true`, this option won't be included in [usage].
///
@@ -180,17 +174,89 @@
Map<String, String> allowedHelp,
String defaultsTo,
Function callback,
- bool allowMultiple: false,
- bool splitCommas,
+ @Deprecated("Use addMultiOption() instead.") bool allowMultiple: false,
+ @Deprecated("Use addMultiOption() instead.") bool splitCommas,
bool hide: false}) {
if (!allowMultiple && splitCommas != null) {
throw new ArgumentError(
'splitCommas may not be set if allowMultiple is false.');
}
- _addOption(name, abbr, help, valueHelp, allowed, allowedHelp, defaultsTo,
- callback, allowMultiple ? OptionType.multiple : OptionType.single,
- splitCommas: splitCommas, hide: hide);
+ _addOption(
+ name,
+ abbr,
+ help,
+ valueHelp,
+ allowed,
+ allowedHelp,
+ allowMultiple
+ ? (defaultsTo == null ? <String>[] : [defaultsTo])
+ : defaultsTo,
+ callback,
+ allowMultiple ? OptionType.multiple : OptionType.single,
+ splitCommas: splitCommas,
+ hide: hide);
+ }
+
+ /// Defines an option that takes multiple values.
+ ///
+ /// The [abbr] argument is a single-character string that can be used as a
+ /// shorthand for this option. For example, `abbr: "a"` will allow the user to
+ /// pass `-a value` or `-avalue`.
+ ///
+ /// The [help] argument is used by [usage] to describe this option.
+ ///
+ /// The [valueHelp] argument is used by [usage] as a name for the value this
+ /// argument takes. For example, `valueHelp: "FOO"` will include
+ /// `--option=<FOO>` rather than just `--option` in the usage string.
+ ///
+ /// The [allowed] argument is a list of valid values for this argument. If
+ /// it's non-`null` and the user passes a value that's not included in the
+ /// list, [parse] will throw a [FormatException]. The allowed values will also
+ /// be included in [usage].
+ ///
+ /// The [allowedHelp] argument is a map from values in [allowed] to
+ /// documentation for those values that will be included in [usage].
+ ///
+ /// The [defaultsTo] argument indicates the values this option will have if
+ /// the user doesn't explicitly pass it in (or `[]` by default).
+ ///
+ /// The [callback] argument is invoked with the option's value when the option
+ /// is parsed. Note that this makes argument parsing order-dependent in ways
+ /// that are often surprising, and its use is discouraged in favor of reading
+ /// values from the [ArgResult].
+ ///
+ /// If [splitCommas] is `true` (the default), multiple options may be passed
+ /// by writing `--option a,b` in addition to `--option a --option b`.
+ ///
+ /// If [hide] is `true`, this option won't be included in [usage].
+ ///
+ /// Throws an [ArgumentError] if:
+ ///
+ /// * There is already an option with name [name].
+ /// * There is already an option using abbreviation [abbr].
+ void addMultiOption(String name,
+ {String abbr,
+ String help,
+ String valueHelp,
+ Iterable<String> allowed,
+ Map<String, String> allowedHelp,
+ Iterable<String> defaultsTo,
+ void callback(List<String> values),
+ bool splitCommas: true,
+ bool hide: false}) {
+ _addOption(
+ name,
+ abbr,
+ help,
+ valueHelp,
+ allowed,
+ allowedHelp,
+ defaultsTo?.toList() ?? <String>[],
+ callback == null ? null : (value) => callback(value as List<String>),
+ OptionType.multiple,
+ splitCommas: splitCommas,
+ hide: hide);
}
void _addOption(
diff --git a/lib/src/option.dart b/lib/src/option.dart
index 0902fe3..727ea52 100644
--- a/lib/src/option.dart
+++ b/lib/src/option.dart
@@ -141,10 +141,8 @@
/// list containing [defaultsTo] if set.
dynamic getOrDefault(value) {
if (value != null) return value;
-
- if (!isMultiple) return defaultsTo;
- if (defaultsTo != null) return [defaultsTo];
- return [];
+ if (isMultiple) return defaultsTo ?? <String>[];
+ return defaultsTo;
}
static final _invalidChars = new RegExp(r'''[ \t\r\n"'\\/]''');
diff --git a/pubspec.yaml b/pubspec.yaml
index 4985ea2..c476bc3 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: args
-version: 1.4.0-dev
+version: 1.4.0
author: "Dart Team <misc@dartlang.org>"
homepage: https://github.com/dart-lang/args
description: >
diff --git a/test/args_test.dart b/test/args_test.dart
index ff968ad..8741ad8 100644
--- a/test/args_test.dart
+++ b/test/args_test.dart
@@ -274,8 +274,11 @@
parser.addFlag('flag-def', defaultsTo: true);
parser.addOption('single-no');
parser.addOption('single-def', defaultsTo: 'def');
- parser.addOption('multi-no', allowMultiple: true);
- parser.addOption('multi-def', allowMultiple: true, defaultsTo: 'def');
+ parser.addOption('allow-multi-no', allowMultiple: true);
+ parser.addOption('allow-multi-def',
+ allowMultiple: true, defaultsTo: 'def');
+ parser.addMultiOption('multi-no');
+ parser.addMultiOption('multi-def', defaultsTo: ['def']);
expect(parser.options['flag-no'].getOrDefault(null), equals(null));
expect(parser.options['flag-no'].getOrDefault(false), equals(false));
@@ -285,6 +288,13 @@
expect(parser.options['single-no'].getOrDefault('v'), equals('v'));
expect(parser.options['single-def'].getOrDefault(null), equals('def'));
expect(parser.options['single-def'].getOrDefault('v'), equals('v'));
+ expect(parser.options['allow-multi-no'].getOrDefault(null), equals([]));
+ expect(
+ parser.options['allow-multi-no'].getOrDefault(['v']), equals(['v']));
+ expect(parser.options['allow-multi-def'].getOrDefault(null),
+ equals(['def']));
+ expect(
+ parser.options['allow-multi-def'].getOrDefault(['v']), equals(['v']));
expect(parser.options['multi-no'].getOrDefault(null), equals([]));
expect(parser.options['multi-no'].getOrDefault(['v']), equals(['v']));
expect(parser.options['multi-def'].getOrDefault(null), equals(['def']));
diff --git a/test/parse_test.dart b/test/parse_test.dart
index 87f3d81..d212cf0 100644
--- a/test/parse_test.dart
+++ b/test/parse_test.dart
@@ -152,104 +152,186 @@
expect(a, isNull);
});
- test(
- 'for multiple present, allowMultiple, options are invoked with '
- 'value as a list', () {
- var a;
- var parser = new ArgParser();
- parser.addOption('a',
- allowMultiple: true, callback: (value) => a = value);
+ group("with allowMultiple", () {
+ test('for multiple present, options are invoked with value as a list',
+ () {
+ var a;
+ var parser = new ArgParser();
+ parser.addOption('a',
+ allowMultiple: true, callback: (value) => a = value);
- parser.parse(['--a=v', '--a=x']);
- expect(a, equals(['v', 'x']));
+ parser.parse(['--a=v', '--a=x']);
+ expect(a, equals(['v', 'x']));
- // This reified type is important in strong mode so that people can
- // safely write "as List<String>".
- expect(a, new isInstanceOf<List<String>>());
+ // This reified type is important in strong mode so that people can
+ // safely write "as List<String>".
+ expect(a, new isInstanceOf<List<String>>());
+ });
+
+ test(
+ 'for single present, options are invoked with value as a single '
+ 'element list', () {
+ var a;
+ var parser = new ArgParser();
+ parser.addOption('a',
+ allowMultiple: true, callback: (value) => a = value);
+
+ parser.parse(['--a=v']);
+ expect(a, equals(['v']));
+ });
+
+ test('for absent, options are invoked with default value as a list',
+ () {
+ var a;
+ var parser = new ArgParser();
+ parser.addOption('a',
+ allowMultiple: true,
+ defaultsTo: 'v',
+ callback: (value) => a = value);
+
+ parser.parse([]);
+ expect(a, equals(['v']));
+ });
+
+ test('for absent, options are invoked with value as an empty list', () {
+ var a;
+ var parser = new ArgParser();
+ parser.addOption('a',
+ allowMultiple: true, callback: (value) => a = value);
+
+ parser.parse([]);
+ expect(a, isEmpty);
+ });
+
+ test('parses comma-separated strings', () {
+ var a;
+ var parser = new ArgParser();
+ parser.addOption('a',
+ allowMultiple: true, callback: (value) => a = value);
+
+ parser.parse(['--a=v,w', '--a=x']);
+ expect(a, equals(['v', 'w', 'x']));
+ });
+
+ test("doesn't parse comma-separated strings with splitCommas: false",
+ () {
+ var a;
+ var parser = new ArgParser();
+ parser.addOption('a',
+ allowMultiple: true,
+ splitCommas: false,
+ callback: (value) => a = value);
+
+ parser.parse(['--a=v,w', '--a=x']);
+ expect(a, equals(['v,w', 'x']));
+ });
+
+ test('parses empty strings', () {
+ var a;
+ var parser = new ArgParser();
+ parser.addOption('a',
+ allowMultiple: true, callback: (value) => a = value);
+
+ parser.parse(['--a=,v', '--a=w,', '--a=,', '--a=x,,y', '--a', '']);
+ expect(a, equals(['', 'v', 'w', '', '', '', 'x', '', 'y', '']));
+ });
+
+ test('with allowed parses comma-separated strings', () {
+ var a;
+ var parser = new ArgParser();
+ parser.addOption('a',
+ allowMultiple: true,
+ allowed: ['v', 'w', 'x'],
+ callback: (value) => a = value);
+
+ parser.parse(['--a=v,w', '--a=x']);
+ expect(a, equals(['v', 'w', 'x']));
+ });
});
- test(
- 'for single present, allowMultiple, options are invoked with '
- ' value as a single element list', () {
- var a;
- var parser = new ArgParser();
- parser.addOption('a',
- allowMultiple: true, callback: (value) => a = value);
+ group("with addMultiOption", () {
+ test('for multiple present, options are invoked with value as a list',
+ () {
+ var a;
+ var parser = new ArgParser();
+ parser.addMultiOption('a', callback: (value) => a = value);
- parser.parse(['--a=v']);
- expect(a, equals(['v']));
- });
+ parser.parse(['--a=v', '--a=x']);
+ expect(a, equals(['v', 'x']));
- test(
- 'for absent, allowMultiple, options are invoked with default '
- 'value as a list.', () {
- var a;
- var parser = new ArgParser();
- parser.addOption('a',
- allowMultiple: true,
- defaultsTo: 'v',
- callback: (value) => a = value);
+ // This reified type is important in strong mode so that people can
+ // safely write "as List<String>".
+ expect(a, new isInstanceOf<List<String>>());
+ });
- parser.parse([]);
- expect(a, equals(['v']));
- });
+ test(
+ 'for single present, options are invoked with value as a single '
+ 'element list', () {
+ var a;
+ var parser = new ArgParser();
+ parser.addMultiOption('a', callback: (value) => a = value);
- test(
- 'for absent, allowMultiple, options are invoked with value '
- 'as an empty list.', () {
- var a;
- var parser = new ArgParser();
- parser.addOption('a',
- allowMultiple: true, callback: (value) => a = value);
+ parser.parse(['--a=v']);
+ expect(a, equals(['v']));
+ });
- parser.parse([]);
- expect(a, isEmpty);
- });
+ test('for absent, options are invoked with default value', () {
+ var a;
+ var parser = new ArgParser();
+ parser.addMultiOption('a',
+ defaultsTo: ['v', 'w'], callback: (value) => a = value);
- test('allowMultiple parses comma-separated strings', () {
- var a;
- var parser = new ArgParser();
- parser.addOption('a',
- allowMultiple: true, callback: (value) => a = value);
+ parser.parse([]);
+ expect(a, equals(['v', 'w']));
+ });
- parser.parse(['--a=v,w', '--a=x']);
- expect(a, equals(['v', 'w', 'x']));
- });
+ test('for absent, options are invoked with value as an empty list', () {
+ var a;
+ var parser = new ArgParser();
+ parser.addMultiOption('a', callback: (value) => a = value);
- test(
- "allowMultiple doesn't parses comma-separated strings with "
- "splitCommas: false", () {
- var a;
- var parser = new ArgParser();
- parser.addOption('a',
- allowMultiple: true,
- splitCommas: false,
- callback: (value) => a = value);
+ parser.parse([]);
+ expect(a, isEmpty);
+ });
- parser.parse(['--a=v,w', '--a=x']);
- expect(a, equals(['v,w', 'x']));
- });
+ test('parses comma-separated strings', () {
+ var a;
+ var parser = new ArgParser();
+ parser.addMultiOption('a', callback: (value) => a = value);
- test('allowMultiple parses empty strings', () {
- var a;
- var parser = new ArgParser();
- parser.addOption('a',
- allowMultiple: true, callback: (value) => a = value);
+ parser.parse(['--a=v,w', '--a=x']);
+ expect(a, equals(['v', 'w', 'x']));
+ });
- parser.parse(['--a=,v', '--a=w,', '--a=,', '--a=x,,y', '--a', '']);
- expect(a, equals(['', 'v', 'w', '', '', '', 'x', '', 'y', '']));
- });
+ test("doesn't parse comma-separated strings with splitCommas: false",
+ () {
+ var a;
+ var parser = new ArgParser();
+ parser.addMultiOption('a',
+ splitCommas: false, callback: (value) => a = value);
- test('allowMultiple with allowed parses comma-separated strings', () {
- var a;
- var parser = new ArgParser();
- parser.addOption('a',
- allowMultiple: true,
- allowed: ['v', 'w', 'x'],
- callback: (value) => a = value);
+ parser.parse(['--a=v,w', '--a=x']);
+ expect(a, equals(['v,w', 'x']));
+ });
- parser.parse(['--a=v,w', '--a=x']);
- expect(a, equals(['v', 'w', 'x']));
+ test('parses empty strings', () {
+ var a;
+ var parser = new ArgParser();
+ parser.addMultiOption('a', callback: (value) => a = value);
+
+ parser.parse(['--a=,v', '--a=w,', '--a=,', '--a=x,,y', '--a', '']);
+ expect(a, equals(['', 'v', 'w', '', '', '', 'x', '', 'y', '']));
+ });
+
+ test('with allowed parses comma-separated strings', () {
+ var a;
+ var parser = new ArgParser();
+ parser.addMultiOption('a',
+ allowed: ['v', 'w', 'x'], callback: (value) => a = value);
+
+ parser.parse(['--a=v,w', '--a=x']);
+ expect(a, equals(['v', 'w', 'x']));
+ });
});
});
@@ -340,12 +422,22 @@
throwsFormat(parser, ['-mprofile']);
});
- test('throw if a comma-separated value is not allowed', () {
- var parser = new ArgParser();
- parser.addOption('mode',
- abbr: 'm', allowMultiple: true, allowed: ['debug', 'release']);
+ group('throw if a comma-separated value is not allowed', () {
+ test("with allowMultiple", () {
+ var parser = new ArgParser();
+ parser.addOption('mode',
+ abbr: 'm', allowMultiple: true, allowed: ['debug', 'release']);
- throwsFormat(parser, ['-mdebug,profile']);
+ throwsFormat(parser, ['-mdebug,profile']);
+ });
+
+ test("with addMultiOption", () {
+ var parser = new ArgParser();
+ parser
+ .addMultiOption('mode', abbr: 'm', allowed: ['debug', 'release']);
+
+ throwsFormat(parser, ['-mdebug,profile']);
+ });
});
test('throw if any but the first is not a flag', () {
@@ -449,22 +541,40 @@
expect(args['define'], equals('2'));
});
- test('returns a List if multi-valued', () {
- var parser = new ArgParser();
- parser.addOption('define', allowMultiple: true);
- var args = parser.parse(['--define=1']);
- expect(args['define'], equals(['1']));
- args = parser.parse(['--define=1', '--define=2']);
- expect(args['define'], equals(['1', '2']));
+ group('returns a List', () {
+ test('with allowMultiple', () {
+ var parser = new ArgParser();
+ parser.addOption('define', allowMultiple: true);
+ var args = parser.parse(['--define=1']);
+ expect(args['define'], equals(['1']));
+ args = parser.parse(['--define=1', '--define=2']);
+ expect(args['define'], equals(['1', '2']));
+ });
+
+ test('with addMultiOption', () {
+ var parser = new ArgParser();
+ parser.addMultiOption('define');
+ var args = parser.parse(['--define=1']);
+ expect(args['define'], equals(['1']));
+ args = parser.parse(['--define=1', '--define=2']);
+ expect(args['define'], equals(['1', '2']));
+ });
});
- test(
- 'returns the default value for multi-valued arguments '
- 'if not explicitly set', () {
- var parser = new ArgParser();
- parser.addOption('define', defaultsTo: '0', allowMultiple: true);
- var args = parser.parse(['']);
- expect(args['define'], equals(['0']));
+ group('returns the default value if not explicitly set', () {
+ test('with allowMultiple', () {
+ var parser = new ArgParser();
+ parser.addOption('define', defaultsTo: '0', allowMultiple: true);
+ var args = parser.parse(['']);
+ expect(args['define'], equals(['0']));
+ });
+
+ test('with addMultiOption', () {
+ var parser = new ArgParser();
+ parser.addMultiOption('define', defaultsTo: ['0']);
+ var args = parser.parse(['']);
+ expect(args['define'], equals(['0']));
+ });
});
test('are case-sensitive', () {