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.
diff --git a/pkgs/args/CHANGELOG.md b/pkgs/args/CHANGELOG.md
index 54985ff..cde2460 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 240867f..e95c94d 100644
--- a/pkgs/args/lib/src/arg_results.dart
+++ b/pkgs/args/lib/src/arg_results.dart
@@ -66,15 +66,18 @@
   /// > 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 @@
   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('"$name" is not a flag.');
+      throw ArgumentError.value(name, 'name', '"$name" is not a flag.');
     }
-    return option.valueOrDefault(_parsed[name]) as bool;
+    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.value(name, 'name', '"$name" is not a flag.');
+    }
+    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 @@
   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 @@
   List<String> 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<String>;
   }
@@ -146,7 +184,8 @@
   /// [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 9aca0f2..0449769 100644
--- a/pkgs/args/lib/src/parser.dart
+++ b/pkgs/args/lib/src/parser.dart
@@ -124,6 +124,7 @@
     // 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 @@
     }
   }
 
-  /// 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 d65ca0b..8679ca9 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 eb2f3ae..853a62d 100644
--- a/pkgs/args/test/parse_test.dart
+++ b/pkgs/args/test/parse_test.dart
@@ -90,6 +90,50 @@
         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()', () {