Add --directory option (#2876)

To run a command in another directory from CWD
diff --git a/lib/src/command.dart b/lib/src/command.dart
index b6eaab1..313ea99 100644
--- a/lib/src/command.dart
+++ b/lib/src/command.dart
@@ -43,6 +43,8 @@
 /// of subcommands. Only leaf commands are ever actually invoked. If a command
 /// has subcommands, then one of those must always be chosen.
 abstract class PubCommand extends Command<int> {
+  String get directory => argResults['directory'] ?? _pubTopLevel.directory;
+
   SystemCache get cache => _cache ??= SystemCache(isOffline: isOffline);
 
   SystemCache _cache;
@@ -55,7 +57,7 @@
   ///
   /// This will load the pubspec and fail with an error if the current directory
   /// is not a package.
-  Entrypoint get entrypoint => _entrypoint ??= Entrypoint.current(cache);
+  Entrypoint get entrypoint => _entrypoint ??= Entrypoint(directory, cache);
 
   Entrypoint _entrypoint;
 
@@ -279,6 +281,7 @@
   bool get captureStackChains;
   log.Verbosity get verbosity;
   bool get trace;
+  String get directory;
 
   /// The argResults from the level of parsing of the 'pub' command.
   ArgResults get argResults;
diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart
index 9050273..fda2250 100644
--- a/lib/src/command/add.dart
+++ b/lib/src/command/add.dart
@@ -74,6 +74,8 @@
 
     argParser.addFlag('precompile',
         help: 'Precompile executables in immediate dependencies.');
+    argParser.addOption('directory',
+        abbr: 'C', help: 'Run this in the directory<dir>.', valueHelp: 'dir');
   }
 
   @override
@@ -96,6 +98,7 @@
       /// in case [package] was already a transitive dependency. In the case
       /// where the user specifies a version constraint, this serves to ensure
       /// that a resolution exists before we update pubspec.yaml.
+      // TODO(sigurdm): We should really use a spinner here.
       solveResult = await resolveVersions(
           SolveType.UPGRADE, cache, Package.inMemory(updatedPubSpec));
     } on GitException {
@@ -149,7 +152,7 @@
 
       /// Create a new [Entrypoint] since we have to reprocess the updated
       /// pubspec file.
-      await Entrypoint.current(cache).acquireDependencies(SolveType.GET,
+      await Entrypoint(directory, cache).acquireDependencies(SolveType.GET,
           precompile: argResults['precompile']);
     }
 
diff --git a/lib/src/command/deps.dart b/lib/src/command/deps.dart
index aea2e18..98e68a3 100644
--- a/lib/src/command/deps.dart
+++ b/lib/src/command/deps.dart
@@ -52,11 +52,14 @@
 
     argParser.addFlag('executables',
         negatable: false, help: 'List all available executables.');
+
+    argParser.addOption('directory',
+        abbr: 'C', help: 'Run this in the directory<dir>.', valueHelp: 'dir');
   }
 
   @override
   Future<void> runProtected() async {
-    // Explicitly run this in case we don't access `entrypoint.packageGraph`.
+    // Explicitly Run this in the directorycase we don't access `entrypoint.packageGraph`.
     entrypoint.assertUpToDate();
 
     _buffer = StringBuffer();
diff --git a/lib/src/command/downgrade.dart b/lib/src/command/downgrade.dart
index 3e75775..b46ce1d 100644
--- a/lib/src/command/downgrade.dart
+++ b/lib/src/command/downgrade.dart
@@ -33,6 +33,9 @@
         help: "Report what dependencies would change but don't change any.");
 
     argParser.addFlag('packages-dir', hide: true);
+
+    argParser.addOption('directory',
+        abbr: 'C', help: 'Run this in the directory<dir>.', valueHelp: 'dir');
   }
 
   @override
diff --git a/lib/src/command/get.dart b/lib/src/command/get.dart
index d6b0305..1f23806 100644
--- a/lib/src/command/get.dart
+++ b/lib/src/command/get.dart
@@ -32,6 +32,9 @@
         help: 'Precompile executables in immediate dependencies.');
 
     argParser.addFlag('packages-dir', hide: true);
+
+    argParser.addOption('directory',
+        abbr: 'C', help: 'Run this in the directory<dir>.', valueHelp: 'dir');
   }
 
   @override
diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart
index abc0c65..bbb2373 100644
--- a/lib/src/command/lish.dart
+++ b/lib/src/command/lish.dart
@@ -63,6 +63,9 @@
     argParser.addOption('server',
         help: 'The package server to which to upload this package.',
         hide: true);
+
+    argParser.addOption('directory',
+        abbr: 'C', help: 'Run this in the directory<dir>.', valueHelp: 'dir');
   }
 
   Future<void> _publish(List<int> packageBytes) async {
diff --git a/lib/src/command/list_package_dirs.dart b/lib/src/command/list_package_dirs.dart
index d0c5ed8..10fe5aa 100644
--- a/lib/src/command/list_package_dirs.dart
+++ b/lib/src/command/list_package_dirs.dart
@@ -26,6 +26,8 @@
   ListPackageDirsCommand() {
     argParser.addOption('format',
         help: 'How output should be displayed.', allowed: ['json']);
+    argParser.addOption('directory',
+        abbr: 'C', help: 'Run this in the directory<dir>.', valueHelp: 'dir');
   }
 
   @override
diff --git a/lib/src/command/outdated.dart b/lib/src/command/outdated.dart
index 632ff6f..843b306 100644
--- a/lib/src/command/outdated.dart
+++ b/lib/src/command/outdated.dart
@@ -8,6 +8,7 @@
 import 'dart:math';
 
 import 'package:meta/meta.dart';
+import 'package:path/path.dart' as path;
 import 'package:pub_semver/pub_semver.dart';
 
 import '../command.dart';
@@ -107,6 +108,8 @@
       help: 'Show transitive dependencies.\n'
           '(defaults to off in --mode=null-safety).',
     );
+    argParser.addOption('directory',
+        abbr: 'C', help: 'Run this in the directory<dir>.', valueHelp: 'dir');
   }
 
   @override
@@ -258,25 +261,23 @@
       final useColors =
           argResults.wasParsed('color') ? argResults['color'] : canUseAnsiCodes;
 
-      await _outputHuman(
-        rows,
-        mode,
-        useColors: useColors,
-        showAll: showAll,
-        includeDevDependencies: includeDevDependencies,
-        lockFileExists: fileExists(entrypoint.lockFilePath),
-        hasDirectDependencies: rootPubspec.dependencies.values.any(
-          // Test if it contains non-SDK dependencies
-          (c) => c.source is! SdkSource,
-        ),
-        hasDevDependencies: rootPubspec.devDependencies.values.any(
-          // Test if it contains non-SDK dependencies
-          (c) => c.source is! SdkSource,
-        ),
-        showTransitiveDependencies: showTransitiveDependencies,
-        hasUpgradableResolution: hasUpgradableResolution,
-        hasResolvableResolution: hasResolvableResolution,
-      );
+      await _outputHuman(rows, mode,
+          useColors: useColors,
+          showAll: showAll,
+          includeDevDependencies: includeDevDependencies,
+          lockFileExists: fileExists(entrypoint.lockFilePath),
+          hasDirectDependencies: rootPubspec.dependencies.values.any(
+            // Test if it contains non-SDK dependencies
+            (c) => c.source is! SdkSource,
+          ),
+          hasDevDependencies: rootPubspec.devDependencies.values.any(
+            // Test if it contains non-SDK dependencies
+            (c) => c.source is! SdkSource,
+          ),
+          showTransitiveDependencies: showTransitiveDependencies,
+          hasUpgradableResolution: hasUpgradableResolution,
+          hasResolvableResolution: hasResolvableResolution,
+          directory: path.normalize(directory));
     }
   }
 
@@ -448,11 +449,10 @@
   @required bool showTransitiveDependencies,
   @required bool hasUpgradableResolution,
   @required bool hasResolvableResolution,
+  @required String directory,
 }) async {
-  final explanation = mode.explanation;
-  if (explanation != null) {
-    log.message(explanation + '\n');
-  }
+  final directoryDesc = directory == '.' ? '' : ' in $directory';
+  log.message(mode.explanation(directoryDesc) + '\n');
   final markedRows =
       Map.fromIterables(rows, await mode.markVersionDetails(rows));
 
@@ -603,7 +603,7 @@
   Future<List<List<_MarkedVersionDetails>>> markVersionDetails(
       List<_PackageDetails> packageDetails);
 
-  String get explanation;
+  String explanation(String directoryDescription);
   String get foundNoBadText;
   String get allGood;
   String get noResolutionText;
@@ -614,8 +614,8 @@
 
 class _OutdatedMode implements Mode {
   @override
-  String get explanation => '''
-Showing outdated packages.
+  String explanation(String directoryDescription) => '''
+Showing outdated packages$directoryDescription.
 [${log.red('*')}] indicates versions that are not the latest available.
 ''';
 
@@ -692,8 +692,8 @@
       {@required this.shouldShowSpinner});
 
   @override
-  String get explanation => '''
-Showing dependencies that are currently not opted in to null-safety.
+  String explanation(String directoryDescription) => '''
+Showing dependencies$directoryDescription that are currently not opted in to null-safety.
 [${log.red(_notCompliantEmoji)}] indicates versions without null safety support.
 [${log.green(_compliantEmoji)}] indicates versions opting in to null safety.
 ''';
diff --git a/lib/src/command/remove.dart b/lib/src/command/remove.dart
index 9ed4ebe..b899988 100644
--- a/lib/src/command/remove.dart
+++ b/lib/src/command/remove.dart
@@ -41,6 +41,8 @@
 
     argParser.addFlag('precompile',
         help: 'Precompile executables in immediate dependencies.');
+    argParser.addOption('directory',
+        abbr: 'C', help: 'Run this in the directory<dir>.', valueHelp: 'dir');
   }
 
   @override
@@ -66,7 +68,7 @@
       /// Update the pubspec.
       _writeRemovalToPubspec(packages);
 
-      await Entrypoint.current(cache).acquireDependencies(SolveType.GET,
+      await Entrypoint(directory, cache).acquireDependencies(SolveType.GET,
           precompile: argResults['precompile']);
     }
   }
diff --git a/lib/src/command/run.dart b/lib/src/command/run.dart
index 7491d61..4d6e7c6 100644
--- a/lib/src/command/run.dart
+++ b/lib/src/command/run.dart
@@ -45,6 +45,8 @@
     argParser.addOption('mode', help: 'Deprecated option', hide: true);
     // mode exposed for `dartdev run` to use as subprocess.
     argParser.addFlag('dart-dev-run', hide: true);
+    argParser.addOption('directory',
+        abbr: 'C', help: 'Run this in the directory<dir>.', valueHelp: 'dir');
   }
 
   @override
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart
index 81aa747..499379e 100644
--- a/lib/src/command/upgrade.dart
+++ b/lib/src/command/upgrade.dart
@@ -62,6 +62,8 @@
           'and updates pubspec.yaml.',
       negatable: false,
     );
+    argParser.addOption('directory',
+        abbr: 'C', help: 'Run this in the directory<dir>.', valueHelp: 'dir');
   }
 
   /// Avoid showing spinning progress messages when not in a terminal.
@@ -218,7 +220,7 @@
       // TODO: Allow Entrypoint to be created with in-memory pubspec, so that
       //       we can show the changes when not in --dry-run mode. For now we only show
       //       the changes made to pubspec.yaml in dry-run mode.
-      await Entrypoint.current(cache).acquireDependencies(
+      await Entrypoint(directory, cache).acquireDependencies(
         SolveType.UPGRADE,
         precompile: _precompile,
       );
@@ -311,7 +313,7 @@
       // TODO: Allow Entrypoint to be created with in-memory pubspec, so that
       //       we can show the changes in --dry-run mode. For now we only show
       //       the changes made to pubspec.yaml in dry-run mode.
-      await Entrypoint.current(cache).acquireDependencies(
+      await Entrypoint(directory, cache).acquireDependencies(
         SolveType.UPGRADE,
         precompile: _precompile,
       );
diff --git a/lib/src/command/uploader.dart b/lib/src/command/uploader.dart
index 6fd09d2..99a6a57 100644
--- a/lib/src/command/uploader.dart
+++ b/lib/src/command/uploader.dart
@@ -35,6 +35,8 @@
     argParser.addOption('package',
         help: 'The package whose uploaders will be modified.\n'
             '(defaults to the current package)');
+    argParser.addOption('directory',
+        abbr: 'C', help: 'Run this in the directory<dir>.', valueHelp: 'dir');
   }
 
   @override
diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart
index f1ecb4d..8210f42 100644
--- a/lib/src/command_runner.dart
+++ b/lib/src/command_runner.dart
@@ -44,6 +44,9 @@
 
 class PubCommandRunner extends CommandRunner<int> implements PubTopLevel {
   @override
+  String get directory => _argResults['directory'];
+
+  @override
   bool get captureStackChains {
     return _argResults['trace'] ||
         _argResults['verbose'] ||
@@ -109,6 +112,13 @@
     });
     argParser.addFlag('verbose',
         abbr: 'v', negatable: false, help: 'Shortcut for "--verbosity=all".');
+    argParser.addOption(
+      'directory',
+      abbr: 'C',
+      help: 'Run the subcommand in the directory<dir>.',
+      defaultsTo: '.',
+      valueHelp: 'dir',
+    );
 
     // When adding new commands be sure to also add them to
     // `pub_embeddable_command.dart`.
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 55a56b3..127bc0a 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -169,11 +169,6 @@
   /// The path to the directory containing dependency executable snapshots.
   String get _snapshotPath => p.join(cachePath, 'bin');
 
-  /// Loads the entrypoint for the package at the current directory.
-  Entrypoint.current(this.cache)
-      : root = Package.load(null, '.', cache.sources),
-        isGlobal = false;
-
   /// Loads the entrypoint from a package at [rootDir].
   Entrypoint(String rootDir, this.cache)
       : root = Package.load(null, rootDir, cache.sources),
diff --git a/lib/src/pub_embeddable_command.dart b/lib/src/pub_embeddable_command.dart
index 6b147fc..3a56d9a 100644
--- a/lib/src/pub_embeddable_command.dart
+++ b/lib/src/pub_embeddable_command.dart
@@ -32,11 +32,21 @@
   @override
   String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-global';
 
+  @override
+  String get directory => argResults['directory'];
+
   PubEmbeddableCommand() : super() {
     argParser.addFlag('trace',
         help: 'Print debugging information when an error occurs.');
     argParser.addFlag('verbose',
         abbr: 'v', negatable: false, help: 'Shortcut for "--verbosity=all".');
+    argParser.addOption(
+      'directory',
+      abbr: 'C',
+      help: 'Run the subcommand in the directory<dir>.',
+      defaultsTo: '.',
+      valueHelp: 'dir',
+    );
     // This list is intentionally shorter than the one in
     // pub_command_runner.dart.
     //
diff --git a/lib/src/solver/report.dart b/lib/src/solver/report.dart
index 39f005f..3fd18cf 100644
--- a/lib/src/solver/report.dart
+++ b/lib/src/solver/report.dart
@@ -2,6 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'package:path/path.dart' as path;
 import 'package:pub_semver/pub_semver.dart';
 
 import '../command_runner.dart';
@@ -70,25 +71,33 @@
       return oldId != newId;
     }).length;
 
+    var suffix = '';
+    if (_root.dir != null) {
+      final dir = path.normalize(_root.dir);
+      if (dir != '.') {
+        suffix = ' in $dir';
+      }
+    }
+
     if (dryRun) {
       if (numChanged == 0) {
-        log.message('No dependencies would change.');
+        log.message('No dependencies would change$suffix.');
       } else if (numChanged == 1) {
-        log.message('Would change $numChanged dependency.');
+        log.message('Would change $numChanged dependency$suffix.');
       } else {
-        log.message('Would change $numChanged dependencies.');
+        log.message('Would change $numChanged dependencies$suffix.');
       }
     } else {
       if (numChanged == 0) {
         if (_type == SolveType.GET) {
-          log.message('Got dependencies!');
+          log.message('Got dependencies$suffix!');
         } else {
-          log.message('No dependencies changed.');
+          log.message('No dependencies changed$suffix.');
         }
       } else if (numChanged == 1) {
-        log.message('Changed $numChanged dependency!');
+        log.message('Changed $numChanged dependency$suffix!');
       } else {
-        log.message('Changed $numChanged dependencies!');
+        log.message('Changed $numChanged dependencies$suffix!');
       }
     }
   }
diff --git a/test/directory_option_test.dart b/test/directory_option_test.dart
new file mode 100644
index 0000000..cb3398d
--- /dev/null
+++ b/test/directory_option_test.dart
@@ -0,0 +1,92 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:shelf/shelf.dart' as shelf;
+import 'descriptor.dart';
+import 'golden_file.dart';
+import 'test_pub.dart';
+
+Future<void> main() async {
+  test('commands taking a --directory/-C parameter work', () async {
+    await servePackages((b) =>
+        b..serve('foo', '1.0.0')..serve('foo', '0.1.2')..serve('bar', '1.2.3'));
+    await credentialsFile(globalPackageServer, 'access token').create();
+    globalPackageServer
+        .extraHandlers[RegExp('/api/packages/test_pkg/uploaders')] = (request) {
+      return shelf.Response.ok(
+          jsonEncode({
+            'success': {'message': 'Good job!'}
+          }),
+          headers: {'content-type': 'application/json'});
+    };
+
+    await validPackage.create();
+    await dir(appPath, [
+      dir('bin', [
+        file('app.dart', '''
+main() => print('Hi');    
+''')
+      ]),
+      dir('example', [
+        pubspec({
+          'name': 'example',
+          'environment': {'sdk': '>=1.2.0 <2.0.0'},
+          'dependencies': {
+            'test_pkg': {'path': '../'}
+          }
+        })
+      ]),
+      dir('example2', [
+        pubspec({
+          'name': 'example',
+          'environment': {'sdk': '>=1.2.0 <2.0.0'},
+          'dependencies': {
+            'myapp': {'path': '../'} // Wrong name of dependency
+          }
+        })
+      ]),
+    ]).create();
+    final buffer = StringBuffer();
+    Future<void> run(List<String> args) async {
+      await runPubIntoBuffer(
+        args,
+        buffer,
+        workingDirectory: sandbox,
+        environment: {'_PUB_TEST_SDK_VERSION': '1.12.0'},
+      );
+    }
+
+    await run(['add', '--directory=$appPath', 'foo']);
+    // Try the top-level version also.
+    await run(['-C', appPath, 'add', 'bar']);
+    // When both top-level and after command, the one after command takes
+    // precedence.
+    await run([
+      '-C',
+      p.join(appPath, 'example'),
+      'get',
+      '--directory=$appPath',
+      'bar',
+    ]);
+    await run(['remove', 'bar', '-C', appPath]);
+    await run(['get', 'bar', '-C', appPath]);
+    await run(['get', 'bar', '-C', '$appPath/example']);
+    await run(['get', 'bar', '-C', '$appPath/example2']);
+    await run(['get', 'bar', '-C', '$appPath/broken_dir']);
+    await run(['downgrade', '-C', appPath]);
+    await run(['upgrade', 'bar', '-C', appPath]);
+    await run(['run', '-C', appPath, 'bin/app.dart']);
+    await run(['publish', '-C', appPath, '--dry-run']);
+    await run(['uploader', '-C', appPath, 'add', 'sigurdm@google.com']);
+    await run(['deps', '-C', appPath]);
+    // TODO(sigurdm): we should also test `list-package-dirs` - it is a bit
+    // hard on windows due to quoted back-slashes on windows.
+    expectMatchesGoldenFile(
+        buffer.toString(), 'test/goldens/directory_option.txt');
+  });
+}
diff --git a/test/embedding/goldens/helptext.txt b/test/embedding/goldens/helptext.txt
index 864e30f..c06b931 100644
--- a/test/embedding/goldens/helptext.txt
+++ b/test/embedding/goldens/helptext.txt
@@ -28,9 +28,11 @@
 [E] Missing subcommand for "pub_command_runner pub".
 [E] 
 [E] Usage: pub_command_runner pub [arguments...]
-[E] -h, --help          Print this usage information.
-[E]     --[no-]trace    Print debugging information when an error occurs.
-[E] -v, --verbose       Shortcut for "--verbosity=all".
+[E] -h, --help               Print this usage information.
+[E]     --[no-]trace         Print debugging information when an error occurs.
+[E] -v, --verbose            Shortcut for "--verbosity=all".
+[E] -C, --directory=<dir>    Run the subcommand in the directory<dir>.
+[E]                          (defaults to ".")
 [E] 
 [E] Available subcommands:
 [E]   add         Add a dependency to pubspec.yaml.
@@ -54,9 +56,11 @@
 Work with packages.
 
 Usage: pub_command_runner pub [arguments...]
--h, --help          Print this usage information.
-    --[no-]trace    Print debugging information when an error occurs.
--v, --verbose       Shortcut for "--verbosity=all".
+-h, --help               Print this usage information.
+    --[no-]trace         Print debugging information when an error occurs.
+-v, --verbose            Shortcut for "--verbosity=all".
+-C, --directory=<dir>    Run the subcommand in the directory<dir>.
+                         (defaults to ".")
 
 Available subcommands:
   add         Add a dependency to pubspec.yaml.
@@ -85,6 +89,7 @@
 -n, --dry-run            Report what dependencies would change but don't change
                          any.
     --[no-]precompile    Precompile executables in immediate dependencies.
+-C, --directory=<dir>    Run this in the directory<dir>.
 
 Run "pub_command_runner help" to see global options.
 See https://dart.dev/tools/pub/cmd/pub-get for detailed documentation.
diff --git a/test/goldens/directory_option.txt b/test/goldens/directory_option.txt
new file mode 100644
index 0000000..0465b6b
--- /dev/null
+++ b/test/goldens/directory_option.txt
@@ -0,0 +1,81 @@
+$ pub add --directory=myapp foo
+Resolving dependencies...
++ foo 1.0.0
+Changed 1 dependency in myapp!
+
+$ pub -C myapp add bar
+Resolving dependencies...
++ bar 1.2.3
+Changed 1 dependency in myapp!
+
+$ pub -C myapp/example get --directory=myapp bar
+Resolving dependencies...
+Got dependencies in myapp!
+
+$ pub remove bar -C myapp
+Resolving dependencies...
+These packages are no longer being depended on:
+- bar 1.2.3
+Changed 1 dependency in myapp!
+
+$ pub get bar -C myapp
+Resolving dependencies...
+Got dependencies in myapp!
+
+$ pub get bar -C myapp/example
+Resolving dependencies...
++ foo 1.0.0
++ test_pkg 1.0.0 from path myapp
+Changed 2 dependencies in myapp/example!
+
+$ pub get bar -C myapp/example2
+Resolving dependencies...
+[ERR] Error on line 1, column 9 of myapp/pubspec.yaml: "name" field doesn't match expected name "myapp".
+[ERR]   ╷
+[ERR] 1 │ {"name":"test_pkg","version":"1.0.0","homepage":"http://pub.dartlang.org","description":"A package, I guess.","environment":{"sdk":">=1.8.0 <=2.0.0"}, dependencies: { foo: ^1.0.0}}
+[ERR]   │         ^^^^^^^^^^
+[ERR]   ╵
+[Exit code] 65
+
+$ pub get bar -C myapp/broken_dir
+[ERR] Could not find a file named "pubspec.yaml" in "$SANDBOX/myapp/broken_dir".
+[Exit code] 66
+
+$ pub downgrade -C myapp
+Resolving dependencies...
+  foo 1.0.0
+No dependencies changed in myapp.
+
+$ pub upgrade bar -C myapp
+Resolving dependencies...
+  foo 1.0.0
+No dependencies changed in myapp.
+
+$ pub run -C myapp bin/app.dart
+Hi
+
+$ pub publish -C myapp --dry-run
+Publishing test_pkg 1.0.0 to http://localhost:$PORT:
+|-- CHANGELOG.md
+|-- LICENSE
+|-- README.md
+|-- bin
+|   '-- app.dart
+|-- example
+|   '-- pubspec.yaml
+|-- example2
+|   '-- pubspec.yaml
+|-- lib
+|   '-- test_pkg.dart
+'-- pubspec.yaml
+[ERR] 
+[ERR] Package has 0 warnings.
+
+$ pub uploader -C myapp add sigurdm@google.com
+Good job!
+
+$ pub deps -C myapp
+Dart SDK 1.12.0
+test_pkg 1.0.0
+'-- foo 1.0.0
+
diff --git a/test/lish/force_cannot_be_combined_with_dry_run_test.dart b/test/lish/force_cannot_be_combined_with_dry_run_test.dart
index ea46ead..15456e0 100644
--- a/test/lish/force_cannot_be_combined_with_dry_run_test.dart
+++ b/test/lish/force_cannot_be_combined_with_dry_run_test.dart
@@ -16,9 +16,10 @@
 Cannot use both --force and --dry-run.
 
 Usage: pub publish [options]
--h, --help       Print this usage information.
--n, --dry-run    Validate but do not publish the package.
--f, --force      Publish without confirmation if there are no errors.
+-h, --help               Print this usage information.
+-n, --dry-run            Validate but do not publish the package.
+-f, --force              Publish without confirmation if there are no errors.
+-C, --directory=<dir>    Run this in the directory<dir>.
 
 Run "pub help" to see global options.
 See https://dart.dev/tools/pub/cmd/pub-lish for detailed documentation.
diff --git a/test/outdated/goldens/bad_arguments.txt b/test/outdated/goldens/bad_arguments.txt
index 1c0f1a7..b184530 100644
--- a/test/outdated/goldens/bad_arguments.txt
+++ b/test/outdated/goldens/bad_arguments.txt
@@ -21,6 +21,7 @@
 [ERR]                                    fullfilling --mode.
 [ERR]     --[no-]transitive              Show transitive dependencies.
 [ERR]                                    (defaults to off in --mode=null-safety).
+[ERR] -C, --directory=<dir>              Run this in the directory<dir>.
 [ERR] 
 [ERR] Run "pub help" to see global options.
 [ERR] See https://dart.dev/tools/pub/cmd/pub-outdated for detailed documentation.
@@ -49,6 +50,7 @@
 [ERR]                                    fullfilling --mode.
 [ERR]     --[no-]transitive              Show transitive dependencies.
 [ERR]                                    (defaults to off in --mode=null-safety).
+[ERR] -C, --directory=<dir>              Run this in the directory<dir>.
 [ERR] 
 [ERR] Run "pub help" to see global options.
 [ERR] See https://dart.dev/tools/pub/cmd/pub-outdated for detailed documentation.
diff --git a/test/outdated/goldens/helptext.txt b/test/outdated/goldens/helptext.txt
index 85e8836..20a4ecc 100644
--- a/test/outdated/goldens/helptext.txt
+++ b/test/outdated/goldens/helptext.txt
@@ -21,6 +21,7 @@
                                    fullfilling --mode.
     --[no-]transitive              Show transitive dependencies.
                                    (defaults to off in --mode=null-safety).
+-C, --directory=<dir>              Run this in the directory<dir>.
 
 Run "pub help" to see global options.
 See https://dart.dev/tools/pub/cmd/pub-outdated for detailed documentation.
diff --git a/test/outdated/goldens/no_pubspec.txt b/test/outdated/goldens/no_pubspec.txt
index 85afd8b..ec3a40a 100644
--- a/test/outdated/goldens/no_pubspec.txt
+++ b/test/outdated/goldens/no_pubspec.txt
@@ -1,4 +1,4 @@
-$ pub outdated 
+$ pub outdated
 [ERR] Could not find a file named "pubspec.yaml" in "$SANDBOX/myapp".
 [Exit code] 66
 
diff --git a/test/outdated/outdated_test.dart b/test/outdated/outdated_test.dart
index a3210ff..1c0a28d 100644
--- a/test/outdated/outdated_test.dart
+++ b/test/outdated/outdated_test.dart
@@ -2,61 +2,31 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-import 'dart:io';
-
 import 'package:test/test.dart';
 import '../descriptor.dart' as d;
 import '../golden_file.dart';
 import '../test_pub.dart';
 
-/// Runs `pub outdated [args]` and appends the output to [buffer].
-Future<void> runPubOutdated(List<String> args, StringBuffer buffer,
-    {Map<String, String> environment}) async {
-  final process =
-      await startPub(args: ['outdated', ...args], environment: environment);
-  final exitCode = await process.exitCode;
-
-  buffer.writeln([
-    '\$ pub outdated ${args.join(' ')}',
-    ...await process.stdout.rest.where((line) {
-      // Downloading order is not deterministic, so to avoid flakiness we filter
-      // out these lines.
-      return !line.startsWith('Downloading ');
-    }).toList(),
-  ].join('\n'));
-  final stderrLines = await process.stderr.rest.toList();
-  for (final line in stderrLines) {
-    final sanitized = line
-        .replaceAll(d.sandbox, r'$SANDBOX')
-        .replaceAll(Platform.pathSeparator, '/');
-    buffer.writeln('[ERR] $sanitized');
-  }
-  if (exitCode != 0) {
-    buffer.writeln('[Exit code] $exitCode');
-  }
-  buffer.write('\n');
-}
-
 /// Try running 'pub outdated' with a number of different sets of arguments.
 ///
 /// Compare the stdout and stderr output to the file in goldens/$[name].
 Future<void> variations(String name, {Map<String, String> environment}) async {
   final buffer = StringBuffer();
   for (final args in [
-    ['--json'],
-    ['--no-color'],
-    ['--no-color', '--no-transitive'],
-    ['--no-color', '--up-to-date'],
-    ['--no-color', '--prereleases'],
-    ['--no-color', '--no-dev-dependencies'],
-    ['--no-color', '--no-dependency-overrides'],
-    ['--no-color', '--mode=null-safety'],
-    ['--no-color', '--mode=null-safety', '--transitive'],
-    ['--no-color', '--mode=null-safety', '--no-prereleases'],
-    ['--json', '--mode=null-safety'],
-    ['--json', '--no-dev-dependencies'],
+    ['outdated', '--json'],
+    ['outdated', '--no-color'],
+    ['outdated', '--no-color', '--no-transitive'],
+    ['outdated', '--no-color', '--up-to-date'],
+    ['outdated', '--no-color', '--prereleases'],
+    ['outdated', '--no-color', '--no-dev-dependencies'],
+    ['outdated', '--no-color', '--no-dependency-overrides'],
+    ['outdated', '--no-color', '--mode=null-safety'],
+    ['outdated', '--no-color', '--mode=null-safety', '--transitive'],
+    ['outdated', '--no-color', '--mode=null-safety', '--no-prereleases'],
+    ['outdated', '--json', '--mode=null-safety'],
+    ['outdated', '--json', '--no-dev-dependencies'],
   ]) {
-    await runPubOutdated(args, buffer, environment: environment);
+    await runPubIntoBuffer(args, buffer, environment: environment);
   }
   // The easiest way to update the golden files is to delete them and rerun the
   // test.
@@ -66,7 +36,10 @@
 Future<void> main() async {
   test('help text', () async {
     final buffer = StringBuffer();
-    await runPubOutdated(['--help'], buffer);
+    await runPubIntoBuffer(
+      ['outdated', '--help'],
+      buffer,
+    );
     expectMatchesGoldenFile(
         buffer.toString(), 'test/outdated/goldens/helptext.txt');
   });
@@ -74,10 +47,7 @@
   test('no pubspec', () async {
     await d.dir(appPath, []).create();
     final buffer = StringBuffer();
-    await runPubOutdated(
-      [],
-      buffer,
-    );
+    await runPubIntoBuffer(['outdated'], buffer);
     expectMatchesGoldenFile(
         buffer.toString(), 'test/outdated/goldens/no_pubspec.txt');
   });
@@ -469,8 +439,8 @@
 
   test("doesn't allow arguments. Handles bad flags", () async {
     final sb = StringBuffer();
-    await runPubOutdated(['random_argument'], sb);
-    await runPubOutdated(['--bad_flag'], sb);
+    await runPubIntoBuffer(['outdated', 'random_argument'], sb);
+    await runPubIntoBuffer(['outdated', '--bad_flag'], sb);
     expectMatchesGoldenFile(
         sb.toString(), 'test/outdated/goldens/bad_arguments.txt');
   });
diff --git a/test/pub_uploader_test.dart b/test/pub_uploader_test.dart
index f1967e0..92f5a06 100644
--- a/test/pub_uploader_test.dart
+++ b/test/pub_uploader_test.dart
@@ -17,9 +17,10 @@
 Manage uploaders for a package on pub.dartlang.org.
 
 Usage: pub uploader [options] {add/remove} <email>
--h, --help       Print this usage information.
-    --package    The package whose uploaders will be modified.
-                 (defaults to the current package)
+-h, --help               Print this usage information.
+    --package            The package whose uploaders will be modified.
+                         (defaults to the current package)
+-C, --directory=<dir>    Run this in the directory<dir>.
 
 Run "pub help" to see global options.
 See https://dart.dev/tools/pub/cmd/pub-uploader for detailed documentation.
diff --git a/test/run/dartdev/errors_if_path_in_dependency_test.dart b/test/run/dartdev/errors_if_path_in_dependency_test.dart
index fcd8d0c..38b2e74 100644
--- a/test/run/dartdev/errors_if_path_in_dependency_test.dart
+++ b/test/run/dartdev/errors_if_path_in_dependency_test.dart
@@ -33,6 +33,7 @@
                                         slower startup).
     --[no-]sound-null-safety            Override the default null safety
                                         execution mode.
+-C, --directory=<dir>                   Run this in the directory<dir>.
 
 Run "pub help" to see global options.
 See https://dart.dev/tools/pub/cmd/pub-run for detailed documentation.
diff --git a/test/run/errors_if_no_executable_is_given_test.dart b/test/run/errors_if_no_executable_is_given_test.dart
index f3195fa..44fdf4b 100644
--- a/test/run/errors_if_no_executable_is_given_test.dart
+++ b/test/run/errors_if_no_executable_is_given_test.dart
@@ -25,6 +25,7 @@
                                         slower startup).
     --[no-]sound-null-safety            Override the default null safety
                                         execution mode.
+-C, --directory=<dir>                   Run this in the directory<dir>.
 
 Run "pub help" to see global options.
 See https://dart.dev/tools/pub/cmd/pub-run for detailed documentation.
diff --git a/test/run/errors_if_path_in_dependency_test.dart b/test/run/errors_if_path_in_dependency_test.dart
index 76cbf79..ff7edb8 100644
--- a/test/run/errors_if_path_in_dependency_test.dart
+++ b/test/run/errors_if_path_in_dependency_test.dart
@@ -33,6 +33,7 @@
                                         slower startup).
     --[no-]sound-null-safety            Override the default null safety
                                         execution mode.
+-C, --directory=<dir>                   Run this in the directory<dir>.
 
 Run "pub help" to see global options.
 See https://dart.dev/tools/pub/cmd/pub-run for detailed documentation.
diff --git a/test/test_pub.dart b/test/test_pub.dart
index 0e8bbd4..f780f3b 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -841,3 +841,48 @@
 
 /// A [StreamMatcher] that matches multiple lines of output.
 StreamMatcher emitsLines(String output) => emitsInOrder(output.split('\n'));
+
+Iterable<String> _filter(List<String> input) {
+  return input
+      // Downloading order is not deterministic, so to avoid flakiness we filter
+      // out these lines.
+      .where((line) => !line.startsWith('Downloading '))
+      // Any paths in output should be relative to the sandbox and with forward
+      // slashes to be stable across platforms.
+      .map((line) {
+    line = line
+        .replaceAll(d.sandbox, r'$SANDBOX')
+        .replaceAll(Platform.pathSeparator, '/');
+    if (globalPackageServer != null) {
+      line = line.replaceAll(globalPackageServer.port.toString(), '\$PORT');
+    }
+    return line;
+  });
+}
+
+/// Runs `pub outdated [args]` and appends the output to [buffer].
+Future<void> runPubIntoBuffer(
+  List<String> args,
+  StringBuffer buffer, {
+  Map<String, String> environment,
+  String workingDirectory,
+}) async {
+  final process = await startPub(
+    args: args,
+    environment: environment,
+    workingDirectory: workingDirectory,
+  );
+  final exitCode = await process.exitCode;
+
+  buffer.writeln(_filter([
+    '\$ pub ${args.join(' ')}',
+    ...await process.stdout.rest.toList(),
+  ]).join('\n'));
+  for (final line in _filter(await process.stderr.rest.toList())) {
+    buffer.writeln('[ERR] $line');
+  }
+  if (exitCode != 0) {
+    buffer.writeln('[Exit code] $exitCode');
+  }
+  buffer.write('\n');
+}