├── .test_config ├── example ├── arg_parser │ ├── README.md │ ├── pubspec.yaml │ └── example.dart └── command_runner │ ├── README.md │ ├── pubspec.yaml │ └── draw.dart ├── .gitignore ├── .github ├── workflows │ ├── publish.yaml │ └── test-package.yml └── dependabot.yml ├── pubspec.yaml ├── lib ├── args.dart ├── src │ ├── usage_exception.dart │ ├── arg_parser_exception.dart │ ├── help_command.dart │ ├── allow_anything_parser.dart │ ├── arg_results.dart │ ├── utils.dart │ ├── option.dart │ ├── usage.dart │ ├── parser.dart │ └── arg_parser.dart └── command_runner.dart ├── analysis_options.yaml ├── LICENSE ├── CONTRIBUTING.md ├── test ├── parse_performance_test.dart ├── allow_anything_test.dart ├── trailing_options_test.dart ├── command_test.dart ├── command_parse_test.dart ├── utils_test.dart ├── test_utils.dart ├── args_test.dart ├── usage_test.dart └── command_runner_test.dart ├── CHANGELOG.md └── README.md /.test_config: -------------------------------------------------------------------------------- 1 | { 2 | "test_package": true 3 | } -------------------------------------------------------------------------------- /example/arg_parser/README.md: -------------------------------------------------------------------------------- 1 | # Example of using `ArgParser` 2 | 3 | `dart run example.dart` 4 | -------------------------------------------------------------------------------- /example/command_runner/README.md: -------------------------------------------------------------------------------- 1 | # Example of using `CommandRunner` 2 | 3 | This example uses `CommandRunner` to create a tool for drawing ascii art shapes. 4 | 5 | `dart run draw.dart circle --radius=10` 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don’t commit the following directories created by pub. 2 | .buildlog 3 | .pub/ 4 | .dart_tool/ 5 | build/ 6 | packages 7 | .packages 8 | 9 | # Or the files created by dart2js. 10 | *.dart.js 11 | *.js_ 12 | *.js.deps 13 | *.js.map 14 | 15 | # Include when developing application packages. 16 | pubspec.lock 17 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # A CI configuration to auto-publish pub packages. 2 | 3 | name: Publish 4 | 5 | on: 6 | pull_request: 7 | branches: [ main ] 8 | push: 9 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+*' ] 10 | 11 | jobs: 12 | publish: 13 | if: ${{ github.repository_owner == 'dart-lang' }} 14 | uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration file. 2 | # See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates 3 | version: 2 4 | 5 | updates: 6 | - package-ecosystem: github-actions 7 | directory: / 8 | schedule: 9 | interval: monthly 10 | labels: 11 | - autosubmit 12 | groups: 13 | github-actions: 14 | patterns: 15 | - "*" 16 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: args 2 | version: 2.6.0-wip 3 | description: >- 4 | Library for defining parsers for parsing raw command-line arguments into a set 5 | of options and values using GNU and POSIX style options. 6 | repository: https://github.com/dart-lang/args 7 | 8 | topics: 9 | - cli 10 | 11 | environment: 12 | sdk: ^3.3.0 13 | 14 | dev_dependencies: 15 | dart_flutter_team_lints: ^3.0.0 16 | test: ^1.16.0 17 | -------------------------------------------------------------------------------- /lib/args.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | export 'src/arg_parser.dart' show ArgParser; 6 | export 'src/arg_parser_exception.dart' show ArgParserException; 7 | export 'src/arg_results.dart' show ArgResults; 8 | export 'src/option.dart' show Option, OptionType; 9 | -------------------------------------------------------------------------------- /example/arg_parser/pubspec.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | # for details. All rights reserved. Use of this source code is governed by a 3 | # BSD-style license that can be found in the LICENSE file. 4 | 5 | name: arg_parser_example 6 | version: 1.0.0 7 | description: An example of using ArgParser 8 | publish_to: 'none' 9 | environment: 10 | sdk: '>=2.14.0 <3.0.0' 11 | dependencies: 12 | args: 13 | path: ../.. 14 | -------------------------------------------------------------------------------- /example/command_runner/pubspec.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | # for details. All rights reserved. Use of this source code is governed by a 3 | # BSD-style license that can be found in the LICENSE file. 4 | 5 | name: command_runner_example 6 | version: 1.0.0 7 | description: An example of using CommandRunner 8 | publish_to: 'none' 9 | environment: 10 | sdk: '>=2.14.0 <3.0.0' 11 | dependencies: 12 | args: 13 | path: ../.. 14 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/language/analysis-options 2 | 3 | include: package:dart_flutter_team_lints/analysis_options.yaml 4 | 5 | linter: 6 | rules: 7 | - avoid_unused_constructor_parameters 8 | - cancel_subscriptions 9 | - literal_only_boolean_expressions 10 | - missing_whitespace_between_adjacent_strings 11 | - no_adjacent_strings_in_list 12 | - no_runtimeType_toString 13 | - package_api_docs 14 | - unnecessary_await_in_return 15 | -------------------------------------------------------------------------------- /lib/src/usage_exception.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | class UsageException implements Exception { 6 | final String message; 7 | final String usage; 8 | 9 | UsageException(this.message, this.usage); 10 | 11 | @override 12 | String toString() => '$message\n\n$usage'; 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/arg_parser_exception.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// An exception thrown by `ArgParser`. 6 | class ArgParserException extends FormatException { 7 | /// The command(s) that were parsed before discovering the error. 8 | /// 9 | /// This will be empty if the error was on the root parser. 10 | final List commands; 11 | 12 | /// The name of the argument that was being parsed when the error was 13 | /// discovered. 14 | final String? argumentName; 15 | 16 | ArgParserException(super.message, 17 | [Iterable? commands, 18 | this.argumentName, 19 | super.source, 20 | super.offset]) 21 | : commands = commands == null ? const [] : List.unmodifiable(commands); 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013, the Dart project authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /lib/src/help_command.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import '../command_runner.dart'; 6 | 7 | /// The built-in help command that's added to every [CommandRunner]. 8 | /// 9 | /// This command displays help information for the various subcommands. 10 | class HelpCommand extends Command { 11 | @override 12 | final name = 'help'; 13 | 14 | @override 15 | String get description => 16 | 'Display help information for ${runner!.executableName}.'; 17 | 18 | @override 19 | String get invocation => '${runner!.executableName} help [command]'; 20 | 21 | @override 22 | bool get hidden => true; 23 | 24 | @override 25 | Null run() { 26 | // Show the default help if no command was specified. 27 | if (argResults!.rest.isEmpty) { 28 | runner!.printUsage(); 29 | return; 30 | } 31 | 32 | // Walk the command tree to show help for the selected command or 33 | // subcommand. 34 | var commands = runner!.commands; 35 | Command? command; 36 | var commandString = runner!.executableName; 37 | 38 | for (var name in argResults!.rest) { 39 | if (commands.isEmpty) { 40 | command!.usageException( 41 | 'Command "$commandString" does not expect a subcommand.'); 42 | } 43 | 44 | if (commands[name] == null) { 45 | if (command == null) { 46 | runner!.usageException('Could not find a command named "$name".'); 47 | } 48 | 49 | command.usageException( 50 | 'Could not find a subcommand named "$name" for "$commandString".'); 51 | } 52 | 53 | command = commands[name]; 54 | commands = command!.subcommands; 55 | commandString += ' $name'; 56 | } 57 | 58 | command!.printUsage(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | # Run CI on pushes to the main branch, and on PRs against main. 5 | push: 6 | branches: [ main ] 7 | pull_request: 8 | branches: [ main ] 9 | schedule: 10 | - cron: "0 0 * * 0" 11 | env: 12 | PUB_ENVIRONMENT: bot.github 13 | 14 | jobs: 15 | # Check code formatting and static analysis on a single OS (linux) 16 | # against Dart dev. 17 | analyze: 18 | runs-on: ubuntu-latest 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | sdk: [dev] 23 | steps: 24 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 25 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 26 | with: 27 | sdk: ${{ matrix.sdk }} 28 | - id: install 29 | name: Install dependencies 30 | run: dart pub get 31 | - name: Check formatting 32 | if: always() && steps.install.outcome == 'success' 33 | run: dart format --output=none --set-exit-if-changed . 34 | - name: Analyze code 35 | run: dart analyze --fatal-infos 36 | if: always() && steps.install.outcome == 'success' 37 | 38 | # Run tests on a matrix consisting of three dimensions: 39 | # 1. OS: ubuntu-latest, (macos-latest, windows-latest) 40 | # 2. release channel: dev, (stable) 41 | test: 42 | needs: analyze 43 | runs-on: ${{ matrix.os }} 44 | strategy: 45 | fail-fast: false 46 | matrix: 47 | os: [ubuntu-latest] 48 | sdk: ['3.3', dev] 49 | steps: 50 | - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 51 | - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 52 | with: 53 | sdk: ${{ matrix.sdk }} 54 | - id: install 55 | name: Install dependencies 56 | run: dart pub get 57 | - name: Run VM tests 58 | run: dart test --platform vm 59 | if: always() && steps.install.outcome == 'success' 60 | - name: Run Chrome tests 61 | run: dart test --platform chrome 62 | if: always() && steps.install.outcome == 'success' 63 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement (CLA). You (or your employer) retain the copyright to your 10 | contribution; this simply gives us permission to use and redistribute your 11 | contributions as part of the project. Head over to 12 | to see your current agreements on file or 13 | to sign a new one. 14 | 15 | You generally only need to submit a CLA once, so if you've already submitted one 16 | (even if it was for a different project), you probably don't need to do it 17 | again. 18 | 19 | ## Code Reviews 20 | 21 | All submissions, including submissions by project members, require review. We 22 | use GitHub pull requests for this purpose. Consult 23 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 24 | information on using pull requests. 25 | 26 | ## Coding style 27 | 28 | The Dart source code in this repo follows the: 29 | 30 | * [Dart style guide](https://dart.dev/guides/language/effective-dart/style) 31 | 32 | You should familiarize yourself with those guidelines. 33 | 34 | ## File headers 35 | 36 | All files in the Dart project must start with the following header; if you add a 37 | new file please also add this. The year should be a single number stating the 38 | year the file was created (don't use a range like "2011-2012"). Additionally, if 39 | you edit an existing file, you shouldn't update the year. 40 | 41 | // Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file 42 | // for details. All rights reserved. Use of this source code is governed by a 43 | // BSD-style license that can be found in the LICENSE file. 44 | 45 | ## Publishing automation 46 | 47 | For information about our publishing automation and release process, see 48 | https://github.com/dart-lang/ecosystem/wiki/Publishing-automation. 49 | 50 | ## Community Guidelines 51 | 52 | This project follows 53 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 54 | 55 | We pledge to maintain an open and welcoming environment. For details, see our 56 | [code of conduct](https://dart.dev/code-of-conduct). 57 | -------------------------------------------------------------------------------- /test/parse_performance_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:args/args.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | void main() { 9 | group('ArgParser.parse() is fast', () { 10 | test('for short flags', () { 11 | _testParserPerformance(ArgParser()..addFlag('short', abbr: 's'), '-s'); 12 | }); 13 | 14 | test('for abbreviations', () { 15 | _testParserPerformance( 16 | ArgParser() 17 | ..addFlag('short', abbr: 's') 18 | ..addFlag('short2', abbr: 't') 19 | ..addFlag('short3', abbr: 'u') 20 | ..addFlag('short4', abbr: 'v'), 21 | '-stuv'); 22 | }); 23 | 24 | test('for long flags', () { 25 | _testParserPerformance(ArgParser()..addFlag('long-flag'), '--long-flag'); 26 | }); 27 | 28 | test('for long options with =', () { 29 | _testParserPerformance(ArgParser()..addOption('long-option-name'), 30 | '--long-option-name=long-option-value'); 31 | }); 32 | }); 33 | } 34 | 35 | /// Tests how quickly [parser] parses [string]. 36 | /// 37 | /// Checks that a 10x increase in arg count does not lead to greater than 30x 38 | /// increase in parse time. 39 | void _testParserPerformance(ArgParser parser, String string) { 40 | var baseSize = 50000; 41 | var baseList = List.generate(baseSize, (_) => string); 42 | 43 | var multiplier = 10; 44 | var largeList = List.generate(baseSize * multiplier, (_) => string); 45 | 46 | ArgResults baseAction() => parser.parse(baseList); 47 | ArgResults largeAction() => parser.parse(largeList); 48 | 49 | // Warm up JIT. 50 | baseAction(); 51 | largeAction(); 52 | 53 | var baseTime = _time(baseAction); 54 | var largeTime = _time(largeAction); 55 | 56 | print('Parsed $baseSize elements in ${baseTime}ms, ' 57 | '${baseSize * multiplier} elements in ${largeTime}ms.'); 58 | 59 | expect( 60 | largeTime, 61 | lessThan(baseTime * multiplier * 3), 62 | reason: 63 | 'Comparing large data set time ${largeTime}ms to small data set time ' 64 | '${baseTime}ms. Data set increased ${multiplier}x, time is allowed to ' 65 | 'increase up to ${multiplier * 3}x, but it increased ' 66 | '${largeTime ~/ baseTime}x.', 67 | ); 68 | } 69 | 70 | int _time(void Function() function) { 71 | var stopwatch = Stopwatch()..start(); 72 | function(); 73 | return stopwatch.elapsedMilliseconds; 74 | } 75 | -------------------------------------------------------------------------------- /test/allow_anything_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:args/args.dart'; 6 | import 'package:args/command_runner.dart'; 7 | import 'package:test/test.dart'; 8 | 9 | import 'test_utils.dart'; 10 | 11 | void main() { 12 | group('new ArgParser.allowAnything()', () { 13 | late ArgParser parser; 14 | setUp(() { 15 | parser = ArgParser.allowAnything(); 16 | }); 17 | 18 | test('exposes empty values', () { 19 | expect(parser.options, isEmpty); 20 | expect(parser.commands, isEmpty); 21 | expect(parser.allowTrailingOptions, isFalse); 22 | expect(parser.allowsAnything, isTrue); 23 | expect(parser.usage, isEmpty); 24 | expect(parser.findByAbbreviation('a'), isNull); 25 | }); 26 | 27 | test('mutation methods throw errors', () { 28 | expect(() => parser.addCommand('command'), throwsUnsupportedError); 29 | expect(() => parser.addFlag('flag'), throwsUnsupportedError); 30 | expect(() => parser.addOption('option'), throwsUnsupportedError); 31 | expect(() => parser.addSeparator('==='), throwsUnsupportedError); 32 | }); 33 | 34 | test('getDefault() throws an error', () { 35 | expect(() => parser.defaultFor('option'), throwsArgumentError); 36 | }); 37 | 38 | test('parses all values as rest arguments', () { 39 | var results = parser.parse(['--foo', '-abc', '--', 'bar']); 40 | expect(results.options, isEmpty); 41 | expect(results.rest, equals(['--foo', '-abc', '--', 'bar'])); 42 | expect(results.arguments, equals(['--foo', '-abc', '--', 'bar'])); 43 | expect(results.command, isNull); 44 | expect(results.name, isNull); 45 | }); 46 | 47 | test('works as a subcommand', () { 48 | var commandParser = ArgParser()..addCommand('command', parser); 49 | var results = 50 | commandParser.parse(['command', '--foo', '-abc', '--', 'bar']); 51 | expect(results.command!.options, isEmpty); 52 | expect(results.command!.rest, equals(['--foo', '-abc', '--', 'bar'])); 53 | expect( 54 | results.command!.arguments, equals(['--foo', '-abc', '--', 'bar'])); 55 | expect(results.command!.name, equals('command')); 56 | }); 57 | 58 | test('works as a subcommand in a CommandRunner', () async { 59 | var commandRunner = 60 | CommandRunner('command', 'Description of command'); 61 | var command = AllowAnythingCommand(); 62 | commandRunner.addCommand(command); 63 | 64 | await commandRunner.run([command.name, '--foo', '--bar', '-b', 'qux']); 65 | }); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /lib/src/allow_anything_parser.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:collection'; 6 | 7 | import 'arg_parser.dart'; 8 | import 'arg_results.dart'; 9 | import 'option.dart'; 10 | import 'parser.dart'; 11 | 12 | /// An ArgParser that treats *all input* as non-option arguments. 13 | class AllowAnythingParser implements ArgParser { 14 | @override 15 | Map get options => const {}; 16 | @override 17 | Map get commands => const {}; 18 | @override 19 | bool get allowTrailingOptions => false; 20 | @override 21 | bool get allowsAnything => true; 22 | @override 23 | int? get usageLineLength => null; 24 | 25 | @override 26 | ArgParser addCommand(String name, [ArgParser? parser]) { 27 | throw UnsupportedError( 28 | "ArgParser.allowAnything().addCommands() isn't supported."); 29 | } 30 | 31 | @override 32 | void addFlag(String name, 33 | {String? abbr, 34 | String? help, 35 | bool? defaultsTo = false, 36 | bool negatable = true, 37 | void Function(bool)? callback, 38 | bool hide = false, 39 | List aliases = const []}) { 40 | throw UnsupportedError( 41 | "ArgParser.allowAnything().addFlag() isn't supported."); 42 | } 43 | 44 | @override 45 | void addOption(String name, 46 | {String? abbr, 47 | String? help, 48 | String? valueHelp, 49 | Iterable? allowed, 50 | Map? allowedHelp, 51 | String? defaultsTo, 52 | void Function(String?)? callback, 53 | bool allowMultiple = false, 54 | bool? splitCommas, 55 | bool mandatory = false, 56 | bool hide = false, 57 | List aliases = const []}) { 58 | throw UnsupportedError( 59 | "ArgParser.allowAnything().addOption() isn't supported."); 60 | } 61 | 62 | @override 63 | void addMultiOption(String name, 64 | {String? abbr, 65 | String? help, 66 | String? valueHelp, 67 | Iterable? allowed, 68 | Map? allowedHelp, 69 | Iterable? defaultsTo, 70 | void Function(List)? callback, 71 | bool splitCommas = true, 72 | bool hide = false, 73 | List aliases = const []}) { 74 | throw UnsupportedError( 75 | "ArgParser.allowAnything().addMultiOption() isn't supported."); 76 | } 77 | 78 | @override 79 | void addSeparator(String text) { 80 | throw UnsupportedError( 81 | "ArgParser.allowAnything().addSeparator() isn't supported."); 82 | } 83 | 84 | @override 85 | ArgResults parse(Iterable args) => 86 | Parser(null, this, Queue.of(args)).parse(); 87 | 88 | @override 89 | String get usage => ''; 90 | 91 | @override 92 | dynamic defaultFor(String option) { 93 | throw ArgumentError('No option named $option'); 94 | } 95 | 96 | @override 97 | dynamic getDefault(String option) { 98 | throw ArgumentError('No option named $option'); 99 | } 100 | 101 | @override 102 | Option? findByAbbreviation(String abbr) => null; 103 | 104 | @override 105 | Option? findByNameOrAlias(String name) => null; 106 | } 107 | -------------------------------------------------------------------------------- /test/trailing_options_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:args/args.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | import 'test_utils.dart'; 9 | 10 | void main() { 11 | test('allowTrailingOptions defaults to true', () { 12 | var parser = ArgParser(); 13 | expect(parser.allowTrailingOptions, isTrue); 14 | }); 15 | 16 | group('when trailing options are allowed', () { 17 | late ArgParser parser; 18 | setUp(() { 19 | parser = ArgParser(allowTrailingOptions: true); 20 | }); 21 | 22 | void expectThrows(List args, String arg) { 23 | throwsFormat(parser, args, reason: 'with allowTrailingOptions: true'); 24 | } 25 | 26 | test('collects non-options in rest', () { 27 | parser.addFlag('flag'); 28 | parser.addOption('opt', abbr: 'o'); 29 | var results = parser.parse(['a', '--flag', 'b', '-o', 'value', 'c']); 30 | expect(results['flag'], isTrue); 31 | expect(results['opt'], equals('value')); 32 | expect(results.rest, equals(['a', 'b', 'c'])); 33 | }); 34 | 35 | test('stops parsing options at "--"', () { 36 | parser.addFlag('flag'); 37 | parser.addOption('opt', abbr: 'o'); 38 | var results = parser.parse(['a', '--flag', '--', '-ovalue', 'c']); 39 | expect(results['flag'], isTrue); 40 | expect(results.rest, equals(['a', '-ovalue', 'c'])); 41 | }); 42 | 43 | test('only consumes first "--"', () { 44 | parser.addFlag('flag', abbr: 'f'); 45 | parser.addOption('opt', abbr: 'o'); 46 | var results = parser.parse(['a', '--', 'b', '--', 'c']); 47 | expect(results.rest, equals(['a', 'b', '--', 'c'])); 48 | }); 49 | 50 | test('parses a trailing flag', () { 51 | parser.addFlag('flag'); 52 | var results = parser.parse(['arg', '--flag']); 53 | 54 | expect(results['flag'], isTrue); 55 | expect(results.rest, equals(['arg'])); 56 | }); 57 | 58 | test('throws on a trailing option missing its value', () { 59 | parser.addOption('opt'); 60 | expectThrows(['arg', '--opt'], '--opt'); 61 | }); 62 | 63 | test('parses a trailing option', () { 64 | parser.addOption('opt'); 65 | var results = parser.parse(['arg', '--opt', 'v']); 66 | expect(results['opt'], equals('v')); 67 | expect(results.rest, equals(['arg'])); 68 | }); 69 | 70 | test('throws on a trailing unknown flag', () { 71 | expectThrows(['arg', '--xflag'], '--xflag'); 72 | }); 73 | 74 | test('throws on a trailing unknown option and value', () { 75 | expectThrows(['arg', '--xopt', 'v'], '--xopt'); 76 | }); 77 | 78 | test('throws on a command', () { 79 | parser.addCommand('com'); 80 | expectThrows(['arg', 'com'], 'com'); 81 | }); 82 | }); 83 | 84 | test("uses the innermost command's trailing options behavior", () { 85 | var parser = ArgParser(allowTrailingOptions: true); 86 | parser.addFlag('flag', abbr: 'f'); 87 | var command = 88 | parser.addCommand('cmd', ArgParser(allowTrailingOptions: false)); 89 | command.addFlag('verbose', abbr: 'v'); 90 | 91 | var results = parser.parse(['a', '-f', 'b']); 92 | expect(results['flag'], isTrue); 93 | expect(results.rest, equals(['a', 'b'])); 94 | 95 | results = parser.parse(['cmd', '-f', 'a', '-v', '--unknown']); 96 | expect(results['flag'], isTrue); // Not trailing. 97 | expect(results.command!['verbose'], isFalse); 98 | expect(results.command!.rest, equals(['a', '-v', '--unknown'])); 99 | }); 100 | } 101 | -------------------------------------------------------------------------------- /example/command_runner/draw.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:async'; 6 | import 'dart:math'; 7 | 8 | import 'package:args/command_runner.dart'; 9 | 10 | void main(List args) async { 11 | final runner = CommandRunner('draw', 'Draws shapes') 12 | ..addCommand(SquareCommand()) 13 | ..addCommand(CircleCommand()) 14 | ..addCommand(TriangleCommand()); 15 | runner.argParser.addOption('char', help: 'The character to use for drawing'); 16 | final output = await runner.run(args); 17 | print(output); 18 | } 19 | 20 | class SquareCommand extends Command { 21 | SquareCommand() { 22 | argParser.addOption('size', help: 'Size of the square'); 23 | } 24 | 25 | @override 26 | String get name => 'square'; 27 | 28 | @override 29 | String get description => 'Draws a square'; 30 | 31 | @override 32 | List get aliases => ['s']; 33 | 34 | @override 35 | FutureOr? run() { 36 | final size = int.parse(argResults?.option('size') ?? '20'); 37 | final char = globalResults?.option('char')?[0] ?? '#'; 38 | return draw(size, size, char, (x, y) => true); 39 | } 40 | } 41 | 42 | class CircleCommand extends Command { 43 | CircleCommand() { 44 | argParser.addOption('radius', help: 'Radius of the circle'); 45 | } 46 | 47 | @override 48 | String get name => 'circle'; 49 | 50 | @override 51 | String get description => 'Draws a circle'; 52 | 53 | @override 54 | List get aliases => ['c']; 55 | 56 | @override 57 | FutureOr? run() { 58 | final size = 2 * int.parse(argResults?.option('radius') ?? '10'); 59 | final char = globalResults?.option('char')?[0] ?? '#'; 60 | return draw(size, size, char, (x, y) => x * x + y * y < 1); 61 | } 62 | } 63 | 64 | class TriangleCommand extends Command { 65 | TriangleCommand() { 66 | addSubcommand(EquilateralTriangleCommand()); 67 | addSubcommand(IsoscelesTriangleCommand()); 68 | } 69 | 70 | @override 71 | String get name => 'triangle'; 72 | 73 | @override 74 | String get description => 'Draws a triangle'; 75 | 76 | @override 77 | List get aliases => ['t']; 78 | } 79 | 80 | class EquilateralTriangleCommand extends Command { 81 | EquilateralTriangleCommand() { 82 | argParser.addOption('size', help: 'Size of the triangle'); 83 | } 84 | 85 | @override 86 | String get name => 'equilateral'; 87 | 88 | @override 89 | String get description => 'Draws an equilateral triangle'; 90 | 91 | @override 92 | List get aliases => ['e']; 93 | 94 | @override 95 | FutureOr? run() { 96 | final size = int.parse(argResults?.option('size') ?? '20'); 97 | final char = globalResults?.option('char')?[0] ?? '#'; 98 | return drawTriangle(size, size * sqrt(3) ~/ 2, char); 99 | } 100 | } 101 | 102 | class IsoscelesTriangleCommand extends Command { 103 | IsoscelesTriangleCommand() { 104 | argParser.addOption('width', help: 'Width of the triangle'); 105 | argParser.addOption('height', help: 'Height of the triangle'); 106 | } 107 | 108 | @override 109 | String get name => 'isosceles'; 110 | 111 | @override 112 | String get description => 'Draws an isosceles triangle'; 113 | 114 | @override 115 | List get aliases => ['i']; 116 | 117 | @override 118 | FutureOr? run() { 119 | final width = int.parse(argResults?.option('width') ?? '50'); 120 | final height = int.parse(argResults?.option('height') ?? '10'); 121 | final char = globalResults?.option('char')?[0] ?? '#'; 122 | return drawTriangle(width, height, char); 123 | } 124 | } 125 | 126 | String draw( 127 | int width, int height, String char, bool Function(double, double) pixel) { 128 | final out = StringBuffer(); 129 | for (var y = 0; y <= height; ++y) { 130 | final ty = 2 * y / height - 1; 131 | for (var x = 0; x <= width; ++x) { 132 | final tx = 2 * x / width - 1; 133 | out.write(pixel(tx, ty) ? char : ' '); 134 | } 135 | out.write('\n'); 136 | } 137 | return out.toString(); 138 | } 139 | 140 | String drawTriangle(int width, int height, String char) { 141 | return draw(width, height, char, (x, y) => x.abs() <= (1 + y) / 2); 142 | } 143 | -------------------------------------------------------------------------------- /test/command_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:args/command_runner.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | import 'test_utils.dart'; 9 | 10 | void main() { 11 | late FooCommand foo; 12 | setUp(() { 13 | foo = FooCommand(); 14 | 15 | // Make sure [Command.runner] is set up. 16 | CommandRunner('test', 'A test command runner.').addCommand(foo); 17 | }); 18 | 19 | group('.invocation has a sane default', () { 20 | test('without subcommands', () { 21 | expect(foo.invocation, equals('test foo [arguments]')); 22 | }); 23 | 24 | test('with subcommands', () { 25 | foo.addSubcommand(AsyncCommand()); 26 | expect(foo.invocation, equals('test foo [arguments]')); 27 | }); 28 | 29 | test('for a subcommand', () { 30 | var async = AsyncCommand(); 31 | foo.addSubcommand(async); 32 | 33 | expect(async.invocation, equals('test foo async [arguments]')); 34 | }); 35 | }); 36 | 37 | group('.usage', () { 38 | test('returns the usage string', () { 39 | expect(foo.usage, equals(''' 40 | Set a value. 41 | 42 | Usage: test foo [arguments] 43 | -h, --help Print this usage information. 44 | 45 | Run "test help" to see global options.''')); 46 | }); 47 | 48 | test('contains custom options', () { 49 | foo.argParser.addFlag('flag', help: 'Do something.'); 50 | 51 | expect(foo.usage, equals(''' 52 | Set a value. 53 | 54 | Usage: test foo [arguments] 55 | -h, --help Print this usage information. 56 | --[no-]flag Do something. 57 | 58 | Run "test help" to see global options.''')); 59 | }); 60 | 61 | test("doesn't print hidden subcommands", () { 62 | foo.addSubcommand(AsyncCommand()); 63 | foo.addSubcommand(HiddenCommand()); 64 | 65 | expect(foo.usage, equals(''' 66 | Set a value. 67 | 68 | Usage: test foo [arguments] 69 | -h, --help Print this usage information. 70 | 71 | Available subcommands: 72 | async Set a value asynchronously. 73 | 74 | Run "test help" to see global options.''')); 75 | }); 76 | 77 | test("doesn't print subcommand aliases", () { 78 | foo.addSubcommand(AliasedCommand()); 79 | 80 | expect(foo.usage, equals(''' 81 | Set a value. 82 | 83 | Usage: test foo [arguments] 84 | -h, --help Print this usage information. 85 | 86 | Available subcommands: 87 | aliased Set a value. 88 | 89 | Run "test help" to see global options.''')); 90 | }); 91 | 92 | test('wraps long command descriptions with subcommands', () { 93 | var wrapping = WrappingCommand(); 94 | 95 | // Make sure [Command.runner] is set up. 96 | CommandRunner('longtest', 'A long-lined test command runner.') 97 | .addCommand(wrapping); 98 | 99 | wrapping.addSubcommand(LongCommand()); 100 | expect(wrapping.usage, equals(''' 101 | This command overrides the argParser so 102 | that it will wrap long lines. 103 | 104 | Usage: longtest wrapping 105 | [arguments] 106 | -h, --help Print this usage 107 | information. 108 | 109 | Available subcommands: 110 | long This command has a long 111 | description that needs to be 112 | wrapped sometimes. 113 | 114 | Run "longtest help" to see global 115 | options.''')); 116 | }); 117 | 118 | test('wraps long command descriptions', () { 119 | var longCommand = LongCommand(); 120 | 121 | // Make sure [Command.runner] is set up. 122 | CommandRunner('longtest', 'A long-lined test command runner.') 123 | .addCommand(longCommand); 124 | 125 | expect(longCommand.usage, equals(''' 126 | This command has a long description that 127 | needs to be wrapped sometimes. 128 | It has embedded newlines, 129 | and indented lines that also need 130 | to be wrapped and have their 131 | indentation preserved. 132 | 0123456789012345678901234567890123456789 133 | 0123456789012345678901234567890123456789 134 | 01234567890123456789 135 | 136 | Usage: longtest long [arguments] 137 | -h, --help Print this usage 138 | information. 139 | 140 | Run "longtest help" to see global 141 | options.''')); 142 | }); 143 | }); 144 | 145 | test('usageException splits up the message and usage', () { 146 | expect( 147 | () => foo.usageException('message'), throwsUsageException('message', ''' 148 | Usage: test foo [arguments] 149 | -h, --help Print this usage information. 150 | 151 | Run "test help" to see global options.''')); 152 | }); 153 | 154 | test('considers a command hidden if all its subcommands are hidden', () { 155 | foo.addSubcommand(HiddenCommand()); 156 | expect(foo.hidden, isTrue); 157 | }); 158 | } 159 | -------------------------------------------------------------------------------- /lib/src/arg_results.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:collection'; 6 | 7 | import 'arg_parser.dart'; 8 | 9 | /// Creates a new [ArgResults]. 10 | /// 11 | /// Since [ArgResults] doesn't have a public constructor, this lets [ArgParser] 12 | /// get to it. This function isn't exported to the public API of the package. 13 | ArgResults newArgResults( 14 | ArgParser parser, 15 | Map parsed, 16 | String? name, 17 | ArgResults? command, 18 | List rest, 19 | List arguments) { 20 | return ArgResults._(parser, parsed, name, command, rest, arguments); 21 | } 22 | 23 | /// The results of parsing a series of command line arguments using 24 | /// [ArgParser.parse]. 25 | /// 26 | /// Includes the parsed options and any remaining unparsed command line 27 | /// arguments. 28 | class ArgResults { 29 | /// The [ArgParser] whose options were parsed for these results. 30 | final ArgParser _parser; 31 | 32 | /// The option values that were parsed from arguments. 33 | final Map _parsed; 34 | 35 | /// The name of the command for which these options are parsed, or `null` if 36 | /// these are the top-level results. 37 | final String? name; 38 | 39 | /// The command that was selected, or `null` if none was. 40 | /// 41 | /// This will contain the options that were selected for that command. 42 | final ArgResults? command; 43 | 44 | /// The remaining command-line arguments that were not parsed as options or 45 | /// flags. 46 | /// 47 | /// If `--` was used to separate the options from the remaining arguments, 48 | /// it will not be included in this list unless parsing stopped before the 49 | /// `--` was reached. 50 | final List rest; 51 | 52 | /// The original arguments that were parsed. 53 | final List arguments; 54 | 55 | ArgResults._(this._parser, this._parsed, this.name, this.command, 56 | List rest, List arguments) 57 | : rest = UnmodifiableListView(rest), 58 | arguments = UnmodifiableListView(arguments); 59 | 60 | /// Returns the parsed or default command-line option named [name]. 61 | /// 62 | /// [name] must be a valid option name in the parser. 63 | /// 64 | /// > [!Note] 65 | /// > Callers should prefer using the more strongly typed methods - [flag] for 66 | /// > flags, [option] for options, and [multiOption] for multi-options. 67 | dynamic operator [](String name) { 68 | if (!_parser.options.containsKey(name)) { 69 | throw ArgumentError('Could not find an option named "--$name".'); 70 | } 71 | 72 | final option = _parser.options[name]!; 73 | if (option.mandatory && !_parsed.containsKey(name)) { 74 | throw ArgumentError('Option $name is mandatory.'); 75 | } 76 | 77 | return option.valueOrDefault(_parsed[name]); 78 | } 79 | 80 | /// Returns the parsed or default command-line flag named [name]. 81 | /// 82 | /// [name] must be a valid flag name in the parser. 83 | bool flag(String name) { 84 | var option = _parser.options[name]; 85 | if (option == null) { 86 | throw ArgumentError('Could not find an option named "--$name".'); 87 | } 88 | if (!option.isFlag) { 89 | throw ArgumentError('"$name" is not a flag.'); 90 | } 91 | return option.valueOrDefault(_parsed[name]) as bool; 92 | } 93 | 94 | /// Returns the parsed or default command-line option named [name]. 95 | /// 96 | /// [name] must be a valid option name in the parser. 97 | String? option(String name) { 98 | var option = _parser.options[name]; 99 | if (option == null) { 100 | throw ArgumentError('Could not find an option named "--$name".'); 101 | } 102 | if (!option.isSingle) { 103 | throw ArgumentError('"$name" is a multi-option.'); 104 | } 105 | return option.valueOrDefault(_parsed[name]) as String?; 106 | } 107 | 108 | /// Returns the list of parsed (or default) command-line options for [name]. 109 | /// 110 | /// [name] must be a valid option name in the parser. 111 | List multiOption(String name) { 112 | var option = _parser.options[name]; 113 | if (option == null) { 114 | throw ArgumentError('Could not find an option named "--$name".'); 115 | } 116 | if (!option.isMultiple) { 117 | throw ArgumentError('"$name" is not a multi-option.'); 118 | } 119 | return option.valueOrDefault(_parsed[name]) as List; 120 | } 121 | 122 | /// The names of the available options. 123 | /// 124 | /// Includes the options whose values were parsed or that have defaults. 125 | /// Options that weren't present and have no default are omitted. 126 | Iterable get options { 127 | var result = _parsed.keys.toSet(); 128 | 129 | // Include the options that have defaults. 130 | _parser.options.forEach((name, option) { 131 | if (option.defaultsTo != null) result.add(name); 132 | }); 133 | 134 | return result; 135 | } 136 | 137 | /// Returns `true` if the option with [name] was parsed from an actual 138 | /// argument. 139 | /// 140 | /// Returns `false` if it wasn't provided and the default value or no default 141 | /// value would be used instead. 142 | /// 143 | /// [name] must be a valid option name in the parser. 144 | bool wasParsed(String name) { 145 | if (!_parser.options.containsKey(name)) { 146 | throw ArgumentError('Could not find an option named "--$name".'); 147 | } 148 | 149 | return _parsed.containsKey(name); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | import 'dart:math' as math; 5 | 6 | /// Pads [source] to [length] by adding spaces at the end. 7 | String padRight(String source, int length) => 8 | source + ' ' * (length - source.length); 9 | 10 | /// Wraps a block of text into lines no longer than [length]. 11 | /// 12 | /// Tries to split at whitespace, but if that's not good enough to keep it 13 | /// under the limit, then it splits in the middle of a word. 14 | /// 15 | /// Preserves indentation (leading whitespace) for each line (delimited by '\n') 16 | /// in the input, and indents wrapped lines the same amount. 17 | /// 18 | /// If [hangingIndent] is supplied, then that many spaces are added to each 19 | /// line, except for the first line. This is useful for flowing text with a 20 | /// heading prefix (e.g. "Usage: "): 21 | /// 22 | /// ```dart 23 | /// var prefix = "Usage: "; 24 | /// print( 25 | /// prefix + wrapText(invocation, hangingIndent: prefix.length, length: 40), 26 | /// ); 27 | /// ``` 28 | /// 29 | /// yields: 30 | /// ``` 31 | /// Usage: app main_command 32 | /// [arguments] 33 | /// ``` 34 | /// 35 | /// If [length] is not specified, then no wrapping occurs, and the original 36 | /// [text] is returned unchanged. 37 | String wrapText(String text, {int? length, int? hangingIndent}) { 38 | if (length == null) return text; 39 | hangingIndent ??= 0; 40 | var splitText = text.split('\n'); 41 | var result = []; 42 | for (var line in splitText) { 43 | var trimmedText = line.trimLeft(); 44 | final leadingWhitespace = 45 | line.substring(0, line.length - trimmedText.length); 46 | List notIndented; 47 | if (hangingIndent != 0) { 48 | // When we have a hanging indent, we want to wrap the first line at one 49 | // width, and the rest at another (offset by hangingIndent), so we wrap 50 | // them twice and recombine. 51 | var firstLineWrap = wrapTextAsLines(trimmedText, 52 | length: length - leadingWhitespace.length); 53 | notIndented = [firstLineWrap.removeAt(0)]; 54 | trimmedText = trimmedText.substring(notIndented[0].length).trimLeft(); 55 | if (firstLineWrap.isNotEmpty) { 56 | notIndented.addAll(wrapTextAsLines(trimmedText, 57 | length: length - leadingWhitespace.length - hangingIndent)); 58 | } 59 | } else { 60 | notIndented = wrapTextAsLines(trimmedText, 61 | length: length - leadingWhitespace.length); 62 | } 63 | String? hangingIndentString; 64 | result.addAll(notIndented.map((String line) { 65 | // Don't return any lines with just whitespace on them. 66 | if (line.isEmpty) return ''; 67 | var result = '${hangingIndentString ?? ''}$leadingWhitespace$line'; 68 | hangingIndentString ??= ' ' * hangingIndent!; 69 | return result; 70 | })); 71 | } 72 | return result.join('\n'); 73 | } 74 | 75 | /// Wraps a block of text into lines no longer than [length], 76 | /// starting at the [start] column, and returns the result as a list of strings. 77 | /// 78 | /// Tries to split at whitespace, but if that's not good enough to keep it 79 | /// under the limit, then splits in the middle of a word. Preserves embedded 80 | /// newlines, but not indentation (it trims whitespace from each line). 81 | /// 82 | /// If [length] is not specified, then no wrapping occurs, and the original 83 | /// [text] is returned after splitting it on newlines. Whitespace is not trimmed 84 | /// in this case. 85 | List wrapTextAsLines(String text, {int start = 0, int? length}) { 86 | assert(start >= 0); 87 | 88 | /// Returns true if the code unit at [index] in [text] is a whitespace 89 | /// character. 90 | /// 91 | /// Based on: https://en.wikipedia.org/wiki/Whitespace_character#Unicode 92 | bool isWhitespace(String text, int index) { 93 | var rune = text.codeUnitAt(index); 94 | return rune >= 0x0009 && rune <= 0x000D || 95 | rune == 0x0020 || 96 | rune == 0x0085 || 97 | rune == 0x1680 || 98 | rune == 0x180E || 99 | rune >= 0x2000 && rune <= 0x200A || 100 | rune == 0x2028 || 101 | rune == 0x2029 || 102 | rune == 0x202F || 103 | rune == 0x205F || 104 | rune == 0x3000 || 105 | rune == 0xFEFF; 106 | } 107 | 108 | if (length == null) return text.split('\n'); 109 | 110 | var result = []; 111 | var effectiveLength = math.max(length - start, 10); 112 | for (var line in text.split('\n')) { 113 | line = line.trim(); 114 | if (line.length <= effectiveLength) { 115 | result.add(line); 116 | continue; 117 | } 118 | 119 | var currentLineStart = 0; 120 | int? lastWhitespace; 121 | for (var i = 0; i < line.length; ++i) { 122 | if (isWhitespace(line, i)) lastWhitespace = i; 123 | 124 | if (i - currentLineStart >= effectiveLength) { 125 | // Back up to the last whitespace, unless there wasn't any, in which 126 | // case we just split where we are. 127 | if (lastWhitespace != null) i = lastWhitespace; 128 | 129 | result.add(line.substring(currentLineStart, i).trim()); 130 | 131 | // Skip any intervening whitespace. 132 | while (isWhitespace(line, i) && i < line.length) { 133 | i++; 134 | } 135 | 136 | currentLineStart = i; 137 | lastWhitespace = null; 138 | } 139 | } 140 | result.add(line.substring(currentLineStart).trim()); 141 | } 142 | return result; 143 | } 144 | -------------------------------------------------------------------------------- /example/arg_parser/example.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// This is an example of converting the args in test.dart to use this API. 6 | /// It shows what it looks like to build an [ArgParser] and then, when the code 7 | /// is run, demonstrates what the generated usage text looks like. 8 | library; 9 | 10 | import 'dart:io'; 11 | 12 | import 'package:args/args.dart'; 13 | 14 | void main() { 15 | var parser = ArgParser(); 16 | 17 | parser.addSeparator('===== Platform'); 18 | 19 | parser.addOption('compiler', 20 | abbr: 'c', 21 | defaultsTo: 'none', 22 | help: 'Specify any compilation step (if needed).', 23 | allowed: [ 24 | 'none', 25 | 'dart2js', 26 | 'dartc' 27 | ], 28 | allowedHelp: { 29 | 'none': 'Do not compile the Dart code (run native Dart code on the' 30 | ' VM).\n(only valid with the following runtimes: vm, drt)', 31 | 'dart2js': 'Compile dart code to JavaScript by running dart2js.\n' 32 | '(only valid with the following runtimes: d8, drt, chrome\n' 33 | 'safari, ie, firefox, opera, none (compile only))', 34 | 'dartc': 'Perform static analysis on Dart code by running dartc.\n' 35 | '(only valid with the following runtimes: none)', 36 | }); 37 | 38 | parser.addOption('runtime', 39 | abbr: 'r', 40 | defaultsTo: 'vm', 41 | help: 'Where the tests should be run.', 42 | allowed: [ 43 | 'vm', 44 | 'd8', 45 | 'drt', 46 | 'dartium', 47 | 'ff', 48 | 'firefox', 49 | 'chrome', 50 | 'safari', 51 | 'ie', 52 | 'opera', 53 | 'none' 54 | ], 55 | allowedHelp: { 56 | 'vm': 'Run Dart code on the standalone dart vm.', 57 | 'd8': 'Run JavaScript from the command line using v8.', 58 | 'drt': 'Run Dart or JavaScript in the headless version of Chrome,\n' 59 | 'content shell.', 60 | 'dartium': 'Run Dart or JavaScript in Dartium.', 61 | 'ff': 'Run JavaScript in Firefox', 62 | 'chrome': 'Run JavaScript in Chrome', 63 | 'safari': 'Run JavaScript in Safari', 64 | 'ie': 'Run JavaScript in Internet Explorer', 65 | 'opera': 'Run JavaScript in Opera', 66 | 'none': 'No runtime, compile only (for example, used for dartc static\n' 67 | 'analysis tests).', 68 | }); 69 | 70 | parser.addOption('arch', 71 | abbr: 'a', 72 | defaultsTo: 'ia32', 73 | help: 'The architecture to run tests for', 74 | allowed: ['all', 'ia32', 'x64', 'simarm']); 75 | 76 | parser.addOption('system', 77 | abbr: 's', 78 | defaultsTo: Platform.operatingSystem, 79 | help: 'The operating system to run tests on', 80 | allowed: ['linux', 'macos', 'windows']); 81 | 82 | parser.addSeparator('===== Runtime'); 83 | 84 | parser.addOption('mode', 85 | abbr: 'm', 86 | defaultsTo: 'debug', 87 | help: 'Mode in which to run the tests', 88 | allowed: ['all', 'debug', 'release']); 89 | 90 | parser.addFlag('checked', 91 | defaultsTo: false, help: 'Run tests in checked mode'); 92 | 93 | parser.addFlag('host-checked', 94 | defaultsTo: false, help: 'Run compiler in checked mode'); 95 | 96 | parser.addOption('timeout', abbr: 't', help: 'Timeout in seconds'); 97 | 98 | parser.addOption('tasks', 99 | abbr: 'j', 100 | defaultsTo: Platform.numberOfProcessors.toString(), 101 | help: 'The number of parallel tasks to run'); 102 | 103 | parser.addOption('shards', 104 | defaultsTo: '1', 105 | help: 'The number of instances that the tests will be sharded over'); 106 | 107 | parser.addOption('shard', 108 | defaultsTo: '1', 109 | help: 'The index of this instance when running in sharded mode'); 110 | 111 | parser.addFlag('valgrind', 112 | defaultsTo: false, help: 'Run tests through valgrind'); 113 | 114 | parser.addSeparator('===== Output'); 115 | 116 | parser.addOption('progress', 117 | abbr: 'p', 118 | defaultsTo: 'compact', 119 | help: 'Progress indication mode', 120 | allowed: [ 121 | 'compact', 122 | 'color', 123 | 'line', 124 | 'verbose', 125 | 'silent', 126 | 'status', 127 | 'buildbot' 128 | ]); 129 | 130 | parser.addFlag('report', 131 | defaultsTo: false, 132 | help: 'Print a summary report of the number of tests, by expectation'); 133 | 134 | parser.addFlag('verbose', 135 | abbr: 'v', defaultsTo: false, help: 'Verbose output'); 136 | 137 | parser.addFlag('list', 138 | defaultsTo: false, help: 'List tests only, do not run them'); 139 | 140 | parser.addFlag('time', 141 | help: 'Print timing information after running tests', defaultsTo: false); 142 | 143 | parser.addFlag('batch', 144 | abbr: 'b', help: 'Run browser tests in batch mode', defaultsTo: true); 145 | 146 | parser.addSeparator('===== Miscellaneous'); 147 | 148 | parser.addFlag('keep-generated-tests', 149 | defaultsTo: false, 150 | help: 'Keep the generated files in the temporary directory'); 151 | 152 | parser.addOption('special-command', help: """ 153 | Special command support. Wraps the command line in 154 | a special command. The special command should contain 155 | an '@' character which will be replaced by the normal 156 | command. 157 | 158 | For example if the normal command that will be executed 159 | is 'dart file.dart' and you specify special command 160 | 'python -u valgrind.py @ suffix' the final command will be 161 | 'python -u valgrind.py dart file.dart suffix'"""); 162 | 163 | parser.addOption('dart', help: 'Path to dart executable'); 164 | parser.addOption('drt', help: 'Path to content shell executable'); 165 | parser.addOption('dartium', help: 'Path to Dartium Chrome executable'); 166 | parser.addOption('mandatory', help: 'A mandatory option', mandatory: true); 167 | 168 | print(parser.usage); 169 | } 170 | -------------------------------------------------------------------------------- /lib/src/option.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | /// Creates a new [Option]. 6 | /// 7 | /// Since [Option] doesn't have a public constructor, this lets `ArgParser` 8 | /// get to it. This function isn't exported to the public API of the package. 9 | Option newOption( 10 | String name, 11 | String? abbr, 12 | String? help, 13 | String? valueHelp, 14 | Iterable? allowed, 15 | Map? allowedHelp, 16 | Object? defaultsTo, 17 | Function? callback, 18 | OptionType type, 19 | {bool? negatable, 20 | bool? splitCommas, 21 | bool mandatory = false, 22 | bool hide = false, 23 | List aliases = const []}) { 24 | return Option._(name, abbr, help, valueHelp, allowed, allowedHelp, defaultsTo, 25 | callback, type, 26 | negatable: negatable, 27 | splitCommas: splitCommas, 28 | mandatory: mandatory, 29 | hide: hide, 30 | aliases: aliases); 31 | } 32 | 33 | /// A command-line option. 34 | /// 35 | /// This represents both boolean flags and options which take a value. 36 | class Option { 37 | /// The name of the option that the user passes as an argument. 38 | final String name; 39 | 40 | /// A single-character string that can be used as a shorthand for this option. 41 | /// 42 | /// For example, `abbr: "a"` will allow the user to pass `-a value` or 43 | /// `-avalue`. 44 | final String? abbr; 45 | 46 | /// A description of this option. 47 | final String? help; 48 | 49 | /// A name for the value this option takes. 50 | final String? valueHelp; 51 | 52 | /// A list of valid values for this option. 53 | final List? allowed; 54 | 55 | /// A map from values in [allowed] to documentation for those values. 56 | final Map? allowedHelp; 57 | 58 | /// The value this option will have if the user doesn't explicitly pass it. 59 | final dynamic defaultsTo; 60 | 61 | /// Whether this flag's value can be set to `false`. 62 | /// 63 | /// For example, if [name] is `flag`, the user can pass `--no-flag` to set its 64 | /// value to `false`. 65 | /// 66 | /// This is `null` unless [type] is [OptionType.flag]. 67 | final bool? negatable; 68 | 69 | /// The callback to invoke with the option's value when the option is parsed. 70 | final Function? callback; 71 | 72 | /// Whether this is a flag, a single value option, or a multi-value option. 73 | final OptionType type; 74 | 75 | /// Whether multiple values may be passed by writing `--option a,b` in 76 | /// addition to `--option a --option b`. 77 | final bool splitCommas; 78 | 79 | /// Whether this option must be provided for correct usage. 80 | final bool mandatory; 81 | 82 | /// Whether this option should be hidden from usage documentation. 83 | final bool hide; 84 | 85 | /// All aliases for [name]. 86 | final List aliases; 87 | 88 | /// Whether the option is boolean-valued flag. 89 | bool get isFlag => type == OptionType.flag; 90 | 91 | /// Whether the option takes a single value. 92 | bool get isSingle => type == OptionType.single; 93 | 94 | /// Whether the option allows multiple values. 95 | bool get isMultiple => type == OptionType.multiple; 96 | 97 | Option._( 98 | this.name, 99 | this.abbr, 100 | this.help, 101 | this.valueHelp, 102 | Iterable? allowed, 103 | Map? allowedHelp, 104 | this.defaultsTo, 105 | this.callback, 106 | this.type, 107 | {this.negatable, 108 | bool? splitCommas, 109 | this.mandatory = false, 110 | this.hide = false, 111 | this.aliases = const []}) 112 | : allowed = allowed == null ? null : List.unmodifiable(allowed), 113 | allowedHelp = 114 | allowedHelp == null ? null : Map.unmodifiable(allowedHelp), 115 | // If the user doesn't specify [splitCommas], it defaults to true for 116 | // multiple options. 117 | splitCommas = splitCommas ?? type == OptionType.multiple { 118 | if (name.isEmpty) { 119 | throw ArgumentError('Name cannot be empty.'); 120 | } else if (name.startsWith('-')) { 121 | throw ArgumentError('Name $name cannot start with "-".'); 122 | } 123 | 124 | // Ensure name does not contain any invalid characters. 125 | if (_invalidChars.hasMatch(name)) { 126 | throw ArgumentError('Name "$name" contains invalid characters.'); 127 | } 128 | 129 | var abbr = this.abbr; 130 | if (abbr != null) { 131 | if (abbr.length != 1) { 132 | throw ArgumentError('Abbreviation must be null or have length 1.'); 133 | } else if (abbr == '-') { 134 | throw ArgumentError('Abbreviation cannot be "-".'); 135 | } 136 | 137 | if (_invalidChars.hasMatch(abbr)) { 138 | throw ArgumentError('Abbreviation is an invalid character.'); 139 | } 140 | } 141 | } 142 | 143 | /// Returns [value] if non-`null`, otherwise returns the default value for 144 | /// this option. 145 | /// 146 | /// For single-valued options, it will be [defaultsTo] if set or `null` 147 | /// otherwise. For multiple-valued options, it will be an empty list or a 148 | /// list containing [defaultsTo] if set. 149 | dynamic valueOrDefault(Object? value) { 150 | if (value != null) return value; 151 | if (isMultiple) return defaultsTo ?? []; 152 | return defaultsTo; 153 | } 154 | 155 | @Deprecated('Use valueOrDefault instead.') 156 | dynamic getOrDefault(Object? value) => valueOrDefault(value); 157 | 158 | static final _invalidChars = RegExp(r'''[ \t\r\n"'\\/]'''); 159 | } 160 | 161 | /// What kinds of values an option accepts. 162 | class OptionType { 163 | /// An option that can only be `true` or `false`. 164 | /// 165 | /// The presence of the option name itself in the argument list means `true`. 166 | static const flag = OptionType._('OptionType.flag'); 167 | 168 | /// An option that takes a single value. 169 | /// 170 | /// Examples: 171 | /// 172 | /// --mode debug 173 | /// -mdebug 174 | /// --mode=debug 175 | /// 176 | /// If the option is passed more than once, the last one wins. 177 | static const single = OptionType._('OptionType.single'); 178 | 179 | /// An option that allows multiple values. 180 | /// 181 | /// Example: 182 | /// 183 | /// --output text --output xml 184 | /// 185 | /// In the parsed `ArgResults`, a multiple-valued option will always return 186 | /// a list, even if one or no values were passed. 187 | static const multiple = OptionType._('OptionType.multiple'); 188 | 189 | final String name; 190 | 191 | const OptionType._(this.name); 192 | } 193 | -------------------------------------------------------------------------------- /test/command_parse_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:args/args.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | import 'test_utils.dart'; 9 | 10 | void main() { 11 | group('ArgParser.addCommand()', () { 12 | test('creates a new ArgParser if none is given', () { 13 | var parser = ArgParser(); 14 | var command = parser.addCommand('install'); 15 | expect(parser.commands['install'], equals(command)); 16 | expect(command, const TypeMatcher()); 17 | }); 18 | 19 | test('uses the command parser if given one', () { 20 | var parser = ArgParser(); 21 | var command = ArgParser(); 22 | var result = parser.addCommand('install', command); 23 | expect(parser.commands['install'], equals(command)); 24 | expect(result, equals(command)); 25 | }); 26 | 27 | test('throws on a duplicate command name', () { 28 | var parser = ArgParser(); 29 | parser.addCommand('install'); 30 | throwsIllegalArg(() => parser.addCommand('install')); 31 | }); 32 | }); 33 | 34 | group('ArgParser.parse()', () { 35 | test('parses a command', () { 36 | var parser = ArgParser()..addCommand('install'); 37 | 38 | var args = parser.parse(['install']); 39 | 40 | expect(args.command!.name, equals('install')); 41 | expect(args.rest, isEmpty); 42 | }); 43 | 44 | test('parses a command option', () { 45 | var parser = ArgParser(); 46 | var command = parser.addCommand('install'); 47 | command.addOption('path'); 48 | 49 | var args = parser.parse(['install', '--path', 'some/path']); 50 | expect(args.command!['path'], equals('some/path')); 51 | }); 52 | 53 | test('parses a parent solo option before the command', () { 54 | var parser = ArgParser() 55 | ..addOption('mode', abbr: 'm') 56 | ..addCommand('install'); 57 | 58 | var args = parser.parse(['-m', 'debug', 'install']); 59 | expect(args['mode'], equals('debug')); 60 | expect(args.command!.name, equals('install')); 61 | }); 62 | 63 | test('parses a parent solo option after the command', () { 64 | var parser = ArgParser() 65 | ..addOption('mode', abbr: 'm') 66 | ..addCommand('install'); 67 | 68 | var args = parser.parse(['install', '-m', 'debug']); 69 | expect(args['mode'], equals('debug')); 70 | expect(args.command!.name, equals('install')); 71 | }); 72 | 73 | test('parses a parent option before the command', () { 74 | var parser = ArgParser() 75 | ..addFlag('verbose') 76 | ..addCommand('install'); 77 | 78 | var args = parser.parse(['--verbose', 'install']); 79 | expect(args['verbose'], isTrue); 80 | expect(args.command!.name, equals('install')); 81 | }); 82 | 83 | test('parses a parent option after the command', () { 84 | var parser = ArgParser() 85 | ..addFlag('verbose') 86 | ..addCommand('install'); 87 | 88 | var args = parser.parse(['install', '--verbose']); 89 | expect(args['verbose'], isTrue); 90 | expect(args.command!.name, equals('install')); 91 | }); 92 | 93 | test('parses a parent negated option before the command', () { 94 | var parser = ArgParser() 95 | ..addFlag('verbose', defaultsTo: true) 96 | ..addCommand('install'); 97 | 98 | var args = parser.parse(['--no-verbose', 'install']); 99 | expect(args['verbose'], isFalse); 100 | expect(args.command!.name, equals('install')); 101 | }); 102 | 103 | test('parses a parent negated option after the command', () { 104 | var parser = ArgParser() 105 | ..addFlag('verbose', defaultsTo: true) 106 | ..addCommand('install'); 107 | 108 | var args = parser.parse(['install', '--no-verbose']); 109 | expect(args['verbose'], isFalse); 110 | expect(args.command!.name, equals('install')); 111 | }); 112 | 113 | test('parses a parent abbreviation before the command', () { 114 | var parser = ArgParser() 115 | ..addFlag('debug', abbr: 'd') 116 | ..addFlag('verbose', abbr: 'v') 117 | ..addCommand('install'); 118 | 119 | var args = parser.parse(['-dv', 'install']); 120 | expect(args['debug'], isTrue); 121 | expect(args['verbose'], isTrue); 122 | expect(args.command!.name, equals('install')); 123 | }); 124 | 125 | test('parses a parent abbreviation after the command', () { 126 | var parser = ArgParser() 127 | ..addFlag('debug', abbr: 'd') 128 | ..addFlag('verbose', abbr: 'v') 129 | ..addCommand('install'); 130 | 131 | var args = parser.parse(['install', '-dv']); 132 | expect(args['debug'], isTrue); 133 | expect(args['verbose'], isTrue); 134 | expect(args.command!.name, equals('install')); 135 | }); 136 | 137 | test('does not parse a solo command option before the command', () { 138 | var parser = ArgParser(); 139 | var command = parser.addCommand('install'); 140 | command.addOption('path', abbr: 'p'); 141 | 142 | throwsFormat(parser, ['-p', 'foo', 'install']); 143 | }); 144 | 145 | test('does not parse a command option before the command', () { 146 | var parser = ArgParser(); 147 | var command = parser.addCommand('install'); 148 | command.addOption('path'); 149 | 150 | throwsFormat(parser, ['--path', 'foo', 'install']); 151 | }); 152 | 153 | test('does not parse a command abbreviation before the command', () { 154 | var parser = ArgParser(); 155 | var command = parser.addCommand('install'); 156 | command.addFlag('debug', abbr: 'd'); 157 | command.addFlag('verbose', abbr: 'v'); 158 | 159 | throwsFormat(parser, ['-dv', 'install']); 160 | }); 161 | 162 | test('assigns collapsed options to the proper command', () { 163 | var parser = ArgParser(); 164 | parser.addFlag('apple', abbr: 'a'); 165 | var command = parser.addCommand('cmd'); 166 | command.addFlag('banana', abbr: 'b'); 167 | var subcommand = command.addCommand('subcmd'); 168 | subcommand.addFlag('cherry', abbr: 'c'); 169 | 170 | var args = parser.parse(['cmd', 'subcmd', '-abc']); 171 | expect(args['apple'], isTrue); 172 | expect(args.command!.name, equals('cmd')); 173 | expect(args.command!['banana'], isTrue); 174 | expect(args.command!.command!.name, equals('subcmd')); 175 | expect(args.command!.command!['cherry'], isTrue); 176 | }); 177 | 178 | test('option is given to innermost command that can take it', () { 179 | var parser = ArgParser(); 180 | parser.addFlag('verbose'); 181 | parser.addCommand('cmd') 182 | ..addFlag('verbose') 183 | ..addCommand('subcmd'); 184 | 185 | var args = parser.parse(['cmd', 'subcmd', '--verbose']); 186 | expect(args['verbose'], isFalse); 187 | expect(args.command!.name, equals('cmd')); 188 | expect(args.command!['verbose'], isTrue); 189 | expect(args.command!.command!.name, equals('subcmd')); 190 | }); 191 | 192 | test('remaining arguments are given to the innermost command', () { 193 | var parser = ArgParser(); 194 | parser.addCommand('cmd').addCommand('subcmd'); 195 | 196 | var args = parser.parse(['cmd', 'subcmd', 'other', 'stuff']); 197 | expect(args.command!.name, equals('cmd')); 198 | expect(args.rest, isEmpty); 199 | expect(args.command!.command!.name, equals('subcmd')); 200 | expect(args.command!.rest, isEmpty); 201 | expect(args.command!.command!.rest, equals(['other', 'stuff'])); 202 | }); 203 | }); 204 | } 205 | -------------------------------------------------------------------------------- /test/utils_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:args/src/utils.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | const _lineLength = 40; 9 | const _longLine = 'This is a long line that needs to be wrapped.'; 10 | final _longLineWithNewlines = 11 | 'This is a long line with newlines that\nneeds to be wrapped.\n\n' 12 | '${'0123456789' * 5}'; 13 | final _indentedLongLineWithNewlines = 14 | ' This is an indented long line with newlines that\nneeds to be wrapped.' 15 | '\n\tAnd preserves tabs.\n \n ${'0123456789' * 5}'; 16 | const _shortLine = 'Short line.'; 17 | const _indentedLongLine = ' This is an indented long line that needs to be ' 18 | 'wrapped and indentation preserved.'; 19 | 20 | void main() { 21 | group('padding', () { 22 | test('can pad on the right.', () { 23 | expect(padRight('foo', 6), equals('foo ')); 24 | }); 25 | }); 26 | group('text wrapping', () { 27 | test("doesn't wrap short lines.", () { 28 | expect(wrapText(_shortLine, length: _lineLength), equals(_shortLine)); 29 | }); 30 | test("doesn't wrap at all if not given a length", () { 31 | expect(wrapText(_longLine), equals(_longLine)); 32 | }); 33 | test('able to wrap long lines', () { 34 | expect(wrapText(_longLine, length: _lineLength), equals(''' 35 | This is a long line that needs to be 36 | wrapped.''')); 37 | }); 38 | test('wrap long lines with no whitespace', () { 39 | expect(wrapText('0123456789' * 5, length: _lineLength), equals(''' 40 | 0123456789012345678901234567890123456789 41 | 0123456789''')); 42 | }); 43 | test('refuses to wrap to a column smaller than 10 characters', () { 44 | expect(wrapText('$_longLine ${'0123456789' * 4}', length: 1), equals(''' 45 | This is a 46 | long line 47 | that needs 48 | to be 49 | wrapped. 50 | 0123456789 51 | 0123456789 52 | 0123456789 53 | 0123456789''')); 54 | }); 55 | test('preserves indentation', () { 56 | expect(wrapText(_indentedLongLine, length: _lineLength), equals(''' 57 | This is an indented long line that 58 | needs to be wrapped and indentation 59 | preserved.''')); 60 | }); 61 | test('preserves indentation and stripping trailing whitespace', () { 62 | expect(wrapText('$_indentedLongLine ', length: _lineLength), equals(''' 63 | This is an indented long line that 64 | needs to be wrapped and indentation 65 | preserved.''')); 66 | }); 67 | test('wraps text with newlines', () { 68 | expect(wrapText(_longLineWithNewlines, length: _lineLength), equals(''' 69 | This is a long line with newlines that 70 | needs to be wrapped. 71 | 72 | 0123456789012345678901234567890123456789 73 | 0123456789''')); 74 | }); 75 | test('preserves indentation in the presence of newlines', () { 76 | expect(wrapText(_indentedLongLineWithNewlines, length: _lineLength), 77 | equals(''' 78 | This is an indented long line with 79 | newlines that 80 | needs to be wrapped. 81 | \tAnd preserves tabs. 82 | 83 | 01234567890123456789012345678901234567 84 | 890123456789''')); 85 | }); 86 | test('removes trailing whitespace when wrapping', () { 87 | expect(wrapText('$_longLine \t', length: _lineLength), equals(''' 88 | This is a long line that needs to be 89 | wrapped.''')); 90 | }); 91 | test('preserves trailing whitespace when not wrapping', () { 92 | expect(wrapText('$_longLine \t'), equals('$_longLine \t')); 93 | }); 94 | test('honors hangingIndent parameter', () { 95 | expect( 96 | wrapText(_longLine, length: _lineLength, hangingIndent: 6), equals(''' 97 | This is a long line that needs to be 98 | wrapped.''')); 99 | }); 100 | test('handles hangingIndent with a single unwrapped line.', () { 101 | expect(wrapText(_shortLine, length: _lineLength, hangingIndent: 6), 102 | equals(''' 103 | Short line.''')); 104 | }); 105 | test( 106 | 'handles hangingIndent with two unwrapped lines and the second is empty.', 107 | () { 108 | expect(wrapText('$_shortLine\n', length: _lineLength, hangingIndent: 6), 109 | equals(''' 110 | Short line. 111 | ''')); 112 | }, 113 | ); 114 | test('honors hangingIndent parameter on already indented line.', () { 115 | expect(wrapText(_indentedLongLine, length: _lineLength, hangingIndent: 6), 116 | equals(''' 117 | This is an indented long line that 118 | needs to be wrapped and 119 | indentation preserved.''')); 120 | }); 121 | test('honors hangingIndent parameter on already indented line.', () { 122 | expect( 123 | wrapText(_indentedLongLineWithNewlines, 124 | length: _lineLength, hangingIndent: 6), 125 | equals(''' 126 | This is an indented long line with 127 | newlines that 128 | needs to be wrapped. 129 | And preserves tabs. 130 | 131 | 01234567890123456789012345678901234567 132 | 890123456789''')); 133 | }); 134 | }); 135 | group('text wrapping as lines', () { 136 | test("doesn't wrap short lines.", () { 137 | expect(wrapTextAsLines(_shortLine, length: _lineLength), 138 | equals([_shortLine])); 139 | }); 140 | test("doesn't wrap at all if not given a length", () { 141 | expect(wrapTextAsLines(_longLine), equals([_longLine])); 142 | }); 143 | test('able to wrap long lines', () { 144 | expect(wrapTextAsLines(_longLine, length: _lineLength), 145 | equals(['This is a long line that needs to be', 'wrapped.'])); 146 | }); 147 | test('wrap long lines with no whitespace', () { 148 | expect(wrapTextAsLines('0123456789' * 5, length: _lineLength), 149 | equals(['0123456789012345678901234567890123456789', '0123456789'])); 150 | }); 151 | 152 | test('refuses to wrap to a column smaller than 10 characters', () { 153 | expect( 154 | wrapTextAsLines('$_longLine ${'0123456789' * 4}', length: 1), 155 | equals([ 156 | 'This is a', 157 | 'long line', 158 | 'that needs', 159 | 'to be', 160 | 'wrapped.', 161 | '0123456789', 162 | '0123456789', 163 | '0123456789', 164 | '0123456789' 165 | ])); 166 | }); 167 | test("doesn't preserve indentation", () { 168 | expect( 169 | wrapTextAsLines(_indentedLongLine, length: _lineLength), 170 | equals([ 171 | 'This is an indented long line that needs', 172 | 'to be wrapped and indentation preserved.' 173 | ])); 174 | }); 175 | test('strips trailing whitespace', () { 176 | expect( 177 | wrapTextAsLines('$_indentedLongLine ', length: _lineLength), 178 | equals([ 179 | 'This is an indented long line that needs', 180 | 'to be wrapped and indentation preserved.' 181 | ])); 182 | }); 183 | test('splits text with newlines properly', () { 184 | expect( 185 | wrapTextAsLines(_longLineWithNewlines, length: _lineLength), 186 | equals([ 187 | 'This is a long line with newlines that', 188 | 'needs to be wrapped.', 189 | '', 190 | '0123456789012345678901234567890123456789', 191 | '0123456789' 192 | ])); 193 | }); 194 | test('does not preserves indentation in the presence of newlines', () { 195 | expect( 196 | wrapTextAsLines(_indentedLongLineWithNewlines, length: _lineLength), 197 | equals([ 198 | 'This is an indented long line with', 199 | 'newlines that', 200 | 'needs to be wrapped.', 201 | 'And preserves tabs.', 202 | '', 203 | '0123456789012345678901234567890123456789', 204 | '0123456789' 205 | ])); 206 | }); 207 | test('removes trailing whitespace when wrapping', () { 208 | expect(wrapTextAsLines('$_longLine \t', length: _lineLength), 209 | equals(['This is a long line that needs to be', 'wrapped.'])); 210 | }); 211 | test('preserves trailing whitespace when not wrapping', () { 212 | expect( 213 | wrapTextAsLines('$_longLine \t'), equals(['$_longLine \t'])); 214 | }); 215 | }); 216 | } 217 | -------------------------------------------------------------------------------- /test/test_utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:args/args.dart'; 6 | import 'package:args/command_runner.dart'; 7 | import 'package:test/test.dart'; 8 | 9 | class CommandRunnerWithFooter extends CommandRunner { 10 | @override 11 | String get usageFooter => 'Also, footer!'; 12 | 13 | CommandRunnerWithFooter(super.executableName, super.description); 14 | } 15 | 16 | class CommandRunnerWithFooterAndWrapping extends CommandRunner { 17 | @override 18 | String get usageFooter => 'LONG footer! ' 19 | 'This is a long footer, so we can check wrapping on long footer messages.' 20 | '\n\n' 21 | 'And make sure that they preserve newlines properly.'; 22 | 23 | @override 24 | ArgParser get argParser => _argParser; 25 | final _argParser = ArgParser(usageLineLength: 40); 26 | 27 | CommandRunnerWithFooterAndWrapping(super.executableName, super.description); 28 | } 29 | 30 | class FooCommand extends Command { 31 | bool hasRun = false; 32 | 33 | @override 34 | final name = 'foo'; 35 | 36 | @override 37 | final description = 'Set a value.'; 38 | 39 | @override 40 | final takesArguments = false; 41 | 42 | @override 43 | void run() { 44 | hasRun = true; 45 | } 46 | } 47 | 48 | class ValueCommand extends Command { 49 | @override 50 | final name = 'foo'; 51 | 52 | @override 53 | final description = 'Return a value.'; 54 | 55 | @override 56 | final takesArguments = false; 57 | 58 | @override 59 | int run() => 12; 60 | } 61 | 62 | class AsyncValueCommand extends Command { 63 | @override 64 | final name = 'foo'; 65 | 66 | @override 67 | final description = 'Return a future.'; 68 | 69 | @override 70 | final takesArguments = false; 71 | 72 | @override 73 | Future run() async => 'hi'; 74 | } 75 | 76 | class Category1Command extends Command { 77 | bool hasRun = false; 78 | 79 | @override 80 | final name = 'bar'; 81 | 82 | @override 83 | final description = 'Print a value.'; 84 | 85 | @override 86 | final category = 'Printers'; 87 | 88 | @override 89 | final takesArguments = false; 90 | 91 | @override 92 | void run() { 93 | hasRun = true; 94 | } 95 | } 96 | 97 | class Category2Command extends Command { 98 | bool hasRun = false; 99 | 100 | @override 101 | final name = 'baz'; 102 | 103 | @override 104 | final description = 'Display a value.'; 105 | 106 | @override 107 | final category = 'Displayers'; 108 | 109 | @override 110 | final takesArguments = false; 111 | 112 | @override 113 | void run() { 114 | hasRun = true; 115 | } 116 | } 117 | 118 | class Category2Command2 extends Command { 119 | bool hasRun = false; 120 | 121 | @override 122 | final name = 'baz2'; 123 | 124 | @override 125 | final description = 'Display another value.'; 126 | 127 | @override 128 | final category = 'Displayers'; 129 | 130 | @override 131 | final takesArguments = false; 132 | 133 | @override 134 | void run() { 135 | hasRun = true; 136 | } 137 | } 138 | 139 | class MultilineCommand extends Command { 140 | bool hasRun = false; 141 | 142 | @override 143 | final name = 'multiline'; 144 | 145 | @override 146 | final description = 'Multi\nline.'; 147 | 148 | @override 149 | final takesArguments = false; 150 | 151 | @override 152 | void run() { 153 | hasRun = true; 154 | } 155 | } 156 | 157 | class WrappingCommand extends Command { 158 | bool hasRun = false; 159 | 160 | @override 161 | ArgParser get argParser => _argParser; 162 | final _argParser = ArgParser(usageLineLength: 40); 163 | 164 | @override 165 | final name = 'wrapping'; 166 | 167 | @override 168 | final description = 169 | 'This command overrides the argParser so that it will wrap long lines.'; 170 | 171 | @override 172 | final takesArguments = false; 173 | 174 | @override 175 | void run() { 176 | hasRun = true; 177 | } 178 | } 179 | 180 | class LongCommand extends Command { 181 | bool hasRun = false; 182 | 183 | @override 184 | ArgParser get argParser => _argParser; 185 | final _argParser = ArgParser(usageLineLength: 40); 186 | 187 | @override 188 | final name = 'long'; 189 | 190 | @override 191 | final description = 192 | 'This command has a long description that needs to be wrapped ' 193 | 'sometimes.\nIt has embedded newlines,\n' 194 | ' and indented lines that also need to be wrapped and have their ' 195 | 'indentation preserved.\n${'0123456789' * 10}'; 196 | 197 | @override 198 | final takesArguments = false; 199 | 200 | @override 201 | void run() { 202 | hasRun = true; 203 | } 204 | } 205 | 206 | class MultilineSummaryCommand extends MultilineCommand { 207 | @override 208 | String get summary => description; 209 | } 210 | 211 | class HiddenCommand extends Command { 212 | bool hasRun = false; 213 | 214 | @override 215 | final name = 'hidden'; 216 | 217 | @override 218 | final description = 'Set a value.'; 219 | 220 | @override 221 | final hidden = true; 222 | 223 | @override 224 | final takesArguments = false; 225 | 226 | @override 227 | void run() { 228 | hasRun = true; 229 | } 230 | } 231 | 232 | class HiddenCategorizedCommand extends Command { 233 | bool hasRun = false; 234 | 235 | @override 236 | final name = 'hiddencategorized'; 237 | 238 | @override 239 | final description = 'Set a value.'; 240 | 241 | @override 242 | final category = 'Some category'; 243 | 244 | @override 245 | final hidden = true; 246 | 247 | @override 248 | final takesArguments = false; 249 | 250 | @override 251 | void run() { 252 | hasRun = true; 253 | } 254 | } 255 | 256 | class AliasedCommand extends Command { 257 | bool hasRun = false; 258 | 259 | @override 260 | final name = 'aliased'; 261 | 262 | @override 263 | final description = 'Set a value.'; 264 | 265 | @override 266 | final takesArguments = false; 267 | 268 | @override 269 | final aliases = const ['alias', 'als', 'renamed']; 270 | 271 | @override 272 | void run() { 273 | hasRun = true; 274 | } 275 | } 276 | 277 | class SuggestionAliasedCommand extends Command { 278 | bool hasRun = false; 279 | 280 | @override 281 | final name = 'aliased'; 282 | 283 | @override 284 | final description = 'Set a value.'; 285 | 286 | @override 287 | final takesArguments = false; 288 | 289 | @override 290 | final suggestionAliases = const ['renamed']; 291 | 292 | @override 293 | void run() { 294 | hasRun = true; 295 | } 296 | } 297 | 298 | class AsyncCommand extends Command { 299 | bool hasRun = false; 300 | 301 | @override 302 | final name = 'async'; 303 | 304 | @override 305 | final description = 'Set a value asynchronously.'; 306 | 307 | @override 308 | final takesArguments = false; 309 | 310 | @override 311 | Future run() => Future.value().then((_) => hasRun = true); 312 | } 313 | 314 | class AllowAnythingCommand extends Command { 315 | bool hasRun = false; 316 | 317 | @override 318 | final name = 'allowAnything'; 319 | 320 | @override 321 | final description = 'A command using allowAnything.'; 322 | 323 | @override 324 | final argParser = ArgParser.allowAnything(); 325 | 326 | @override 327 | void run() { 328 | hasRun = true; 329 | } 330 | } 331 | 332 | class CustomNameCommand extends Command { 333 | @override 334 | final String name; 335 | 336 | CustomNameCommand(this.name); 337 | 338 | @override 339 | String get description => 'A command with a custom name'; 340 | } 341 | 342 | void throwsIllegalArg(void Function() function, {String? reason}) { 343 | expect(function, throwsArgumentError, reason: reason); 344 | } 345 | 346 | void throwsFormat(ArgParser parser, List args, {String? reason}) { 347 | expect(() => parser.parse(args), throwsA(isA()), 348 | reason: reason); 349 | } 350 | 351 | void throwsArgParserException(ArgParser parser, List args, 352 | String message, List commands, String arg) { 353 | try { 354 | parser.parse(args); 355 | fail('Expected an ArgParserException'); 356 | } on ArgParserException catch (e) { 357 | expect(e.message, message); 358 | expect(e.commands, commands); 359 | expect(e.argumentName, arg); 360 | } catch (e) { 361 | fail('Expected an ArgParserException, but got $e'); 362 | } 363 | } 364 | 365 | Matcher throwsUsageException(Object? message, Object? usage) => 366 | throwsA(isA() 367 | .having((e) => e.message, 'message', message) 368 | .having((e) => e.usage, 'usage', usage)); 369 | -------------------------------------------------------------------------------- /lib/src/usage.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:math' as math; 6 | 7 | import '../args.dart'; 8 | import 'utils.dart'; 9 | 10 | /// Generates a string of usage (i.e. help) text for a list of options. 11 | /// 12 | /// Internally, it works like a tabular printer. The output is divided into 13 | /// three horizontal columns, like so: 14 | /// 15 | /// -h, --help Prints the usage information 16 | /// | | | | 17 | /// 18 | /// It builds the usage text up one column at a time and handles padding with 19 | /// spaces and wrapping to the next line to keep the cells correctly lined up. 20 | /// 21 | /// [lineLength] specifies the horizontal character position at which the help 22 | /// text is wrapped. Help that extends past this column will be wrapped at the 23 | /// nearest whitespace (or truncated if there is no available whitespace). If 24 | /// `null` there will not be any wrapping. 25 | String generateUsage(List optionsAndSeparators, {int? lineLength}) => 26 | _Usage(optionsAndSeparators, lineLength).generate(); 27 | 28 | class _Usage { 29 | /// Abbreviation, long name, help. 30 | static const _columnCount = 3; 31 | 32 | /// A list of the [Option]s intermingled with [String] separators. 33 | final List _optionsAndSeparators; 34 | 35 | /// The working buffer for the generated usage text. 36 | final _buffer = StringBuffer(); 37 | 38 | /// The column that the "cursor" is currently on. 39 | /// 40 | /// If the next call to [write()] is not for this column, it will correctly 41 | /// handle advancing to the next column (and possibly the next row). 42 | int _currentColumn = 0; 43 | 44 | /// The width in characters of each column. 45 | late final _columnWidths = _calculateColumnWidths(); 46 | 47 | /// How many newlines need to be rendered before the next bit of text can be 48 | /// written. 49 | /// 50 | /// We do this lazily so that the last bit of usage doesn't have dangling 51 | /// newlines. We only write newlines right *before* we write some real 52 | /// content. 53 | int _newlinesNeeded = 0; 54 | 55 | /// The horizontal character position at which help text is wrapped. 56 | /// 57 | /// Help that extends past this column will be wrapped at the nearest 58 | /// whitespace (or truncated if there is no available whitespace). 59 | final int? lineLength; 60 | 61 | _Usage(this._optionsAndSeparators, this.lineLength); 62 | 63 | /// Generates a string displaying usage information for the defined options. 64 | /// This is basically the help text shown on the command line. 65 | String generate() { 66 | for (var optionOrSeparator in _optionsAndSeparators) { 67 | if (optionOrSeparator is String) { 68 | _writeSeparator(optionOrSeparator); 69 | continue; 70 | } 71 | var option = optionOrSeparator as Option; 72 | if (option.hide) continue; 73 | _writeOption(option); 74 | } 75 | 76 | return _buffer.toString(); 77 | } 78 | 79 | void _writeSeparator(String separator) { 80 | // Ensure that there's always a blank line before a separator. 81 | if (_buffer.isNotEmpty) _buffer.write('\n\n'); 82 | _buffer.write(separator); 83 | _newlinesNeeded = 1; 84 | } 85 | 86 | void _writeOption(Option option) { 87 | _write(0, _abbreviation(option)); 88 | _write(1, '${_longOption(option)}${_mandatoryOption(option)}'); 89 | 90 | if (option.help != null) _write(2, option.help!); 91 | 92 | if (option.allowedHelp != null) { 93 | var allowedNames = option.allowedHelp!.keys.toList(); 94 | allowedNames.sort(); 95 | _newline(); 96 | for (var name in allowedNames) { 97 | _write(1, _allowedTitle(option, name)); 98 | _write(2, option.allowedHelp![name]!); 99 | } 100 | _newline(); 101 | } else if (option.allowed != null) { 102 | _write(2, _buildAllowedList(option)); 103 | } else if (option.isFlag) { 104 | if (option.defaultsTo == true) { 105 | _write(2, '(defaults to on)'); 106 | } 107 | } else if (option.isMultiple) { 108 | if (option.defaultsTo != null && 109 | (option.defaultsTo as Iterable).isNotEmpty) { 110 | var defaults = 111 | (option.defaultsTo as List).map((value) => '"$value"').join(', '); 112 | _write(2, '(defaults to $defaults)'); 113 | } 114 | } else if (option.defaultsTo != null) { 115 | _write(2, '(defaults to "${option.defaultsTo}")'); 116 | } 117 | } 118 | 119 | String _abbreviation(Option option) => 120 | option.abbr == null ? '' : '-${option.abbr}, '; 121 | 122 | String _longOption(Option option) { 123 | String result; 124 | if (option.negatable!) { 125 | result = '--[no-]${option.name}'; 126 | } else { 127 | result = '--${option.name}'; 128 | } 129 | 130 | if (option.valueHelp != null) result += '=<${option.valueHelp}>'; 131 | 132 | return result; 133 | } 134 | 135 | String _mandatoryOption(Option option) { 136 | return option.mandatory ? ' (mandatory)' : ''; 137 | } 138 | 139 | String _allowedTitle(Option option, String allowed) { 140 | var isDefault = option.defaultsTo is List 141 | ? (option.defaultsTo as List).contains(allowed) 142 | : option.defaultsTo == allowed; 143 | return ' [$allowed]${isDefault ? ' (default)' : ''}'; 144 | } 145 | 146 | List _calculateColumnWidths() { 147 | var abbr = 0; 148 | var title = 0; 149 | for (var option in _optionsAndSeparators) { 150 | if (option is! Option) continue; 151 | if (option.hide) continue; 152 | 153 | // Make room in the first column if there are abbreviations. 154 | abbr = math.max(abbr, _abbreviation(option).length); 155 | 156 | // Make room for the option. 157 | title = math.max( 158 | title, _longOption(option).length + _mandatoryOption(option).length); 159 | 160 | // Make room for the allowed help. 161 | if (option.allowedHelp != null) { 162 | for (var allowed in option.allowedHelp!.keys) { 163 | title = math.max(title, _allowedTitle(option, allowed).length); 164 | } 165 | } 166 | } 167 | 168 | // Leave a gutter between the columns. 169 | title += 4; 170 | return [abbr, title]; 171 | } 172 | 173 | void _newline() { 174 | _newlinesNeeded++; 175 | _currentColumn = 0; 176 | } 177 | 178 | void _write(int column, String text) { 179 | var lines = text.split('\n'); 180 | // If we are writing the last column, word wrap it to fit. 181 | if (column == _columnWidths.length && lineLength != null) { 182 | var start = 183 | _columnWidths.take(column).reduce((start, width) => start + width); 184 | lines = [ 185 | for (var line in lines) 186 | ...wrapTextAsLines(line, start: start, length: lineLength), 187 | ]; 188 | } 189 | 190 | // Strip leading and trailing empty lines. 191 | while (lines.isNotEmpty && lines.first.trim() == '') { 192 | lines.removeAt(0); 193 | } 194 | while (lines.isNotEmpty && lines.last.trim() == '') { 195 | lines.removeLast(); 196 | } 197 | 198 | for (var line in lines) { 199 | _writeLine(column, line); 200 | } 201 | } 202 | 203 | void _writeLine(int column, String text) { 204 | // Write any pending newlines. 205 | while (_newlinesNeeded > 0) { 206 | _buffer.write('\n'); 207 | _newlinesNeeded--; 208 | } 209 | 210 | // Advance until we are at the right column (which may mean wrapping around 211 | // to the next line. 212 | while (_currentColumn != column) { 213 | if (_currentColumn < _columnCount - 1) { 214 | _buffer.write(' ' * _columnWidths[_currentColumn]); 215 | } else { 216 | _buffer.write('\n'); 217 | } 218 | _currentColumn = (_currentColumn + 1) % _columnCount; 219 | } 220 | 221 | if (column < _columnWidths.length) { 222 | // Fixed-size column, so pad it. 223 | _buffer.write(text.padRight(_columnWidths[column])); 224 | } else { 225 | // The last column, so just write it. 226 | _buffer.write(text); 227 | } 228 | 229 | // Advance to the next column. 230 | _currentColumn = (_currentColumn + 1) % _columnCount; 231 | 232 | // If we reached the last column, we need to wrap to the next line. 233 | if (column == _columnCount - 1) _newlinesNeeded++; 234 | } 235 | 236 | String _buildAllowedList(Option option) { 237 | var isDefault = option.defaultsTo is List 238 | ? (option.defaultsTo as List).contains 239 | : (String value) => value == option.defaultsTo; 240 | 241 | var allowedBuffer = StringBuffer(); 242 | allowedBuffer.write('['); 243 | var first = true; 244 | for (var allowed in option.allowed!) { 245 | if (!first) allowedBuffer.write(', '); 246 | allowedBuffer.write(allowed); 247 | if (isDefault(allowed)) { 248 | allowedBuffer.write(' (default)'); 249 | } 250 | first = false; 251 | } 252 | allowedBuffer.write(']'); 253 | return allowedBuffer.toString(); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.6.0-wip 2 | 3 | * Added source argument when throwing a `ArgParserException`. 4 | * Fix inconsistent `FormatException` messages 5 | * Require Dart 3.3 6 | 7 | ## 2.5.0 8 | 9 | * Introduce new typed `ArgResults` `flag(String)`, `option(String)`, and 10 | `multiOption(String)` methods. 11 | * Require Dart 3.0. 12 | 13 | ## 2.4.2 14 | 15 | * Change the validation of `mandatory` options; they now perform validation when 16 | the value is retrieved (from the `ArgResults` object), instead of when the 17 | args are parsed. 18 | * Require Dart 2.19. 19 | 20 | ## 2.4.1 21 | 22 | * Add a `CONTRIBUTING.md` file; move the publishing automation docs from the 23 | readme into the contributing doc. 24 | * Added package topics to the pubspec file. 25 | 26 | ## 2.4.0 27 | 28 | * Command suggestions will now also suggest based on aliases of a command. 29 | * Introduce getter `Command.suggestionAliases` for names that cannot be used as 30 | aliases, but will trigger suggestions. 31 | 32 | ## 2.3.2 33 | 34 | * Require Dart 2.18 35 | 36 | ## 2.3.1 37 | 38 | * Switch to using package:lints. 39 | * Address an issue with the readme API documentation (#211). 40 | * Populate the pubspec `repository` field. 41 | 42 | ## 2.3.0 43 | 44 | * Add the ability to group commands by category in usage text. 45 | 46 | ## 2.2.0 47 | 48 | * Suggest similar commands if an unknown command is encountered, when using the 49 | `CommandRunner`. 50 | * The max edit distance for suggestions defaults to 2, but can be configured 51 | using the `suggestionDistanceLimit` parameter on the constructor. You can 52 | set it to `0` to disable the feature. 53 | 54 | ## 2.1.1 55 | 56 | * Fix a bug with `mandatory` options which caused a null assertion failure when 57 | used within a command. 58 | 59 | ## 2.1.0 60 | 61 | * Add a `mandatory` argument to require the presence of an option. 62 | * Add `aliases` named argument to `addFlag`, `addOption`, and `addMultiOption`, 63 | as well as a public `findByNameOrAlias` method on `ArgParser`. This allows 64 | you to provide aliases for an argument name, which eases the transition from 65 | one argument name to another. 66 | 67 | ## 2.0.0 68 | 69 | * Stable null safety release. 70 | 71 | ## 2.0.0-nullsafety.0 72 | 73 | * Migrate to null safety. 74 | * **BREAKING** Remove APIs that had been marked as deprecated: 75 | 76 | * Instead of the `allowMulti` and `splitCommas` arguments to 77 | `ArgParser.addOption()`, use `ArgParser.addMultiOption()`. 78 | * Instead of `ArgParser.getUsage()`, use `ArgParser.usage`. 79 | * Instead of `Option.abbreviation`, use `Option.abbr`. 80 | * Instead of `Option.defaultValue`, use `Option.defaultsTo`. 81 | * Instead of `OptionType.FLAG/SINGLE/MULTIPLE`, use 82 | `OptionType.flag/single/multiple`. 83 | * Add a more specific function type to the `callback` argument of `addOption`. 84 | 85 | ## 1.6.0 86 | 87 | * Remove `help` from the list of commands in usage. 88 | * Remove the blank lines in usage which separated the help for options that 89 | happened to span multiple lines. 90 | 91 | ## 1.5.4 92 | 93 | * Fix a bug with option names containing underscores. 94 | * Point towards `CommandRunner` in the docs for `ArgParser.addCommand` since it 95 | is what most authors will want to use instead. 96 | 97 | ## 1.5.3 98 | 99 | * Improve arg parsing performance: use queues instead of lists internally to 100 | get linear instead of quadratic performance, which is important for large 101 | numbers of args (>1000). And, use simple string manipulation instead of 102 | regular expressions for a 1.5x improvement everywhere. 103 | * No longer automatically add a 'help' option to commands that don't validate 104 | their arguments (fix #123). 105 | 106 | ## 1.5.2 107 | 108 | * Added support for `usageLineLength` in `CommandRunner` 109 | 110 | ## 1.5.1 111 | 112 | * Added more comprehensive word wrapping when `usageLineLength` is set. 113 | 114 | ## 1.5.0 115 | 116 | * Add `usageLineLength` to control word wrapping usage text. 117 | 118 | ## 1.4.4 119 | 120 | * Set max SDK version to `<3.0.0`, and adjust other dependencies. 121 | 122 | ## 1.4.3 123 | 124 | * Display the default values for options with `allowedHelp` specified. 125 | 126 | ## 1.4.2 127 | 128 | * Narrow the SDK constraint to only allow SDK versions that support `FutureOr`. 129 | 130 | ## 1.4.1 131 | 132 | * Fix the way default values for multi-valued options are printed in argument 133 | usage. 134 | 135 | ## 1.4.0 136 | 137 | * Deprecated `OptionType.FLAG`, `OptionType.SINGLE`, and `OptionType.MULTIPLE` 138 | in favor of `OptionType.flag`, `OptionType.single`, and `OptionType.multiple` 139 | which follow the style guide. 140 | 141 | * Deprecated `Option.abbreviation` and `Option.defaultValue` in favor of 142 | `Option.abbr` and `Option.defaultsTo`. This makes all of `Option`'s fields 143 | match the corresponding parameters to `ArgParser.addOption()`. 144 | 145 | * Deprecated the `allowMultiple` and `splitCommas` arguments to 146 | `ArgParser.addOption()` in favor of a separate `ArgParser.addMultiOption()` 147 | method. This allows us to provide more accurate type information, and to avoid 148 | adding flags that only make sense for multi-options in places where they might 149 | be usable for single-value options. 150 | 151 | ## 1.3.0 152 | 153 | * Type `Command.run()`'s return value as `FutureOr`. 154 | 155 | ## 1.2.0 156 | 157 | * Type the `callback` parameter to `ArgParser.addOption()` as `Function` rather 158 | than `void Function(value)`. This allows strong-mode users to write `callback: 159 | (String value) { ... }` rather than having to manually cast `value` to a 160 | `String` (or a `List` with `allowMultiple: true`). 161 | 162 | ## 1.1.0 163 | 164 | * `ArgParser.parse()` now takes an `Iterable` rather than a 165 | `List`. 166 | 167 | * `ArgParser.addOption()`'s `allowed` option now takes an `Iterable` 168 | rather than a `List`. 169 | 170 | ## 1.0.2 171 | 172 | * Fix analyzer warning 173 | 174 | ## 1.0.1 175 | 176 | * Fix a fuzzy arrow type warning. 177 | 178 | ## 1.0.0 179 | 180 | * **Breaking change**: The `allowTrailingOptions` argument to `new 181 | ArgumentParser()` defaults to `true` instead of `false`. 182 | 183 | * Add `new ArgParser.allowAnything()`. This allows any input, without parsing 184 | any options. 185 | 186 | ## 0.13.7 187 | 188 | * Add explicit support for forwarding the value returned by `Command.run()` to 189 | `CommandRunner.run()`. This worked unintentionally prior to 0.13.6+1. 190 | 191 | * Add type arguments to `CommandRunner` and `Command` to indicate the return 192 | values of the `run()` functions. 193 | 194 | ## 0.13.6+1 195 | 196 | * When a `CommandRunner` is passed `--help` before any commands, it now prints 197 | the usage of the chosen command. 198 | 199 | ## 0.13.6 200 | 201 | * `ArgParser.parse()` now throws an `ArgParserException`, which implements 202 | `FormatException` and has a field that lists the commands that were parsed. 203 | 204 | * If `CommandRunner.run()` encounters a parse error for a subcommand, it now 205 | prints the subcommand's usage rather than the global usage. 206 | 207 | ## 0.13.5 208 | 209 | * Allow `CommandRunner.argParser` and `Command.argParser` to be overridden in 210 | strong mode. 211 | 212 | ## 0.13.4+2 213 | 214 | * Fix a minor documentation error. 215 | 216 | ## 0.13.4+1 217 | 218 | * Ensure that multiple-value arguments produce reified `List`s. 219 | 220 | ## 0.13.4 221 | 222 | * By default, only the first line of a command's description is included in its 223 | parent runner's usage string. This returns to the default behavior from 224 | before 0.13.3+1. 225 | 226 | * A `Command.summary` getter has been added to explicitly control the summary 227 | that appears in the parent runner's usage string. This getter defaults to the 228 | first line of the description, but can be overridden if the user wants a 229 | multi-line summary. 230 | 231 | ## 0.13.3+6 232 | 233 | * README fixes. 234 | 235 | ## 0.13.3+5 236 | 237 | * Make strong mode clean. 238 | 239 | ## 0.13.3+4 240 | 241 | * Use the proper `usage` getter in the README. 242 | 243 | ## 0.13.3+3 244 | 245 | * Add an explicit default value for the `allowTrailingOptions` parameter to `new 246 | ArgParser()`. This doesn't change the behavior at all; the option already 247 | defaulted to `false`, and passing in `null` still works. 248 | 249 | ## 0.13.3+2 250 | 251 | * Documentation fixes. 252 | 253 | ## 0.13.3+1 254 | 255 | * Print all lines of multi-line command descriptions. 256 | 257 | ## 0.13.2 258 | 259 | * Allow option values that look like options. This more closely matches the 260 | behavior of [`getopt`][getopt], the *de facto* standard for option parsing. 261 | 262 | [getopt]: https://man7.org/linux/man-pages/man3/getopt.3.html 263 | 264 | ## 0.13.1 265 | 266 | * Add `ArgParser.addSeparator()`. Separators allow users to group their options 267 | in the usage text. 268 | 269 | ## 0.13.0 270 | 271 | * **Breaking change**: An option that allows multiple values will now 272 | automatically split apart comma-separated values. This can be controlled with 273 | the `splitCommas` option. 274 | 275 | ## 0.12.2+6 276 | 277 | * Remove the dependency on the `collection` package. 278 | 279 | ## 0.12.2+5 280 | 281 | * Add syntax highlighting to the README. 282 | 283 | ## 0.12.2+4 284 | 285 | * Add an example of using command-line arguments to the README. 286 | 287 | ## 0.12.2+3 288 | 289 | * Fixed implementation of ArgResults.options to really use Iterable 290 | instead of Iterable cast to Iterable. 291 | 292 | ## 0.12.2+2 293 | 294 | * Updated dependency constraint on `unittest`. 295 | 296 | * Formatted source code. 297 | 298 | * Fixed use of deprecated API in example. 299 | 300 | ## 0.12.2+1 301 | 302 | * Fix the built-in `help` command for `CommandRunner`. 303 | 304 | ## 0.12.2 305 | 306 | * Add `CommandRunner` and `Command` classes which make it easy to build a 307 | command-based command-line application. 308 | 309 | * Add an `ArgResults.arguments` field, which contains the original argument list. 310 | 311 | ## 0.12.1 312 | 313 | * Replace `ArgParser.getUsage()` with `ArgParser.usage`, a getter. 314 | `ArgParser.getUsage()` is now deprecated, to be removed in args version 1.0.0. 315 | 316 | ## 0.12.0+2 317 | 318 | * Widen the version constraint on the `collection` package. 319 | 320 | ## 0.12.0+1 321 | 322 | * Remove the documentation link from the pubspec so this is linked to 323 | pub.dev by default. 324 | 325 | ## 0.12.0 326 | 327 | * Removed public constructors for `ArgResults` and `Option`. 328 | 329 | * `ArgResults.wasParsed()` can be used to determine if an option was actually 330 | parsed or the default value is being returned. 331 | 332 | * Replaced `isFlag` and `allowMultiple` fields in the `Option` class with a 333 | three-value `OptionType` enum. 334 | 335 | * Options may define `valueHelp` which will then be shown in the usage. 336 | 337 | ## 0.11.0 338 | 339 | * Move handling trailing options from `ArgParser.parse()` into `ArgParser` 340 | itself. This lets subcommands have different behavior for how they handle 341 | trailing options. 342 | 343 | ## 0.10.0+2 344 | 345 | * Usage ignores hidden options when determining column widths. 346 | -------------------------------------------------------------------------------- /test/args_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:args/args.dart'; 6 | import 'package:test/test.dart'; 7 | 8 | import 'test_utils.dart'; 9 | 10 | void main() { 11 | group('ArgParser.addFlag()', () { 12 | test('throws ArgumentError if the flag already exists', () { 13 | var parser = ArgParser(); 14 | parser.addFlag('foo'); 15 | throwsIllegalArg(() => parser.addFlag('foo')); 16 | }); 17 | 18 | test('throws ArgumentError if the option already exists', () { 19 | var parser = ArgParser(); 20 | parser.addOption('foo'); 21 | throwsIllegalArg(() => parser.addFlag('foo')); 22 | }); 23 | 24 | test('throws ArgumentError if the abbreviation exists', () { 25 | var parser = ArgParser(); 26 | parser.addFlag('foo', abbr: 'f'); 27 | throwsIllegalArg(() => parser.addFlag('flummox', abbr: 'f')); 28 | }); 29 | 30 | test( 31 | 'throws ArgumentError if the abbreviation is longer ' 32 | 'than one character', () { 33 | var parser = ArgParser(); 34 | throwsIllegalArg(() => parser.addFlag('flummox', abbr: 'flu')); 35 | }); 36 | 37 | test('throws ArgumentError if a flag name is invalid', () { 38 | var parser = ArgParser(); 39 | 40 | for (var name in _invalidOptions) { 41 | var reason = '${Error.safeToString(name)} is not valid'; 42 | throwsIllegalArg(() => parser.addFlag(name), reason: reason); 43 | } 44 | }); 45 | 46 | test('accepts valid flag names', () { 47 | var parser = ArgParser(); 48 | 49 | for (var name in _validOptions) { 50 | var reason = '${Error.safeToString(name)} is valid'; 51 | expect(() => parser.addFlag(name), returnsNormally, reason: reason); 52 | } 53 | }); 54 | }); 55 | 56 | group('ArgParser.addOption()', () { 57 | test('throws ArgumentError if the flag already exists', () { 58 | var parser = ArgParser(); 59 | parser.addFlag('foo'); 60 | throwsIllegalArg(() => parser.addOption('foo')); 61 | }); 62 | 63 | test('throws ArgumentError if the option already exists', () { 64 | var parser = ArgParser(); 65 | parser.addOption('foo'); 66 | throwsIllegalArg(() => parser.addOption('foo')); 67 | }); 68 | 69 | test('throws ArgumentError if the abbreviation exists', () { 70 | var parser = ArgParser(); 71 | parser.addFlag('foo', abbr: 'f'); 72 | throwsIllegalArg(() => parser.addOption('flummox', abbr: 'f')); 73 | }); 74 | 75 | test( 76 | 'throws ArgumentError if the abbreviation is longer ' 77 | 'than one character', () { 78 | var parser = ArgParser(); 79 | throwsIllegalArg(() => parser.addOption('flummox', abbr: 'flu')); 80 | }); 81 | 82 | test('throws ArgumentError if the abbreviation is empty', () { 83 | var parser = ArgParser(); 84 | throwsIllegalArg(() => parser.addOption('flummox', abbr: '')); 85 | }); 86 | 87 | test('throws ArgumentError if the abbreviation is an invalid value', () { 88 | var parser = ArgParser(); 89 | for (var name in _invalidOptions) { 90 | throwsIllegalArg(() => parser.addOption('flummox', abbr: name)); 91 | } 92 | }); 93 | 94 | test('throws ArgumentError if the abbreviation is a dash', () { 95 | var parser = ArgParser(); 96 | throwsIllegalArg(() => parser.addOption('flummox', abbr: '-')); 97 | }); 98 | 99 | test('allows explict null value for "abbr"', () { 100 | var parser = ArgParser(); 101 | expect(() => parser.addOption('flummox', abbr: null), returnsNormally); 102 | }); 103 | 104 | test('throws ArgumentError if an option name is invalid', () { 105 | var parser = ArgParser(); 106 | 107 | for (var name in _invalidOptions) { 108 | var reason = '${Error.safeToString(name)} is not valid'; 109 | throwsIllegalArg(() => parser.addOption(name), reason: reason); 110 | } 111 | }); 112 | 113 | test('accepts valid option names', () { 114 | var parser = ArgParser(); 115 | 116 | for (var name in _validOptions) { 117 | var reason = '${Error.safeToString(name)} is valid'; 118 | expect(() => parser.addOption(name), returnsNormally, reason: reason); 119 | } 120 | }); 121 | }); 122 | 123 | group('ArgParser.getDefault()', () { 124 | test('returns the default value for an option', () { 125 | var parser = ArgParser(); 126 | parser.addOption('mode', defaultsTo: 'debug'); 127 | expect(parser.defaultFor('mode'), 'debug'); 128 | }); 129 | 130 | test('throws if the option is unknown', () { 131 | var parser = ArgParser(); 132 | parser.addOption('mode', defaultsTo: 'debug'); 133 | throwsIllegalArg(() => parser.defaultFor('undefined')); 134 | }); 135 | }); 136 | 137 | group('ArgParser.commands', () { 138 | test('returns an empty map if there are no commands', () { 139 | var parser = ArgParser(); 140 | expect(parser.commands, isEmpty); 141 | }); 142 | 143 | test('returns the commands that were added', () { 144 | var parser = ArgParser(); 145 | parser.addCommand('hide'); 146 | parser.addCommand('seek'); 147 | expect(parser.commands, hasLength(2)); 148 | expect(parser.commands['hide'], isNotNull); 149 | expect(parser.commands['seek'], isNotNull); 150 | }); 151 | 152 | test('iterates over the commands in the order they were added', () { 153 | var parser = ArgParser(); 154 | parser.addCommand('a'); 155 | parser.addCommand('d'); 156 | parser.addCommand('b'); 157 | parser.addCommand('c'); 158 | expect(parser.commands.keys, equals(['a', 'd', 'b', 'c'])); 159 | }); 160 | }); 161 | 162 | group('ArgParser.options', () { 163 | test('returns an empty map if there are no options', () { 164 | var parser = ArgParser(); 165 | expect(parser.options, isEmpty); 166 | }); 167 | 168 | test('returns the options that were added', () { 169 | var parser = ArgParser(); 170 | parser.addFlag('hide'); 171 | parser.addOption('seek'); 172 | expect(parser.options, hasLength(2)); 173 | expect(parser.options['hide'], isNotNull); 174 | expect(parser.options['seek'], isNotNull); 175 | }); 176 | 177 | test('iterates over the options in the order they were added', () { 178 | var parser = ArgParser(); 179 | parser.addFlag('a'); 180 | parser.addOption('d'); 181 | parser.addFlag('b'); 182 | parser.addOption('c'); 183 | expect(parser.options.keys, equals(['a', 'd', 'b', 'c'])); 184 | }); 185 | }); 186 | 187 | group('ArgParser.findByNameOrAlias', () { 188 | test('returns null if there is no match', () { 189 | var parser = ArgParser(); 190 | expect(parser.findByNameOrAlias('a'), isNull); 191 | }); 192 | 193 | test('can find options by alias', () { 194 | var parser = ArgParser()..addOption('a', aliases: ['b']); 195 | expect(parser.findByNameOrAlias('b'), 196 | isA