Add support for argument name aliases (#182)
Closes https://github.com/dart-lang/args/issues/181
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 one argument name to another.
Keys are _not_ added to the `options` map for each alias.
cc @Hixie
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d5de6a9..d09d006 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 2.1.0-dev
+
+* 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
+ one argument name to another.
+
## 2.0.0
* Stable null safety release.
diff --git a/lib/src/allow_anything_parser.dart b/lib/src/allow_anything_parser.dart
index 831e3a6..becff98 100644
--- a/lib/src/allow_anything_parser.dart
+++ b/lib/src/allow_anything_parser.dart
@@ -35,7 +35,8 @@
bool? defaultsTo = false,
bool negatable = true,
void Function(bool)? callback,
- bool hide = false}) {
+ bool hide = false,
+ List<String> aliases = const []}) {
throw UnsupportedError(
"ArgParser.allowAnything().addFlag() isn't supported.");
}
@@ -51,7 +52,8 @@
void Function(String?)? callback,
bool allowMultiple = false,
bool? splitCommas,
- bool hide = false}) {
+ bool hide = false,
+ List<String> aliases = const []}) {
throw UnsupportedError(
"ArgParser.allowAnything().addOption() isn't supported.");
}
@@ -66,7 +68,8 @@
Iterable<String>? defaultsTo,
void Function(List<String>)? callback,
bool splitCommas = true,
- bool hide = false}) {
+ bool hide = false,
+ List<String> aliases = const []}) {
throw UnsupportedError(
"ArgParser.allowAnything().addMultiOption() isn't supported.");
}
@@ -96,4 +99,7 @@
@override
Option? findByAbbreviation(String abbr) => null;
+
+ @override
+ Option? findByNameOrAlias(String name) => null;
}
diff --git a/lib/src/arg_parser.dart b/lib/src/arg_parser.dart
index 4279738..87f7a02 100644
--- a/lib/src/arg_parser.dart
+++ b/lib/src/arg_parser.dart
@@ -16,6 +16,9 @@
final Map<String, Option> _options;
final Map<String, ArgParser> _commands;
+ /// A map of aliases to the option names they alias.
+ final Map<String, String> _aliases;
+
/// The options that have been defined for this parser.
final Map<String, Option> options;
@@ -53,7 +56,7 @@
/// the parser stops parsing as soon as it finds an argument that is neither
/// an option nor a command.
factory ArgParser({bool allowTrailingOptions = true, int? usageLineLength}) =>
- ArgParser._(<String, Option>{}, <String, ArgParser>{},
+ ArgParser._(<String, Option>{}, <String, ArgParser>{}, <String, String>{},
allowTrailingOptions: allowTrailingOptions,
usageLineLength: usageLineLength);
@@ -66,6 +69,7 @@
factory ArgParser.allowAnything() = AllowAnythingParser;
ArgParser._(Map<String, Option> options, Map<String, ArgParser> commands,
+ this._aliases,
{bool allowTrailingOptions = true, this.usageLineLength})
: _options = options,
options = UnmodifiableMapView(options),
@@ -116,6 +120,9 @@
///
/// If [hide] is `true`, this option won't be included in [usage].
///
+ /// If [aliases] is provided, these are used as aliases for [name]. These
+ /// aliases will not appear as keys in the [options] map.
+ ///
/// Throws an [ArgumentError] if:
///
/// * There is already an option named [name].
@@ -126,7 +133,8 @@
bool? defaultsTo = false,
bool negatable = true,
void Function(bool)? callback,
- bool hide = false}) {
+ bool hide = false,
+ List<String> aliases = const []}) {
_addOption(
name,
abbr,
@@ -138,7 +146,8 @@
callback == null ? null : (value) => callback(value as bool),
OptionType.flag,
negatable: negatable,
- hide: hide);
+ hide: hide,
+ aliases: aliases);
}
/// Defines an option that takes a value.
@@ -174,6 +183,9 @@
///
/// If [hide] is `true`, this option won't be included in [usage].
///
+ /// If [aliases] is provided, these are used as aliases for [name]. These
+ /// aliases will not appear as keys in the [options] map.
+ ///
/// Throws an [ArgumentError] if:
///
/// * There is already an option with name [name].
@@ -186,10 +198,11 @@
Map<String, String>? allowedHelp,
String? defaultsTo,
void Function(String?)? callback,
- bool hide = false}) {
+ bool hide = false,
+ List<String> aliases = const []}) {
_addOption(name, abbr, help, valueHelp, allowed, allowedHelp, defaultsTo,
callback, OptionType.single,
- hide: hide);
+ hide: hide, aliases: aliases);
}
/// Defines an option that takes multiple values.
@@ -225,6 +238,9 @@
///
/// If [hide] is `true`, this option won't be included in [usage].
///
+ /// If [aliases] is provided, these are used as aliases for [name]. These
+ /// aliases will not appear as keys in the [options] map.
+ ///
/// Throws an [ArgumentError] if:
///
/// * There is already an option with name [name].
@@ -238,7 +254,8 @@
Iterable<String>? defaultsTo,
void Function(List<String>)? callback,
bool splitCommas = true,
- bool hide = false}) {
+ bool hide = false,
+ List<String> aliases = const []}) {
_addOption(
name,
abbr,
@@ -250,7 +267,8 @@
callback == null ? null : (value) => callback(value as List<String>),
OptionType.multiple,
splitCommas: splitCommas,
- hide: hide);
+ hide: hide,
+ aliases: aliases);
}
void _addOption(
@@ -265,10 +283,11 @@
OptionType type,
{bool negatable = false,
bool? splitCommas,
- bool hide = false}) {
- // Make sure the name isn't in use.
- if (_options.containsKey(name)) {
- throw ArgumentError('Duplicate option "$name".');
+ bool hide = false,
+ List<String> aliases = const []}) {
+ var allNames = [name, ...aliases];
+ if (allNames.any((name) => findByNameOrAlias(name) != null)) {
+ throw ArgumentError('Duplicate option or alias "$name".');
}
// Make sure the abbreviation isn't too long or in use.
@@ -282,9 +301,15 @@
var option = newOption(name, abbr, help, valueHelp, allowed, allowedHelp,
defaultsTo, callback, type,
- negatable: negatable, splitCommas: splitCommas, hide: hide);
+ negatable: negatable,
+ splitCommas: splitCommas,
+ hide: hide,
+ aliases: aliases);
_options[name] = option;
_optionsAndSeparators.add(option);
+ for (var alias in aliases) {
+ _aliases[alias] = name;
+ }
}
/// Adds a separator line to the usage.
@@ -309,7 +334,7 @@
/// Returns the default value for [option].
dynamic defaultFor(String option) {
- var value = options[option];
+ var value = findByNameOrAlias(option);
if (value == null) {
throw ArgumentError('No option named $option');
}
@@ -327,4 +352,8 @@
}
return null;
}
+
+ /// Finds the option whose name or alias matches [name], or `null` if no
+ /// option has that name or alias.
+ Option? findByNameOrAlias(String name) => options[_aliases[name] ?? name];
}
diff --git a/lib/src/option.dart b/lib/src/option.dart
index a85acc8..7a91a4b 100644
--- a/lib/src/option.dart
+++ b/lib/src/option.dart
@@ -18,10 +18,14 @@
OptionType type,
{bool? negatable,
bool? splitCommas,
- bool hide = false}) {
+ bool hide = false,
+ List<String> aliases = const []}) {
return Option._(name, abbr, help, valueHelp, allowed, allowedHelp, defaultsTo,
callback, type,
- negatable: negatable, splitCommas: splitCommas, hide: hide);
+ negatable: negatable,
+ splitCommas: splitCommas,
+ hide: hide,
+ aliases: aliases);
}
/// A command-line option.
@@ -73,6 +77,9 @@
/// Whether this option should be hidden from usage documentation.
final bool hide;
+ /// All aliases for [name].
+ final List<String> aliases;
+
/// Whether the option is boolean-valued flag.
bool get isFlag => type == OptionType.flag;
@@ -94,7 +101,8 @@
OptionType type,
{this.negatable,
bool? splitCommas,
- this.hide = false})
+ this.hide = false,
+ this.aliases = const []})
: allowed = allowed == null ? null : List.unmodifiable(allowed),
allowedHelp =
allowedHelp == null ? null : Map.unmodifiable(allowedHelp),
diff --git a/lib/src/parser.dart b/lib/src/parser.dart
index c14a01d..9cfaa60 100644
--- a/lib/src/parser.dart
+++ b/lib/src/parser.dart
@@ -243,7 +243,7 @@
return false;
}
- var option = grammar.options[name];
+ var option = grammar.findByNameOrAlias(name);
if (option != null) {
args.removeFirst();
if (option.isFlag) {
@@ -261,7 +261,7 @@
} else if (name.startsWith('no-')) {
// See if it's a negated flag.
name = name.substring('no-'.length);
- option = grammar.options[name];
+ option = grammar.findByNameOrAlias(name);
if (option == null) {
// Walk up to the parent command if possible.
validate(parent != null, 'Could not find an option named "$name".');
diff --git a/pubspec.yaml b/pubspec.yaml
index f39c81f..c980dba 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: args
-version: 2.0.0
+version: 2.1.0-dev
homepage: https://github.com/dart-lang/args
description: >-
Library for defining parsers for parsing raw command-line arguments into a set
diff --git a/test/args_test.dart b/test/args_test.dart
index d3bfd44..04bbc47 100644
--- a/test/args_test.dart
+++ b/test/args_test.dart
@@ -184,6 +184,40 @@
});
});
+ group('ArgParser.findByNameOrAlias', () {
+ test('returns null if there is no match', () {
+ var parser = ArgParser();
+ expect(parser.findByNameOrAlias('a'), isNull);
+ });
+
+ test('can find options by alias', () {
+ var parser = ArgParser()..addOption('a', aliases: ['b']);
+ expect(parser.findByNameOrAlias('b'),
+ isA<Option>().having((o) => o.name, 'name', 'a'));
+ });
+
+ test('can find flags by alias', () {
+ var parser = ArgParser()..addFlag('a', aliases: ['b']);
+ expect(parser.findByNameOrAlias('b'),
+ isA<Option>().having((o) => o.name, 'name', 'a'));
+ });
+
+ test('does not allow duplicate aliases', () {
+ var parser = ArgParser()..addOption('a', aliases: ['b']);
+ throwsIllegalArg(() => parser.addOption('c', aliases: ['b']));
+ });
+
+ test('does not allow aliases that conflict with existing names', () {
+ var parser = ArgParser()..addOption('a', aliases: ['b']);
+ throwsIllegalArg(() => parser.addOption('c', aliases: ['a']));
+ });
+
+ test('does not allow names that conflict with existing aliases', () {
+ var parser = ArgParser()..addOption('a', aliases: ['b']);
+ throwsIllegalArg(() => parser.addOption('b'));
+ });
+ });
+
group('ArgResults', () {
group('options', () {
test('returns the provided options', () {
diff --git a/test/parse_test.dart b/test/parse_test.dart
index a785f6e..c90219e 100644
--- a/test/parse_test.dart
+++ b/test/parse_test.dart
@@ -70,6 +70,19 @@
var results = parser.parse(['--$allCharacters']);
expect(results[allCharacters], isTrue);
});
+
+ test('can match by alias', () {
+ var parser = ArgParser()..addFlag('a', aliases: ['b']);
+ var results = parser.parse(['--b']);
+ expect(results['a'], isTrue);
+ });
+
+ 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['a'], isFalse);
+ });
});
group('flags negated with "no-"', () {
@@ -245,6 +258,12 @@
parser.parse(['--a=v,w', '--a=x']);
expect(a, equals(['v', 'w', 'x']));
});
+
+ test('can mix and match alias and regular name', () {
+ var parser = ArgParser()..addMultiOption('a', aliases: ['b']);
+ var results = parser.parse(['--a=1', '--b=2']);
+ expect(results['a'], ['1', '2']);
+ });
});
});
@@ -484,6 +503,12 @@
expect(results['verbose'], equals('chatty'));
expect(results['Verbose'], equals('no'));
});
+
+ test('can be set by alias', () {
+ var parser = ArgParser()..addOption('a', aliases: ['b']);
+ var results = parser.parse(['--b=1']);
+ expect(results['a'], '1');
+ });
});
group('remaining args', () {