From 2ad40cdd652b787843af164ad024c27afd3b2724 Mon Sep 17 00:00:00 2001 From: "Lasse R.H. Nielsen" Date: Mon, 9 Feb 2026 12:26:46 +0100 Subject: [PATCH] Add `ArgResult.flagCount`. Counts the occurrences of a flag, and makes it accessible in the `ArgResult`. Only changes parser and result class, the `Option` class still treats flags as `bool` options, with a value that is `true` only when the count is greater than zero. Fixes #937. --- pkgs/args/CHANGELOG.md | 5 +++ pkgs/args/lib/src/arg_results.dart | 65 ++++++++++++++++++++++++------ pkgs/args/lib/src/parser.dart | 11 +++-- pkgs/args/pubspec.yaml | 2 +- pkgs/args/test/parse_test.dart | 44 ++++++++++++++++++++ 5 files changed, 110 insertions(+), 17 deletions(-) diff --git a/pkgs/args/CHANGELOG.md b/pkgs/args/CHANGELOG.md index 54985ffe..cde24609 100644 --- a/pkgs/args/CHANGELOG.md +++ b/pkgs/args/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.9.0-wip + +* Adds `flagCount(name)` to `ArgResults` which returns the number of occurrences + of a flag. Allows, for example, `-vv` to represent "double verbose". + ## 2.8.0 * Allow designating a top-level command or a subcommand as a default one by diff --git a/pkgs/args/lib/src/arg_results.dart b/pkgs/args/lib/src/arg_results.dart index 240867f4..e95c94d9 100644 --- a/pkgs/args/lib/src/arg_results.dart +++ b/pkgs/args/lib/src/arg_results.dart @@ -66,15 +66,18 @@ class ArgResults { /// > flags, [option] for options, and [multiOption] for multi-options. dynamic operator [](String name) { if (!_parser.options.containsKey(name)) { - throw ArgumentError('Could not find an option named "--$name".'); + throw ArgumentError.value( + name, 'name', 'Could not find an option named "--$name".'); } final option = _parser.options[name]!; if (option.mandatory && !_parsed.containsKey(name)) { - throw ArgumentError('Option $name is mandatory.'); + throw ArgumentError.value(name, 'name', 'Option $name is mandatory.'); } - - return option.valueOrDefault(_parsed[name]); + var parsedValue = _parsed[name]; + if (option.isFlag && parsedValue is int) parsedValue = parsedValue > 0; + var result = option.valueOrDefault(parsedValue); + return result; } /// Returns the parsed or default command-line flag named [name]. @@ -83,12 +86,45 @@ class ArgResults { bool flag(String name) { final option = _parser.options[name]; if (option == null) { - throw ArgumentError('Could not find a flag named "--$name".'); + throw ArgumentError.value( + name, 'name', 'Could not find a flag named "--$name".'); + } + if (!option.isFlag) { + throw ArgumentError.value(name, 'name', '"$name" is not a flag.'); + } + var parsedValue = _parsed[name]; + if (parsedValue is int) { + parsedValue = parsedValue > 0; + } + return option.valueOrDefault(parsedValue) as bool; + } + + /// The number of times the flag named [name] occurred. + /// + /// If a flag occurred more than once in the arguments, + /// this is the total number of counts. + /// + /// If the negated flag occurred, it resets the count to zero, + /// ignoring all prior occurrences. Later occurrences may still + /// increase the count again. + /// + /// If the default is to be enabled, the default count is `1` if there were + /// no occurrences of the flag. + /// + /// The [name] must be a valid flag name in the parser. + /// + /// The result is never negative. + int flagCount(String name) { + final option = _parser.options[name]; + if (option == null) { + throw ArgumentError.value( + name, 'name', 'Could not find a flag named "--$name".'); } if (!option.isFlag) { - throw ArgumentError('"$name" is not a flag.'); + throw ArgumentError.value(name, 'name', '"$name" is not a flag.'); } - return option.valueOrDefault(_parsed[name]) as bool; + return (_parsed[name] as int?) ?? + (option.valueOrDefault(null) as bool ? 1 : 0); } /// Returns the parsed or default command-line option named [name]. @@ -97,13 +133,14 @@ class ArgResults { String? option(String name) { final option = _parser.options[name]; if (option == null) { - throw ArgumentError('Could not find an option named "--$name".'); + throw ArgumentError.value( + name, 'name', 'Could not find an option named "--$name".'); } if (!option.isSingle) { - throw ArgumentError('"$name" is a multi-option.'); + throw ArgumentError.value(name, 'name', '"$name" is a multi-option.'); } if (option.mandatory && !_parsed.containsKey(name)) { - throw ArgumentError('Option $name is mandatory.'); + throw ArgumentError.value(name, 'name', 'Option $name is mandatory.'); } return option.valueOrDefault(_parsed[name]) as String?; } @@ -114,10 +151,11 @@ class ArgResults { List multiOption(String name) { var option = _parser.options[name]; if (option == null) { - throw ArgumentError('Could not find an option named "--$name".'); + throw ArgumentError.value( + name, 'name', 'Could not find an option named "--$name".'); } if (!option.isMultiple) { - throw ArgumentError('"$name" is not a multi-option.'); + throw ArgumentError.value(name, 'name', '"$name" is not a multi-option.'); } return option.valueOrDefault(_parsed[name]) as List; } @@ -146,7 +184,8 @@ class ArgResults { /// [name] must be a valid option name in the parser. bool wasParsed(String name) { if (!_parser.options.containsKey(name)) { - throw ArgumentError('Could not find an option named "--$name".'); + throw ArgumentError.value( + name, 'name', 'Could not find an option named "--$name".'); } return _parsed.containsKey(name); diff --git a/pkgs/args/lib/src/parser.dart b/pkgs/args/lib/src/parser.dart index 9aca0f21..0449769c 100644 --- a/pkgs/args/lib/src/parser.dart +++ b/pkgs/args/lib/src/parser.dart @@ -124,6 +124,7 @@ class Parser { // Check if mandatory and invoke existing callbacks. _grammar.options.forEach((name, option) { var parsedOption = _results[name]; + if (option.isFlag && parsedOption is int) parsedOption = parsedOption > 0; var callback = option.callback; if (callback == null) return; @@ -374,11 +375,15 @@ class Parser { } } - /// Validates and stores [value] as the value for [option], which must be a - /// flag. + /// Validates and increases or resets the count for [option]. + /// + /// If [value] is `false`, resets the option's value to zero. + /// If `true`, increases the count of occurrences of the [option], + /// which must be a flag. void _setFlag(Map results, Option option, bool value) { assert(option.isFlag); - results[option.name] = value; + results[option.name] = + value ? ((results[option.name] as int?) ?? 0) + 1 : 0; } /// Validates that [value] is allowed as a value of [option]. diff --git a/pkgs/args/pubspec.yaml b/pkgs/args/pubspec.yaml index d65ca0b2..8679ca9f 100644 --- a/pkgs/args/pubspec.yaml +++ b/pkgs/args/pubspec.yaml @@ -1,5 +1,5 @@ name: args -version: 2.8.0 +version: 2.9.0-wip description: >- Library for defining parsers for parsing raw command-line arguments into a set of options and values using GNU and POSIX style options. diff --git a/pkgs/args/test/parse_test.dart b/pkgs/args/test/parse_test.dart index eb2f3ae2..853a62d9 100644 --- a/pkgs/args/test/parse_test.dart +++ b/pkgs/args/test/parse_test.dart @@ -90,6 +90,50 @@ void main() { var results = parser.parse(['--a']); throwsIllegalArg(() => results.multiOption('a')); }); + + test('flagCount', () { + var parser = ArgParser(); + parser.addFlag('flag', abbr: 'f', defaultsTo: false, negatable: false); + parser.addFlag('negatable-flag', abbr: 'n', defaultsTo: false); + + // Flags not occurring for testing default value. + parser.addFlag('default-true', abbr: 'd', defaultsTo: true); + parser.addFlag('default-false', defaultsTo: false); + + var results = parser.parse([ + '--flag', + '--negatable-flag', + '-f', + '-n', + '-fn', + '--no-negatable-flag', // Resets `-n` count + '-nf', + '-f', + '-n', + '--flag', + '--negatable-flag', + ]); + + // Counts all occurrences. + expect(results.flagCount('flag'), 6); + expect(results.flag('flag'), true); + + // Reset at `--no-negatable-flag`, only counts those after. + expect(results.flagCount('negatable-flag'), 3); + expect(results.flag('negatable-flag'), true); + + expect(results.flagCount('default-true'), 1); + expect(results.flag('default-true'), true); + + expect(results.flagCount('default-false'), 0); + expect(results.flag('default-false'), false); + + // Reset works correctly as last occurrence, + // and doesn't fall back on default value. + results = parser.parse(['-d', '--no-default-true']); + expect(results.flagCount('default-true'), 0); + expect(results.flag('default-true'), false); + }); }); group('flag()', () {