Skip to content

allow @ syntax for package versions in package add #47961

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 31, 2025
Merged
15 changes: 14 additions & 1 deletion src/Cli/dotnet/Commands/Add/Package/AddPackageParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Cli/dotnet/Commands/New/DotnetCommandCallbacks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
10 changes: 9 additions & 1 deletion src/Cli/dotnet/Commands/Package/Add/LocalizableStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@
<value>Command to add package reference</value>
</data>
<data name="CmdPackageDescription" xml:space="preserve">
<value>The package reference to add.</value>
<value>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 '[email protected]'</value>
</data>
<data name="SpecifyExactlyOnePackageReference" xml:space="preserve">
<value>Specify one package reference to add.</value>
Expand Down Expand Up @@ -165,4 +165,12 @@
<data name="CmdDGFileIOException" xml:space="preserve">
<value>Unable to generate a temporary file for project '{0}'. Cannot add package reference. Clear the temp directory and try again.</value>
</data>
<data name="ValidationFailedDuplicateVersion" xml:space="preserve">
<value>Cannot specify --version when the package argument already contains a version.</value>
<comment>{Locked="--version"}</comment>
</data>
<data name="InvalidSemVerVersionString" xml:space="preserve">
<value>Failed to parse "{0}" as a semantic version.</value>
<comment>{0} is a version string that the user entered that was not parsed as a Semantic Version</comment>
</data>
</root>
54 changes: 49 additions & 5 deletions src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> CmdPackageArgument = new DynamicArgument<string>(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<NuGet.Packaging.Core.PackageIdentity> CmdPackageArgument = new DynamicArgument<NuGet.Packaging.Core.PackageIdentity>(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
Expand All @@ -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()));
}
Expand Down Expand Up @@ -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);
Expand All @@ -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<IEnumerable<string>> QueryNuGet(string packageStem, bool allowPrerelease, CancellationToken cancellationToken)
{
try
Expand Down
34 changes: 21 additions & 13 deletions src/Cli/dotnet/Commands/Package/Add/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
/// <param name="parseResult"></param>
/// <param name="fileOrDirectory">
/// 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.
/// </param>
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;
Expand Down Expand Up @@ -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<string>
{
List<string> 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())
Expand Down
14 changes: 12 additions & 2 deletions src/Cli/dotnet/Commands/Package/Add/xlf/LocalizableStrings.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions src/Cli/dotnet/Commands/Package/Add/xlf/LocalizableStrings.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions src/Cli/dotnet/Commands/Package/Add/xlf/LocalizableStrings.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions src/Cli/dotnet/Commands/Package/Add/xlf/LocalizableStrings.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading