Allow (and prefer) using @ instead of : in `pub add` and `pub unpack` (#4759)

diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart
index 23ba02b..5d0c39c 100644
--- a/lib/src/command/add.dart
+++ b/lib/src/command/add.dart
@@ -56,7 +56,7 @@
 Make dependency overrides by prefixing with "override:".
 
 Add packages with specific constraints or other sources by giving a descriptor
-after a colon.
+after an `@`.
 
 For example (follow the same format including spaces):
   * Add a hosted dependency at newest compatible stable version:
@@ -64,27 +64,27 @@
   * Add a hosted dev dependency at newest compatible stable version:
     `$topLevelProgram pub add dev:foo`
   * Add a hosted dependency with the given constraint
-    `$topLevelProgram pub add foo:^1.2.3`
+    `$topLevelProgram pub add foo@^1.2.3`
   * Add multiple dependencies:
     `$topLevelProgram pub add foo dev:bar`
   * Add a dependency override:
-    `$topLevelProgram pub add override:foo:1.0.0`
+    `$topLevelProgram pub add override:foo@1.0.0`
   * Add a path dependency:
-    `$topLevelProgram pub add "foo:{path: ../foo}"`
+    `$topLevelProgram pub add "foo@{path: ../foo}"`
   * Add a hosted dependency:
-    `$topLevelProgram pub add "foo:{hosted: https://my-pub.dev}"`
+    `$topLevelProgram pub add "foo@{hosted: https://my-pub.dev}"`
   * Add an sdk dependency:
-    `$topLevelProgram pub add "foo:{sdk: flutter}"`
+    `$topLevelProgram pub add "foo@{sdk: flutter}"`
   * Add a git dependency:
-    `$topLevelProgram pub add "foo:{git: https://github.com/foo/foo}"`
+    `$topLevelProgram pub add "foo@{git: https://github.com/foo/foo}"`
   * Add a git dependency with a path and ref specified:
     `$topLevelProgram pub add \\
-      "foo:{git:{url: ../foo.git, ref: <branch>, path: <subdir>}}"`''';
+      "foo@{git:{url: ../foo.git, ref: <branch>, path: <subdir>}}"`''';
 
   @override
   String get argumentsDescription =>
-      '[options] [<section>:]<package>[:descriptor] '
-      '[<section>:]<package2>[:descriptor] ...]';
+      '[options] [<section>:]<package>[@<descriptor>] '
+      '[<section>:]<package2>[@<descriptor>] ...]';
 
   @override
   String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-add';
@@ -384,13 +384,13 @@
   static final _argRegExp = RegExp(
     r'^(?:(?<prefix>dev|override):)?'
     r'(?<name>[a-zA-Z0-9_.]+)'
-    r'(?::(?<descriptor>.*))?$',
+    r'(?:[:@](?<descriptor>.*))?$',
   );
 
   static final _lenientArgRegExp = RegExp(
     r'^(?:(?<prefix>[^:]*):)?'
-    r'(?<name>[^:]*)'
-    r'(?::(?<descriptor>.*))?$',
+    r'(?<name>[^:@]*)'
+    r'(?:[:@](?<descriptor>.*))?$',
   );
 
   /// Split [arg] on ':' and interpret it with the flags in [argResults] either
diff --git a/lib/src/command/unpack.dart b/lib/src/command/unpack.dart
index 9882120..f8fe7cb 100644
--- a/lib/src/command/unpack.dart
+++ b/lib/src/command/unpack.dart
@@ -34,7 +34,7 @@
 Downloads and extracts the latest stable version of package:foo from pub.dev
 in a directory `foo-<version>`.
 
-  $topLevelProgram pub unpack foo:1.2.3-pre --no-resolve
+  $topLevelProgram pub unpack foo@1.2.3-pre --no-resolve
 
 Downloads and extracts package:foo version 1.2.3-pre in a directory
 `foo-1.2.3-pre` without running implicit `pub get`.
@@ -44,14 +44,14 @@
 Downloads and extracts the latest stable version of package:foo in a directory
 `archives/foo-<version>`.
 
-  $topLevelProgram pub unpack 'foo:{hosted:"https://my_repo.org"}'
+  $topLevelProgram pub unpack 'foo@{hosted:"https://my_repo.org"}'
 
 Downloads and extracts the latest stable version of package:foo from my_repo.org
 in a directory `foo-<version>`.
 ''';
 
   @override
-  String get argumentsDescription => 'package-name[:descriptor]';
+  String get argumentsDescription => 'package-name[@<descriptor>]';
 
   @override
   String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-unpack';
@@ -81,7 +81,7 @@
 
   static final _argRegExp = RegExp(
     r'^(?<name>[a-zA-Z0-9_.]+)'
-    r'(?::(?<descriptor>.*))?$',
+    r'(?:[:@](?<descriptor>.*))?$',
   );
 
   @override
@@ -95,7 +95,7 @@
     final arg = argResults.rest[0];
     final match = _argRegExp.firstMatch(arg);
     if (match == null) {
-      usageException('Use the form package:descriptor to specify the package.');
+      usageException('Use the form package@descriptor to specify the package.');
     }
     final parseResult = _parseDescriptor(
       match.namedGroup('name')!,
diff --git a/test/add/common/add_test.dart b/test/add/common/add_test.dart
index 8457d86..ddf9186 100644
--- a/test/add/common/add_test.dart
+++ b/test/add/common/add_test.dart
@@ -21,7 +21,7 @@
     await d.appDir(dependencies: {}).create();
 
     await pubAdd(
-      args: ['bad name!:1.2.3'],
+      args: ['bad name!@1.2.3'],
       error: contains('Not a valid package name: "bad name!"'),
       exitCode: exit_codes.USAGE,
     );
@@ -39,7 +39,7 @@
 
     await d.appDir(dependencies: {}).create();
 
-    await pubAdd(args: ['fo_o1.a:{"path":"../foo"}']);
+    await pubAdd(args: ['fo_o1.a@{"path":"../foo"}']);
 
     await d.appPackageConfigFile([
       d.packageConfigEntry(name: 'fo_o1.a', path: '../foo'),
@@ -60,7 +60,7 @@
 
       await d.appDir(dependencies: {}).create();
 
-      await pubAdd(args: ['foo:1.2.3']);
+      await pubAdd(args: ['foo@1.2.3']);
 
       await d.cacheDir({'foo': '1.2.3'}).validate();
       await d.appPackageConfigFile([
@@ -77,7 +77,7 @@
 
       await d.appDir(dependencies: {}).create();
 
-      await pubAdd(args: ['foo:1.2.3', 'bar:1.1.0', 'baz:2.5.3']);
+      await pubAdd(args: ['foo@1.2.3', 'bar@1.1.0', 'baz@2.5.3']);
 
       await d.cacheDir({
         'foo': '1.2.3',
@@ -114,7 +114,7 @@
         '''),
       ]).create();
 
-      await pubAdd(args: ['foo:1.2.3']);
+      await pubAdd(args: ['foo@1.2.3']);
 
       await d.cacheDir({'foo': '1.2.3'}).validate();
       await d.appPackageConfigFile([
@@ -139,7 +139,7 @@
         await d.appDir(dependencies: {}).create();
 
         await pubAdd(
-          args: ['foo:1.2.3', '--dry-run'],
+          args: ['foo@1.2.3', '--dry-run'],
           output: allOf([
             contains('Would change 1 dependency'),
             contains('+ foo 1.2.3'),
@@ -167,7 +167,7 @@
 '''),
       ]).create();
 
-      await pubAdd(args: ['foo:1.2.3']);
+      await pubAdd(args: ['foo@1.2.3']);
       final yaml = loadYaml(
         File(p.join(d.sandbox, appPath, 'pubspec.yaml')).readAsStringSync(),
       );
@@ -197,7 +197,7 @@
           ),
         ]).create();
 
-        await pubAdd(args: ['foo:1.2.3']);
+        await pubAdd(args: ['foo@1.2.3']);
 
         final yaml = loadYaml(
           File(p.join(d.sandbox, appPath, 'pubspec.yaml')).readAsStringSync(),
@@ -244,7 +244,7 @@
         await d.appDir(dependencies: {'foo': '1.2.2'}).create();
 
         await pubAdd(
-          args: ['foo:1.2.3'],
+          args: ['foo@1.2.3'],
           output: contains(
             '"foo" is already in "dependencies". '
             'Will try to update the constraint.',
@@ -262,7 +262,7 @@
         await d.appDir(dependencies: {'foo': '1.2.2'}).create();
 
         await pubAdd(
-          args: ['foo:>=1.2.2'],
+          args: ['foo@>=1.2.2'],
           output: contains(
             '"foo" is already in "dependencies". '
             'Will try to update the constraint.',
@@ -291,7 +291,7 @@
       ]).create();
       await pubGet();
       await pubAdd(
-        args: ['foo:1.2.3'],
+        args: ['foo@1.2.3'],
         output: allOf(
           contains(
             '"foo" was found in dev_dependencies. Removing "foo" and '
@@ -336,7 +336,7 @@
         await pubGet();
 
         await pubAdd(
-          args: ['foo:1.2.3'],
+          args: ['foo@1.2.3'],
           output: allOf(
             contains(
               '"foo" was found in dev_dependencies. Removing "foo" and '
@@ -409,7 +409,7 @@
           }),
         ]).create();
 
-        await pubAdd(args: ['foo:1.2.3']);
+        await pubAdd(args: ['foo@1.2.3']);
 
         await d.dir(appPath, [
           d.pubspec({
@@ -440,7 +440,7 @@
           }),
         ]).create();
 
-        await pubAdd(args: ['foo:1.2.2']);
+        await pubAdd(args: ['foo@1.2.2']);
 
         await d.dir(appPath, [
           d.pubspec({
@@ -462,7 +462,7 @@
         ]).create();
 
         await pubAdd(
-          args: ['foo:one-two-three'],
+          args: ['foo@one-two-three'],
           exitCode: exit_codes.DATA,
           error: contains(
             'Invalid version constraint: Could '
@@ -491,7 +491,7 @@
         ]).create();
 
         await pubAdd(
-          args: ['foo:1.2.3'],
+          args: ['foo@1.2.3'],
           exitCode: exit_codes.DATA,
           error: contains(
             '"foo" resolved to "1.2.2" which does not satisfy constraint '
@@ -530,7 +530,7 @@
         ]).create();
 
         await pubAdd(
-          args: ['foo:1.2.3'],
+          args: ['foo@1.2.3'],
           exitCode: exit_codes.DATA,
           error: contains(
             '"foo" resolved to "1.0.0" which does not satisfy constraint '
@@ -572,7 +572,7 @@
           ]).create();
 
           await pubAdd(
-            args: ['foo:1.2.2'],
+            args: ['foo@1.2.2'],
             exitCode: exit_codes.DATA,
             error: contains(
               '"foo" resolved to "1.0.0" which does not satisfy constraint '
@@ -618,7 +618,7 @@
         d.pubspec({'name': 'myapp', 'dev_dependencies': {}}),
       ]).create();
 
-      await pubAdd(args: ['--dev', 'foo:1.2.3']);
+      await pubAdd(args: ['--dev', 'foo@1.2.3']);
 
       await d.appPackageConfigFile([
         d.packageConfigEntry(name: 'foo', version: '1.2.3'),
@@ -640,7 +640,7 @@
       ]).create();
 
       await pubAdd(
-        args: ['--dev', 'foo:{"path":../foo}'],
+        args: ['--dev', 'foo@{"path":../foo}'],
         error: contains(
           '--dev, --path, --sdk, --git-url, --git-path and --git-ref '
           'cannot be combined',
@@ -659,7 +659,7 @@
           d.pubspec({'name': 'myapp', 'dev_dependencies': {}}),
         ]).create();
 
-        await pubAdd(args: ['dev:foo:1.2.3']);
+        await pubAdd(args: ['dev:foo@1.2.3']);
 
         await d.appPackageConfigFile([
           d.packageConfigEntry(name: 'foo', version: '1.2.3'),
@@ -682,7 +682,7 @@
       ]).create();
 
       await pubAdd(
-        args: ['--dev', 'dev:foo:1.2.3'],
+        args: ['--dev', 'dev:foo@1.2.3'],
         error: contains("Cannot combine 'dev:' with --dev"),
         exitCode: exit_codes.USAGE,
       );
@@ -697,7 +697,7 @@
         d.pubspec({'name': 'myapp', 'dev_dependencies': {}}),
       ]).create();
 
-      await pubAdd(args: ['dev:foo:1.2.3', 'bar:1.2.3']);
+      await pubAdd(args: ['dev:foo@1.2.3', 'bar:1.2.3']);
 
       await d.appPackageConfigFile([
         d.packageConfigEntry(name: 'foo', version: '1.2.3'),
@@ -755,7 +755,7 @@
         ]).create();
 
         await pubAdd(
-          args: ['foo:1.2.3', '--dev'],
+          args: ['foo@1.2.3', '--dev'],
           output: contains(
             '"foo" is already in "dev_dependencies". '
             'Will try to update the constraint.',
@@ -783,7 +783,7 @@
         ]).create();
 
         await pubAdd(
-          args: ['foo:>=1.2.2', '--dev'],
+          args: ['foo@>=1.2.2', '--dev'],
           output: contains(
             '"foo" is already in "dev_dependencies". '
             'Will try to update the constraint.',
@@ -846,7 +846,7 @@
           }),
         ]).create();
 
-        await pubAdd(args: ['foo:1.2.3', '--dev']);
+        await pubAdd(args: ['foo@1.2.3', '--dev']);
 
         await d.dir(appPath, [
           d.pubspec({
@@ -877,7 +877,7 @@
           }),
         ]).create();
 
-        await pubAdd(args: ['foo:1.2.2', '--dev']);
+        await pubAdd(args: ['foo@1.2.2', '--dev']);
 
         await d.dir(appPath, [
           d.pubspec({
@@ -904,7 +904,7 @@
         ]).create();
 
         await pubAdd(
-          args: ['foo:1.2.3', '--dev'],
+          args: ['foo@1.2.3', '--dev'],
           exitCode: exit_codes.DATA,
           error: contains(
             '"foo" resolved to "1.2.2" which does not satisfy constraint '
@@ -943,7 +943,7 @@
         ]).create();
 
         await pubAdd(
-          args: ['foo:1.2.3'],
+          args: ['foo@1.2.3'],
           exitCode: exit_codes.DATA,
           error: contains(
             '"foo" resolved to "1.0.0" which does not satisfy constraint '
@@ -986,7 +986,7 @@
           ]).create();
 
           await pubAdd(
-            args: ['foo:1.2.2', '--dev'],
+            args: ['foo@1.2.2', '--dev'],
             exitCode: exit_codes.DATA,
             error: contains(
               '"foo" resolved to "1.0.0" which does not satisfy constraint '
@@ -1024,7 +1024,7 @@
       ]).create();
 
       await pubAdd(
-        args: ['foo:1.2.3', '--dev'],
+        args: ['foo@1.2.3', '--dev'],
         error: contains(
           '"foo" is already in "dependencies". Use '
           '"pub remove foo" to remove it before adding it to '
@@ -1130,7 +1130,7 @@
     );
 
     // Can override a transitive dependency.
-    await pubAdd(args: ['override:bar:2.0.0']);
+    await pubAdd(args: ['override:bar@2.0.0']);
     await d.dir(appPath, [
       d.file(
         'pubspec.yaml',
@@ -1142,7 +1142,7 @@
     ]).validate();
 
     // Can override with a descriptor:
-    await pubAdd(args: ['override:foo:{"path": "../local_foo"}']);
+    await pubAdd(args: ['override:foo@{"path": "../local_foo"}']);
 
     await d.dir(appPath, [
       d.file(
@@ -1206,4 +1206,10 @@
         )
         .validate();
   });
+  test('still supports : as separator', () async {
+    (await servePackages()).serve('foo', '1.0.0');
+    await d.appDir(dependencies: {}).create();
+    await pubAdd(args: ['foo:1.0.0']);
+    await d.appDir(dependencies: {'foo': '1.0.0'}).validate();
+  });
 }
diff --git a/test/add/common/version_constraint_test.dart b/test/add/common/version_constraint_test.dart
index 5e030fa..39b58e6 100644
--- a/test/add/common/version_constraint_test.dart
+++ b/test/add/common/version_constraint_test.dart
@@ -34,7 +34,7 @@
 
     await d.appDir(dependencies: {}).create();
 
-    await pubAdd(args: ['foo:1.2.3']);
+    await pubAdd(args: ['foo@1.2.3']);
 
     await d.cacheDir({'foo': '1.2.3'}).validate();
     await d.appPackageConfigFile([
@@ -149,7 +149,7 @@
       await d.appDir(dependencies: {'bar': '2.0.3'}).create();
 
       await pubAdd(
-        args: ['foo:1.2.3'],
+        args: ['foo@1.2.3'],
         error: contains(
           'Because every version of foo depends on bar 2.0.4 and myapp '
           'depends on bar 2.0.3, foo is forbidden.',
diff --git a/test/add/hosted/non_default_pub_server_test.dart b/test/add/hosted/non_default_pub_server_test.dart
index 505a21d..e2a7ba4 100644
--- a/test/add/hosted/non_default_pub_server_test.dart
+++ b/test/add/hosted/non_default_pub_server_test.dart
@@ -23,7 +23,7 @@
 
     final url = server.url;
 
-    await pubAdd(args: ['foo:1.2.3', '--hosted-url', url]);
+    await pubAdd(args: ['foo@1.2.3', '--hosted-url', url]);
 
     await d.cacheDir({'foo': '1.2.3'}, port: server.port).validate();
 
@@ -59,7 +59,7 @@
 
     final url = server.url;
 
-    await pubAdd(args: ['foo:1.2.3', '--hosted-url', url]);
+    await pubAdd(args: ['foo@1.2.3', '--hosted-url', url]);
 
     await d
         .appDir(
@@ -92,7 +92,7 @@
     final url = server.url;
 
     await pubAdd(
-      args: ['foo:1.2.3', 'bar:3.2.3', 'baz:1.3.5', '--hosted-url', url],
+      args: ['foo@1.2.3', 'bar:3.2.3', 'baz:1.3.5', '--hosted-url', url],
     );
 
     await d.cacheDir({
diff --git a/test/testdata/goldens/help_test/pub add --help.txt b/test/testdata/goldens/help_test/pub add --help.txt
index 4aca35f..482bd98 100644
--- a/test/testdata/goldens/help_test/pub add --help.txt
+++ b/test/testdata/goldens/help_test/pub add --help.txt
@@ -12,7 +12,7 @@
 Make dependency overrides by prefixing with "override:".
 
 Add packages with specific constraints or other sources by giving a descriptor
-after a colon.
+after an `@`.
 
 For example (follow the same format including spaces):
   * Add a hosted dependency at newest compatible stable version:
@@ -20,24 +20,24 @@
   * Add a hosted dev dependency at newest compatible stable version:
     `dart pub add dev:foo`
   * Add a hosted dependency with the given constraint
-    `dart pub add foo:^1.2.3`
+    `dart pub add foo@^1.2.3`
   * Add multiple dependencies:
     `dart pub add foo dev:bar`
   * Add a dependency override:
-    `dart pub add override:foo:1.0.0`
+    `dart pub add override:foo@1.0.0`
   * Add a path dependency:
-    `dart pub add "foo:{path: ../foo}"`
+    `dart pub add "foo@{path: ../foo}"`
   * Add a hosted dependency:
-    `dart pub add "foo:{hosted: https://my-pub.dev}"`
+    `dart pub add "foo@{hosted: https://my-pub.dev}"`
   * Add an sdk dependency:
-    `dart pub add "foo:{sdk: flutter}"`
+    `dart pub add "foo@{sdk: flutter}"`
   * Add a git dependency:
-    `dart pub add "foo:{git: https://github.com/foo/foo}"`
+    `dart pub add "foo@{git: https://github.com/foo/foo}"`
   * Add a git dependency with a path and ref specified:
     `dart pub add \
-      "foo:{git:{url: ../foo.git, ref: <branch>, path: <subdir>}}"`
+      "foo@{git:{url: ../foo.git, ref: <branch>, path: <subdir>}}"`
 
-Usage: pub add [options] [<section>:]<package>[:descriptor] [<section>:]<package2>[:descriptor] ...]
+Usage: pub add [options] [<section>:]<package>[@<descriptor>] [<section>:]<package2>[@<descriptor>] ...]
 -h, --help               Print this usage information.
     --[no-]offline       Use cached packages instead of accessing the network.
 -n, --dry-run            Report what dependencies would change but don't change any.
diff --git a/test/testdata/goldens/help_test/pub unpack --help.txt b/test/testdata/goldens/help_test/pub unpack --help.txt
index f96e8fa..97a5d14 100644
--- a/test/testdata/goldens/help_test/pub unpack --help.txt
+++ b/test/testdata/goldens/help_test/pub unpack --help.txt
@@ -11,7 +11,7 @@
 Downloads and extracts the latest stable version of package:foo from pub.dev
 in a directory `foo-<version>`.
 
-  dart pub unpack foo:1.2.3-pre --no-resolve
+  dart pub unpack foo@1.2.3-pre --no-resolve
 
 Downloads and extracts package:foo version 1.2.3-pre in a directory
 `foo-1.2.3-pre` without running implicit `pub get`.
@@ -21,13 +21,13 @@
 Downloads and extracts the latest stable version of package:foo in a directory
 `archives/foo-<version>`.
 
-  dart pub unpack 'foo:{hosted:"https://my_repo.org"}'
+  dart pub unpack 'foo@{hosted:"https://my_repo.org"}'
 
 Downloads and extracts the latest stable version of package:foo from my_repo.org
 in a directory `foo-<version>`.
 
 
-Usage: pub unpack package-name[:descriptor]
+Usage: pub unpack package-name[@<descriptor>]
 -h, --help          Print this usage information.
 -f, --[no-]force    Overwrite the target directory if it already exists.
 -o, --output        Download and extract the package in the specified directory.
diff --git a/test/unpack_test.dart b/test/unpack_test.dart
index c5f5528..5485e5f 100644
--- a/test/unpack_test.dart
+++ b/test/unpack_test.dart
@@ -19,7 +19,7 @@
     final server = await servePackages();
     server.serve('foo', '1.2.3');
     await runPub(
-      args: ['unpack', 'foo:1:2:3'],
+      args: ['unpack', 'foo@1:2:3'],
       error: contains(
         'Error on line 1, column 1 of descriptor: Invalid version constraint: '
         'Could not parse version "1:2:3". Unknown text at "1:2:3".',
@@ -28,7 +28,7 @@
     );
 
     await runPub(
-      args: ['unpack', 'foo:1.0'],
+      args: ['unpack', 'foo@1.0'],
       error:
           'Error on line 1, column 1 of descriptor: '
           'A dependency specification must be a string or a mapping.',
@@ -94,7 +94,7 @@
     );
 
     await runPub(
-      args: ['unpack', 'foo:1.2.3-pre', '--output=../'],
+      args: ['unpack', 'foo@1.2.3-pre', '--output=../'],
       output: allOf(
         contains('''
 Downloading foo 1.2.3-pre to `../foo-1.2.3-pre`...
@@ -110,7 +110,7 @@
     );
 
     await runPub(
-      args: ['unpack', 'foo:^0.1.0'],
+      args: ['unpack', 'foo@^0.1.0'],
       output: contains('Downloading foo 0.1.1 to `.${s}foo-0.1.1`...'),
     );
   });
@@ -121,7 +121,7 @@
     server.serve('foo', '1.0.0');
     server.serve('foo', '1.2.3');
     await runPub(
-      args: ['unpack', 'foo:{"hosted":"${server.url}", "version":"1.0.0"}'],
+      args: ['unpack', 'foo@{"hosted":"${server.url}", "version":"1.0.0"}'],
       output: contains('Downloading foo 1.0.0 to `.${s}foo-1.0.0`...'),
     );
   });
@@ -152,7 +152,7 @@
       ],
     );
     await runPub(
-      args: ['unpack', 'foo:1.0.0'],
+      args: ['unpack', 'foo@1.0.0'],
       output: allOf(
         contains('Downloading foo 1.0.0 to `.${s}foo-1.0.0`...'),
         contains('+ bar'),
@@ -163,4 +163,14 @@
       d.dir('foo-1.0.0', [d.file('pubspec_overrides.yaml', 'resolution:\n')]),
     ]).validate();
   });
+
+  test('still supports : as separator', () async {
+    await d.dir(appPath).create();
+    final server = await servePackages();
+    server.serve('foo', '1.2.3');
+    await runPub(
+      args: ['unpack', 'foo:1.2.3'],
+      output: contains('Downloading foo 1.2.3 to `.${s}foo-1.2.3`...'),
+    );
+  });
 }