diff --git a/src/Cli/dotnet/Commands/Add/Package/AddPackageParser.cs b/src/Cli/dotnet/Commands/Add/Package/AddPackageParser.cs
index 2251ca12a2fa..053d9dbadb89 100644
--- a/src/Cli/dotnet/Commands/Add/Package/AddPackageParser.cs
+++ b/src/Cli/dotnet/Commands/Add/Package/AddPackageParser.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.CommandLine;
+using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Tools.Package.Add;
using LocalizableStrings = Microsoft.DotNet.Tools.Package.Add.LocalizableStrings;
@@ -30,7 +31,19 @@ private static CliCommand ConstructCommand()
command.Options.Add(PackageAddCommandParser.PrereleaseOption);
command.Options.Add(PackageCommandParser.ProjectOption);
- command.SetAction((parseResult) => new AddPackageReferenceCommand(parseResult).Execute());
+ command.SetAction((parseResult) =>
+ {
+ // this command can be called with an argument or an option for the project path - we prefer the option.
+ // if the option is not present, we use the argument value instead.
+ if (parseResult.HasOption(PackageCommandParser.ProjectOption))
+ {
+ return new AddPackageReferenceCommand(parseResult, parseResult.GetValue(PackageCommandParser.ProjectOption)).Execute();
+ }
+ else
+ {
+ return new AddPackageReferenceCommand(parseResult, parseResult.GetValue(AddCommandParser.ProjectArgument)).Execute();
+ }
+ });
return command;
}
diff --git a/src/Cli/dotnet/Commands/New/DotnetCommandCallbacks.cs b/src/Cli/dotnet/Commands/New/DotnetCommandCallbacks.cs
index 7ff86966b2a0..8d5ff20de3d7 100644
--- a/src/Cli/dotnet/Commands/New/DotnetCommandCallbacks.cs
+++ b/src/Cli/dotnet/Commands/New/DotnetCommandCallbacks.cs
@@ -20,7 +20,7 @@ internal static bool AddPackageReference(string projectPath, string packageName,
{
commandArgs = commandArgs.Append(PackageAddCommandParser.VersionOption.Name).Append(version);
}
- var addPackageReferenceCommand = new AddPackageReferenceCommand(AddCommandParser.GetCommand().Parse(commandArgs.ToArray()));
+ var addPackageReferenceCommand = new AddPackageReferenceCommand(AddCommandParser.GetCommand().Parse(commandArgs.ToArray()), projectPath);
return addPackageReferenceCommand.Execute() == 0;
}
diff --git a/src/Cli/dotnet/Commands/Package/Add/LocalizableStrings.resx b/src/Cli/dotnet/Commands/Package/Add/LocalizableStrings.resx
index 4825ed787675..febefad0d2bc 100644
--- a/src/Cli/dotnet/Commands/Package/Add/LocalizableStrings.resx
+++ b/src/Cli/dotnet/Commands/Package/Add/LocalizableStrings.resx
@@ -124,7 +124,7 @@
Command to add package reference
- The package reference to add.
+ The package reference to add. This can be in the form of just the package identifier, for example 'Newtonsoft.Json', or a package identifier and version separated by '@', for example 'Newtonsoft.Json@13.0.3'Specify one package reference to add.
@@ -165,4 +165,12 @@
Unable to generate a temporary file for project '{0}'. Cannot add package reference. Clear the temp directory and try again.
+
+ Cannot specify --version when the package argument already contains a version.
+ {Locked="--version"}
+
+
+ Failed to parse "{0}" as a semantic version.
+ {0} is a version string that the user entered that was not parsed as a Semantic Version
+
diff --git a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs
index 535ec055daf3..58bd849a32be 100644
--- a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs
+++ b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs
@@ -8,15 +8,50 @@
using NuGet.Versioning;
using Microsoft.DotNet.Tools.Package.Add;
using Microsoft.DotNet.Cli.Extensions;
+using System.CommandLine.Parsing;
+using NuGet.Packaging.Core;
namespace Microsoft.DotNet.Cli;
internal static class PackageAddCommandParser
{
- public static readonly CliArgument CmdPackageArgument = new DynamicArgument(LocalizableStrings.CmdPackage)
+ public static NuGet.Packaging.Core.PackageIdentity ParsePackageIdentity(ArgumentResult packageArgResult)
+ {
+ // per the Arity of the CmdPackageArgument's Arity, we should have exactly one token -
+ // this is a safety net for if we change the Arity in the future and forget to update this parser.
+ if (packageArgResult.Tokens.Count != 1)
+ {
+ throw new ArgumentException($"Expected exactly one token, but got {packageArgResult.Tokens.Count}.");
+ }
+ var token = packageArgResult.Tokens[0].Value;
+ var indexOfAt = token.IndexOf('@');
+ if (indexOfAt == -1)
+ {
+ // no version specified, so we just return the package id
+ return new NuGet.Packaging.Core.PackageIdentity(token, null);
+ }
+ // we have a version specified, so we need to split the token into id and version
+ else
+ {
+ var id = token[0..indexOfAt];
+ var versionString = token[(indexOfAt + 1)..];
+ if (NuGet.Versioning.SemanticVersion.TryParse(versionString, out var version))
+ {
+ return new NuGet.Packaging.Core.PackageIdentity(id, new NuGetVersion(version.Major, version.Minor, version.Patch, version.ReleaseLabels, version.Metadata));
+ }
+ else
+ {
+ throw new ArgumentException(string.Format(LocalizableStrings.InvalidSemVerVersionString, versionString));
+ }
+ };
+ }
+
+ public static readonly CliArgument CmdPackageArgument = new DynamicArgument(LocalizableStrings.CmdPackage)
{
Description = LocalizableStrings.CmdPackageDescription,
- Arity = ArgumentArity.ExactlyOne
+ Arity = ArgumentArity.ExactlyOne,
+ CustomParser = ParsePackageIdentity,
+
}.AddCompletions((context) =>
{
// we should take --prerelease flags into account for version completion
@@ -32,11 +67,11 @@ internal static class PackageAddCommandParser
.AddCompletions((context) =>
{
// we can only do version completion if we have a package id
- if (context.ParseResult.GetValue(CmdPackageArgument) is string packageId)
+ if (context.ParseResult.GetValue(CmdPackageArgument) is NuGet.Packaging.Core.PackageIdentity packageId && !packageId.HasVersion)
{
// we should take --prerelease flags into account for version completion
var allowPrerelease = context.ParseResult.GetValue(PrereleaseOption);
- return QueryVersionsForPackage(packageId, context.WordToComplete, allowPrerelease, CancellationToken.None)
+ return QueryVersionsForPackage(packageId.Id, context.WordToComplete, allowPrerelease, CancellationToken.None)
.Result
.Select(version => new CompletionItem(version.ToNormalizedString()));
}
@@ -89,6 +124,7 @@ private static CliCommand ConstructCommand()
{
CliCommand command = new("add", LocalizableStrings.AppFullName);
+ VersionOption.Validators.Add(DisallowVersionIfPackageIdentityHasVersionValidator);
command.Arguments.Add(CmdPackageArgument);
command.Options.Add(VersionOption);
command.Options.Add(FrameworkOption);
@@ -99,11 +135,19 @@ private static CliCommand ConstructCommand()
command.Options.Add(PrereleaseOption);
command.Options.Add(PackageCommandParser.ProjectOption);
- command.SetAction((parseResult) => new AddPackageReferenceCommand(parseResult).Execute());
+ command.SetAction((parseResult) => new AddPackageReferenceCommand(parseResult, parseResult.GetValue(PackageCommandParser.ProjectOption)).Execute());
return command;
}
+ private static void DisallowVersionIfPackageIdentityHasVersionValidator(OptionResult result)
+ {
+ if (result.Parent.GetValue(CmdPackageArgument) is PackageIdentity identity && identity.HasVersion)
+ {
+ result.AddError(LocalizableStrings.ValidationFailedDuplicateVersion);
+ }
+ }
+
public static async Task> QueryNuGet(string packageStem, bool allowPrerelease, CancellationToken cancellationToken)
{
try
diff --git a/src/Cli/dotnet/Commands/Package/Add/Program.cs b/src/Cli/dotnet/Commands/Package/Add/Program.cs
index 3f1011ce91e0..603f6017cfa7 100644
--- a/src/Cli/dotnet/Commands/Package/Add/Program.cs
+++ b/src/Cli/dotnet/Commands/Package/Add/Program.cs
@@ -7,27 +7,30 @@
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Tools.MSBuild;
using Microsoft.DotNet.Tools.NuGet;
+using NuGet.Packaging.Core;
namespace Microsoft.DotNet.Tools.Package.Add;
-internal class AddPackageReferenceCommand(ParseResult parseResult) : CommandBase(parseResult)
+///
+///
+/// Since this command is invoked via both 'package add' and 'add package', different symbols will control what the project path to search is.
+/// It's cleaner for the separate callsites to know this instead of pushing that logic here.
+///
+internal class AddPackageReferenceCommand(ParseResult parseResult, string fileOrDirectory) : CommandBase(parseResult)
{
- private readonly string _packageId = parseResult.GetValue(PackageAddCommandParser.CmdPackageArgument);
- private readonly string _fileOrDirectory = parseResult.HasOption(PackageCommandParser.ProjectOption) ?
- parseResult.GetValue(PackageCommandParser.ProjectOption) :
- parseResult.GetValue(AddCommandParser.ProjectArgument);
+ private readonly PackageIdentity _packageId = parseResult.GetValue(PackageAddCommandParser.CmdPackageArgument);
public override int Execute()
{
var projectFilePath = string.Empty;
- if (!File.Exists(_fileOrDirectory))
+ if (!File.Exists(fileOrDirectory))
{
- projectFilePath = MsbuildProject.GetProjectFileFromDirectory(_fileOrDirectory).FullName;
+ projectFilePath = MsbuildProject.GetProjectFileFromDirectory(fileOrDirectory).FullName;
}
else
{
- projectFilePath = _fileOrDirectory;
+ projectFilePath = fileOrDirectory;
}
var tempDgFilePath = string.Empty;
@@ -98,17 +101,22 @@ private void DisposeTemporaryFile(string filePath)
}
}
- private string[] TransformArgs(string packageId, string tempDgFilePath, string projectFilePath)
+ private string[] TransformArgs(PackageIdentity packageId, string tempDgFilePath, string projectFilePath)
{
- var args = new List
- {
+ List args = [
"package",
"add",
"--package",
- packageId,
+ packageId.Id,
"--project",
projectFilePath
- };
+ ];
+
+ if (packageId.HasVersion)
+ {
+ args.Add("--version");
+ args.Add(packageId.Version.ToString());
+ }
args.AddRange(_parseResult
.OptionValuesToBeForwarded(PackageAddCommandParser.GetCommand())
diff --git a/src/Cli/dotnet/Commands/Package/Add/xlf/LocalizableStrings.cs.xlf b/src/Cli/dotnet/Commands/Package/Add/xlf/LocalizableStrings.cs.xlf
index 0abf28f2d0d9..17fcb251cc85 100644
--- a/src/Cli/dotnet/Commands/Package/Add/xlf/LocalizableStrings.cs.xlf
+++ b/src/Cli/dotnet/Commands/Package/Add/xlf/LocalizableStrings.cs.xlf
@@ -12,6 +12,11 @@
Příkaz pro přidání odkazu na balíček
+
+ Failed to parse "{0}" as a semantic version.
+ Failed to parse "{0}" as a semantic version.
+ {0} is a version string that the user entered that was not parsed as a Semantic Version
+ Specify one package reference to add.Zadejte jeden odkaz na balíček, který chcete přidat.
@@ -68,8 +73,8 @@
- The package reference to add.
- Odkaz na balíček, který se má přidat
+ The package reference to add. This can be in the form of just the package identifier, for example 'Newtonsoft.Json', or a package identifier and version separated by '@', for example 'Newtonsoft.Json@13.0.3'
+ Odkaz na balíček, který se má přidat
@@ -82,6 +87,11 @@
Nejde generovat dočasný soubor pro projekt {0}. Není možné přidat odkaz na balíček. Vyprázdněte dočasný adresář a zkuste to znovu.
+
+ Cannot specify --version when the package argument already contains a version.
+ Cannot specify --version when the package argument already contains a version.
+ {Locked="--version"}
+