Top-level --colors flag (#3467)

diff --git a/bin/dependency_services.dart b/bin/dependency_services.dart
index 3dabf5d..f117afd 100644
--- a/bin/dependency_services.dart
+++ b/bin/dependency_services.dart
@@ -46,6 +46,7 @@
             usageLineLength: lineLength) {
     argParser.addFlag('verbose',
         abbr: 'v', negatable: false, help: 'Shortcut for "--verbosity=all".');
+    PubTopLevel.addColorFlag(argParser);
     argParser.addOption(
       'directory',
       abbr: 'C',
diff --git a/lib/src/command.dart b/lib/src/command.dart
index a1a8a2e..ae942ea 100644
--- a/lib/src/command.dart
+++ b/lib/src/command.dart
@@ -178,7 +178,8 @@
   @override
   @nonVirtual
   FutureOr<int> run() async {
-    computeCommand(_pubTopLevel.argResults);
+    _computeCommand(_pubTopLevel.argResults);
+    _decideOnColors(_pubTopLevel.argResults);
 
     log.verbosity = _pubTopLevel.verbosity;
     log.fine('Pub ${sdk.version}');
@@ -300,7 +301,17 @@
   /// returned. For instance `install` becomes `get`.
   static final String command = _command ?? '';
 
-  static void computeCommand(ArgResults argResults) {
+  static void _decideOnColors(ArgResults argResults) {
+    if (!argResults.wasParsed('color')) {
+      forceColors = ForceColorOption.auto;
+    } else {
+      forceColors = argResults['color'] as bool
+          ? ForceColorOption.always
+          : ForceColorOption.never;
+    }
+  }
+
+  static void _computeCommand(ArgResults argResults) {
     var list = <String?>[];
     for (var command = argResults.command;
         command != null;
@@ -329,6 +340,17 @@
   bool get captureStackChains;
   log.Verbosity get verbosity;
   bool get trace;
+
+  static addColorFlag(ArgParser argParser) {
+    argParser.addFlag(
+      'color',
+      help: 'Use colors in terminal output.\n'
+          'Defaults to color when connected to a '
+          'terminal, and no-color otherwise.',
+    );
+  }
+
+  /// The directory containing the pubspec.yaml of the project to work on.
   String? get directory;
 
   /// The argResults from the level of parsing of the 'pub' command.
diff --git a/lib/src/command/outdated.dart b/lib/src/command/outdated.dart
index 601bdc8..59131c4 100644
--- a/lib/src/command/outdated.dart
+++ b/lib/src/command/outdated.dart
@@ -47,13 +47,6 @@
 
   OutdatedCommand() {
     argParser.addFlag(
-      'color',
-      help: 'Whether to color the output.\n'
-          'Defaults to color when connected to a '
-          'terminal, and no-color otherwise.',
-    );
-
-    argParser.addFlag(
       'dependency-overrides',
       defaultsTo: true,
       help: 'Show resolutions with `dependency_overrides`.',
@@ -270,14 +263,8 @@
         includeDevDependencies: includeDevDependencies,
       );
     } else {
-      if (argResults.wasParsed('color')) {
-        forceColors = argResults['color'];
-      }
-      final useColors =
-          argResults.wasParsed('color') ? argResults['color'] : canUseAnsiCodes;
-
       await _outputHuman(rows, mode,
-          useColors: useColors,
+          useColors: canUseAnsiCodes,
           showAll: showAll,
           includeDevDependencies: includeDevDependencies,
           lockFileExists: fileExists(entrypoint.lockFilePath),
diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart
index 21f568b..92d4cf8 100644
--- a/lib/src/command_runner.dart
+++ b/lib/src/command_runner.dart
@@ -121,6 +121,7 @@
     });
     argParser.addFlag('verbose',
         abbr: 'v', negatable: false, help: 'Shortcut for "--verbosity=all".');
+    PubTopLevel.addColorFlag(argParser);
     argParser.addOption(
       'directory',
       abbr: 'C',
@@ -155,7 +156,8 @@
   @override
   Future<int> run(Iterable<String> args) async {
     try {
-      _argResults = parse(args);
+      final argResults = parse(args);
+      _argResults = argResults;
       return await runCommand(argResults) ?? exit_codes.SUCCESS;
     } on UsageException catch (error) {
       log.exception(error);
diff --git a/lib/src/pub_embeddable_command.dart b/lib/src/pub_embeddable_command.dart
index 1c6cb48..c7c7f87 100644
--- a/lib/src/pub_embeddable_command.dart
+++ b/lib/src/pub_embeddable_command.dart
@@ -66,6 +66,7 @@
     argParser.addFlag('trace', hide: true);
     argParser.addFlag('verbose',
         abbr: 'v', negatable: false, help: 'Print detailed logging.');
+    PubTopLevel.addColorFlag(argParser);
     argParser.addOption(
       'directory',
       abbr: 'C',
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 3e1cf48..e4451db 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -389,8 +389,14 @@
 String _urlDecode(String encoded) =>
     Uri.decodeComponent(encoded.replaceAll('+', ' '));
 
-/// Set to `true` if ANSI colors should be output regardless of terminalD
-bool forceColors = false;
+enum ForceColorOption {
+  always,
+  never,
+  auto,
+}
+
+/// Change to decide if ANSI colors should be output regardless of terminalD.
+ForceColorOption forceColors = ForceColorOption.auto;
 
 /// Whether ansi codes such as color escapes are safe to use.
 ///
@@ -398,8 +404,18 @@
 ///
 /// Tests should make sure to run the subprocess with or without an attached
 /// terminal to decide if colors will be provided.
-bool get canUseAnsiCodes =>
-    forceColors || (stdout.hasTerminal && stdout.supportsAnsiEscapes);
+bool get canUseAnsiCodes {
+  switch (forceColors) {
+    case ForceColorOption.always:
+      return true;
+    case ForceColorOption.never:
+      return false;
+    case ForceColorOption.auto:
+      return (!Platform.environment.containsKey('NO_COLOR')) &&
+          stdout.hasTerminal &&
+          stdout.supportsAnsiEscapes;
+  }
+}
 
 /// Gets an ANSI escape if those are supported by stdout (or nothing).
 String getAnsi(String ansiCode) => canUseAnsiCodes ? ansiCode : '';
diff --git a/test/embedding/embedding_test.dart b/test/embedding/embedding_test.dart
index 676d8a1..73370d7 100644
--- a/test/embedding/embedding_test.dart
+++ b/test/embedding/embedding_test.dart
@@ -241,6 +241,23 @@
       workingDirectory: d.path('.'),
     );
   });
+
+  testWithGolden('--color forces colors', (context) async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0');
+    server.serve('foo', '2.0.0');
+    await d.appDir({'foo': '^1.0.0'}).create();
+    await context.runEmbedding(
+      ['pub', '--no-color', 'get'],
+      environment: getPubTestEnvironment(),
+      workingDirectory: d.path(appPath),
+    );
+    await context.runEmbedding(
+      ['pub', '--color', 'get'],
+      workingDirectory: d.path(appPath),
+      environment: getPubTestEnvironment(),
+    );
+  });
 }
 
 String _filter(String input) {
diff --git a/test/testdata/goldens/embedding/embedding_test/--color forces colors.txt b/test/testdata/goldens/embedding/embedding_test/--color forces colors.txt
new file mode 100644
index 0000000..d468889
--- /dev/null
+++ b/test/testdata/goldens/embedding/embedding_test/--color forces colors.txt
@@ -0,0 +1,15 @@
+# GENERATED BY: test/embedding/embedding_test.dart
+
+$ tool/test-bin/pub_command_runner.dart pub --no-color get
+Resolving dependencies...
++ foo 1.0.0 (2.0.0 available)
+Downloading foo 1.0.0...
+Changed 1 dependency!
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+$ tool/test-bin/pub_command_runner.dart pub --color get
+Resolving dependencies...
+  foo 1.0.0 (2.0.0 available)
+Got dependencies!
+
diff --git a/test/testdata/goldens/embedding/embedding_test/--help.txt b/test/testdata/goldens/embedding/embedding_test/--help.txt
index a7ebf2a..ea8fd3f 100644
--- a/test/testdata/goldens/embedding/embedding_test/--help.txt
+++ b/test/testdata/goldens/embedding/embedding_test/--help.txt
@@ -6,6 +6,9 @@
 Usage: pub_command_runner pub [arguments...]
 -h, --help   Print this usage information.
 -v, --verbose   Print detailed logging.
+   --[no-]color   Use colors in terminal output.
+   Defaults to color when connected to a terminal, and
+   no-color otherwise.
 -C, --directory=<dir>   Run the subcommand in the directory<dir>.
    (defaults to ".")
 
diff --git a/test/testdata/goldens/help_test/pub outdated --help.txt b/test/testdata/goldens/help_test/pub outdated --help.txt
index 9a7b1bc..d0f6bbe 100644
--- a/test/testdata/goldens/help_test/pub outdated --help.txt
+++ b/test/testdata/goldens/help_test/pub outdated --help.txt
@@ -6,9 +6,6 @@
 
 Usage: pub outdated [options]
 -h, --help                         Print this usage information.
-    --[no-]color                   Whether to color the output.
-                                   Defaults to color when connected to a
-                                   terminal, and no-color otherwise.
     --[no-]dependency-overrides    Show resolutions with `dependency_overrides`.
                                    (defaults to on)
     --[no-]dev-dependencies        Take dev dependencies into account.
diff --git a/test/testdata/goldens/outdated/outdated_test/does not allow arguments - handles bad flags.txt b/test/testdata/goldens/outdated/outdated_test/does not allow arguments - handles bad flags.txt
index 6ab6163..e9ae621 100644
--- a/test/testdata/goldens/outdated/outdated_test/does not allow arguments - handles bad flags.txt
+++ b/test/testdata/goldens/outdated/outdated_test/does not allow arguments - handles bad flags.txt
@@ -6,9 +6,6 @@
 [STDERR] 
 [STDERR] Usage: pub outdated [options]
 [STDERR] -h, --help                         Print this usage information.
-[STDERR]     --[no-]color                   Whether to color the output.
-[STDERR]                                    Defaults to color when connected to a
-[STDERR]                                    terminal, and no-color otherwise.
 [STDERR]     --[no-]dependency-overrides    Show resolutions with `dependency_overrides`.
 [STDERR]                                    (defaults to on)
 [STDERR]     --[no-]dev-dependencies        Take dev dependencies into account.
@@ -38,9 +35,6 @@
 [STDERR] 
 [STDERR] Usage: pub outdated [options]
 [STDERR] -h, --help                         Print this usage information.
-[STDERR]     --[no-]color                   Whether to color the output.
-[STDERR]                                    Defaults to color when connected to a
-[STDERR]                                    terminal, and no-color otherwise.
 [STDERR]     --[no-]dependency-overrides    Show resolutions with `dependency_overrides`.
 [STDERR]                                    (defaults to on)
 [STDERR]     --[no-]dev-dependencies        Take dev dependencies into account.