diff --git a/CHANGELOG.md b/CHANGELOG.md index f8374ed..6f2b821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.5.0-wip + +* Introduce new typed `ArgResults` `flag(String)`, `option(String)`, and + `multiOption(String)` methods. + ## 2.4.2 * Change the validation of `mandatory` options; they now perform validation when diff --git a/README.md b/README.md index e2609ad..c7c9b89 100644 --- a/README.md +++ b/README.md @@ -111,8 +111,8 @@ parser.addOption('mode'); parser.addFlag('verbose', defaultsTo: true); var results = parser.parse(['--mode', 'debug', 'something', 'else']); -print(results['mode']); // debug -print(results['verbose']); // true +print(results.option('mode')); // debug +print(results.flag('verbose')); // true ``` By default, the `parse()` method allows additional flags and options to be @@ -187,7 +187,7 @@ overriding earlier ones; for example: var parser = ArgParser(); parser.addOption('mode'); var results = parser.parse(['--mode', 'on', '--mode', 'off']); -print(results['mode']); // prints 'off' +print(results.option('mode')); // prints 'off' ``` Multiple values can be parsed with `addMultiOption()`. With this method, an @@ -198,7 +198,7 @@ values: var parser = ArgParser(); parser.addMultiOption('mode'); var results = parser.parse(['--mode', 'on', '--mode', 'off']); -print(results['mode']); // prints '[on, off]' +print(results.multiOption('mode')); // prints '[on, off]' ``` By default, values for a multi-valued option may also be separated with commas: @@ -207,7 +207,7 @@ By default, values for a multi-valued option may also be separated with commas: var parser = ArgParser(); parser.addMultiOption('mode'); var results = parser.parse(['--mode', 'on,off']); -print(results['mode']); // prints '[on, off]' +print(results.multiOption('mode')); // prints '[on, off]' ``` This can be disabled by passing `splitCommas: false`. @@ -326,7 +326,7 @@ class CommitCommand extends Command { void run() { // [argResults] is set before [run()] is called and contains the flags/options // passed to this command. - print(argResults['all']); + print(argResults.flag('all')); } } ``` diff --git a/example/command_runner/draw.dart b/example/command_runner/draw.dart index 35a3601..018bf59 100644 --- a/example/command_runner/draw.dart +++ b/example/command_runner/draw.dart @@ -33,8 +33,8 @@ class SquareCommand extends Command { @override FutureOr? run() { - final size = int.parse(argResults?['size'] as String? ?? '20'); - final char = (globalResults?['char'] as String?)?[0] ?? '#'; + final size = int.parse(argResults?.option('size') ?? '20'); + final char = globalResults?.option('char')?[0] ?? '#'; return draw(size, size, char, (x, y) => true); } } @@ -55,8 +55,8 @@ class CircleCommand extends Command { @override FutureOr? run() { - final size = 2 * int.parse(argResults?['radius'] as String? ?? '10'); - final char = (globalResults?['char'] as String?)?[0] ?? '#'; + final size = 2 * int.parse(argResults?.option('radius') ?? '10'); + final char = globalResults?.option('char')?[0] ?? '#'; return draw(size, size, char, (x, y) => x * x + y * y < 1); } } @@ -93,8 +93,8 @@ class EquilateralTriangleCommand extends Command { @override FutureOr? run() { - final size = int.parse(argResults?['size'] as String? ?? '20'); - final char = (globalResults?['char'] as String?)?[0] ?? '#'; + final size = int.parse(argResults?.option('size') ?? '20'); + final char = globalResults?.option('char')?[0] ?? '#'; return drawTriangle(size, size * sqrt(3) ~/ 2, char); } } @@ -116,9 +116,9 @@ class IsoscelesTriangleCommand extends Command { @override FutureOr? run() { - final width = int.parse(argResults?['width'] as String? ?? '50'); - final height = int.parse(argResults?['height'] as String? ?? '10'); - final char = (globalResults?['char'] as String?)?[0] ?? '#'; + final width = int.parse(argResults?.option('width') ?? '50'); + final height = int.parse(argResults?.option('height') ?? '10'); + final char = globalResults?.option('char')?[0] ?? '#'; return drawTriangle(width, height, char); } } diff --git a/lib/command_runner.dart b/lib/command_runner.dart index 1845160..efd95ca 100644 --- a/lib/command_runner.dart +++ b/lib/command_runner.dart @@ -192,13 +192,13 @@ class CommandRunner { commands = command._subcommands as Map>; commandString += ' ${argResults.name}'; - if (argResults.options.contains('help') && (argResults['help'] as bool)) { + if (argResults.options.contains('help') && argResults.flag('help')) { command.printUsage(); return null; } } - if (topLevelResults['help'] as bool) { + if (topLevelResults.flag('help')) { command!.printUsage(); return null; } diff --git a/lib/src/arg_results.dart b/lib/src/arg_results.dart index 3761ab1..9fa87cd 100644 --- a/lib/src/arg_results.dart +++ b/lib/src/arg_results.dart @@ -21,7 +21,7 @@ ArgResults newArgResults( } /// The results of parsing a series of command line arguments using -/// [ArgParser.parse()]. +/// [ArgParser.parse]. /// /// Includes the parsed options and any remaining unparsed command line /// arguments. @@ -57,9 +57,13 @@ class ArgResults { : rest = UnmodifiableListView(rest), arguments = UnmodifiableListView(arguments); - /// Returns the parsed ore default command-line option named [name]. + /// Returns the parsed or default command-line option named [name]. /// /// [name] must be a valid option name in the parser. + /// + /// > [!Note] + /// > Callers should prefer using the more strongly typed methods - [flag] for + /// > 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".'); @@ -73,6 +77,48 @@ class ArgResults { return option.valueOrDefault(_parsed[name]); } + /// Returns the parsed or default command-line flag named [name]. + /// + /// [name] must be a valid flag name in the parser. + bool flag(String name) { + var option = _parser.options[name]; + if (option == null) { + throw ArgumentError('Could not find an option named "$name".'); + } + if (!option.isFlag) { + throw ArgumentError('"$name" is not a flag.'); + } + return option.valueOrDefault(_parsed[name]) as bool; + } + + /// Returns the parsed or default command-line option named [name]. + /// + /// [name] must be a valid option name in the parser. + String? option(String name) { + var option = _parser.options[name]; + if (option == null) { + throw ArgumentError('Could not find an option named "$name".'); + } + if (!option.isSingle) { + throw ArgumentError('"$name" is a multi-option.'); + } + return option.valueOrDefault(_parsed[name]) as String?; + } + + /// Returns the list of parsed (or default) command-line options for [name]. + /// + /// [name] must be a valid option name in the parser. + List multiOption(String name) { + var option = _parser.options[name]; + if (option == null) { + throw ArgumentError('Could not find an option named "$name".'); + } + if (!option.isMultiple) { + throw ArgumentError('"$name" is not a multi-option.'); + } + return option.valueOrDefault(_parsed[name]) as List; + } + /// The names of the available options. /// /// Includes the options whose values were parsed or that have defaults. diff --git a/pubspec.yaml b/pubspec.yaml index 3166e1d..3acfa3e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,8 +1,8 @@ name: args -version: 2.4.2 +version: 2.5.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. + Library for defining parsers for parsing raw command-line arguments into a set + of options and values using GNU and POSIX style options. repository: https://github.com/dart-lang/args topics: diff --git a/test/parse_test.dart b/test/parse_test.dart index 318d2b5..9276fc4 100644 --- a/test/parse_test.dart +++ b/test/parse_test.dart @@ -83,6 +83,79 @@ void main() { var results = parser.parse(['--no-b']); expect(results['a'], isFalse); }); + + test('throws if requested as a multi-option', () { + var parser = ArgParser(); + parser.addFlag('a', defaultsTo: true); + var results = parser.parse(['--a']); + throwsIllegalArg(() => results.multiOption('a')); + }); + }); + + group('flag()', () { + test('returns true if present', () { + var parser = ArgParser(); + parser.addFlag('verbose'); + + var args = parser.parse(['--verbose']); + expect(args.flag('verbose'), isTrue); + }); + + test('returns default if missing', () { + var parser = ArgParser(); + parser.addFlag('a', defaultsTo: true); + parser.addFlag('b', defaultsTo: false); + + var args = parser.parse([]); + expect(args.flag('a'), isTrue); + expect(args.flag('b'), isFalse); + }); + + test('are false if missing with no default', () { + var parser = ArgParser(); + parser.addFlag('verbose'); + + var args = parser.parse([]); + expect(args.flag('verbose'), isFalse); + }); + + test('are case-sensitive', () { + var parser = ArgParser(); + parser.addFlag('verbose'); + parser.addFlag('Verbose'); + var results = parser.parse(['--verbose']); + expect(results.flag('verbose'), isTrue); + expect(results.flag('Verbose'), isFalse); + }); + + test('match letters, numbers, hyphens and underscores', () { + var parser = ArgParser(); + var allCharacters = + 'abcdefghijklmnopqrstuvwxyz-ABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789'; + parser.addFlag(allCharacters); + var results = parser.parse(['--$allCharacters']); + expect(results.flag(allCharacters), isTrue); + }); + + test('can match by alias', () { + var parser = ArgParser()..addFlag('a', aliases: ['b']); + var results = parser.parse(['--b']); + expect(results.flag('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.flag('a'), isFalse); + }); + + test('throws if requested as a multi-option', () { + var parser = ArgParser(); + parser.addFlag('a', defaultsTo: true); + var results = parser.parse(['--a']); + throwsIllegalArg(() => results.multiOption('a')); + }); }); group('flags negated with "no-"', () { @@ -475,6 +548,13 @@ void main() { expect(args['define'], equals('2')); }); + test('throw if requested as a multi-option', () { + var parser = ArgParser(); + parser.addOption('a', defaultsTo: 'b'); + var results = parser.parse(['--a=c']); + throwsIllegalArg(() => results.multiOption('a')); + }); + group('returns a List', () { test('with addMultiOption', () { var parser = ArgParser(); @@ -545,6 +625,111 @@ void main() { }); }); + group('option()', () { + test('are parsed if present', () { + var parser = ArgParser(); + parser.addOption('mode'); + var args = parser.parse(['--mode=release']); + expect(args.option('mode'), equals('release')); + }); + + test('are null if not present', () { + var parser = ArgParser(); + parser.addOption('mode'); + var args = parser.parse([]); + expect(args.option('mode'), isNull); + }); + + test('default if missing', () { + var parser = ArgParser(); + parser.addOption('mode', defaultsTo: 'debug'); + var args = parser.parse([]); + expect(args.option('mode'), equals('debug')); + }); + + test('allow the value to be separated by whitespace', () { + var parser = ArgParser(); + parser.addOption('mode'); + var args = parser.parse(['--mode', 'release']); + expect(args.option('mode'), equals('release')); + }); + + test('do not throw if the value is in the allowed set', () { + var parser = ArgParser(); + parser.addOption('mode', allowed: ['debug', 'release']); + var args = parser.parse(['--mode=debug']); + expect(args.option('mode'), equals('debug')); + }); + + test('do not throw if there is no allowed set with allowedHelp', () { + var parser = ArgParser(); + parser.addOption('mode', allowedHelp: { + 'debug': 'During development.', + 'release': 'For customers.' + }); + var args = parser.parse(['--mode=profile']); + expect(args.option('mode'), equals('profile')); + }); + + test('returns last provided value', () { + var parser = ArgParser(); + parser.addOption('define'); + var args = parser.parse(['--define=1', '--define=2']); + expect(args.option('define'), equals('2')); + }); + + test('throw if requested as a multi-option', () { + var parser = ArgParser(); + parser.addOption('a', defaultsTo: 'b'); + var results = parser.parse(['--a=c']); + throwsIllegalArg(() => results.multiOption('a')); + }); + + group('returns a List', () { + test('with addMultiOption', () { + var parser = ArgParser(); + parser.addMultiOption('define'); + var args = parser.parse(['--define=1']); + expect(args.multiOption('define'), equals(['1'])); + args = parser.parse(['--define=1', '--define=2']); + expect(args.multiOption('define'), equals(['1', '2'])); + }); + }); + + group('returns the default value if not explicitly set', () { + test('with addMultiOption', () { + var parser = ArgParser(); + parser.addMultiOption('define', defaultsTo: ['0']); + var args = parser.parse(['']); + expect(args.multiOption('define'), equals(['0'])); + }); + }); + + test('are case-sensitive', () { + var parser = ArgParser(); + parser.addOption('verbose', defaultsTo: 'no'); + parser.addOption('Verbose', defaultsTo: 'no'); + var results = parser.parse(['--verbose', 'chatty']); + expect(results.option('verbose'), equals('chatty')); + expect(results.option('Verbose'), equals('no')); + }); + + test('can be set by alias', () { + var parser = ArgParser()..addOption('a', aliases: ['b']); + var results = parser.parse(['--b=1']); + expect(results.option('a'), '1'); + }); + + group('mandatory', () { + test('parse successfully', () { + var parser = ArgParser(); + parser.addOption('test', mandatory: true); + var results = parser.parse(['--test', 'test']); + expect(results.option('test'), equals('test')); + }); + }); + }); + group('remaining args', () { test('stops parsing args when a non-option-like arg is encountered', () { var parser = ArgParser();