Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
bb4b2bc
Add command
edvilme Apr 11, 2025
dd02918
Add logic for acquisition and running
edvilme Apr 14, 2025
62cc62c
Execute tool
edvilme Apr 14, 2025
87d8208
Load package if installed
edvilme Apr 14, 2025
b11e3b8
tool-run: --from-source
edvilme Apr 15, 2025
2902f08
Save tool to temp directory
edvilme Apr 15, 2025
33443f1
Update translations
edvilme Apr 15, 2025
e603952
Update cli snapshots
edvilme Apr 15, 2025
d9ab4d8
Merge branch 'main' into edvilme-toolx
edvilme Apr 15, 2025
786d58b
Address pr comments
edvilme Apr 16, 2025
51e3412
Merge branch 'edvilme-toolx' of https://github.com/edvilme/sdk into e…
edvilme Apr 16, 2025
a51efbe
Fix typo
edvilme Apr 16, 2025
08b1431
Fix typo
edvilme Apr 16, 2025
66395d5
Move logic to ToolRunFromSourceCommand.cs
edvilme Apr 17, 2025
7e6b546
Merge branch 'main' into edvilme-toolx
edvilme Apr 17, 2025
dfb18b0
Add options
edvilme Apr 17, 2025
875889a
Merge branch 'edvilme-toolx' of https://github.com/edvilme/sdk into e…
edvilme Apr 17, 2025
d8a059a
Fix typo
edvilme Apr 17, 2025
4f30d19
Fix silly errors
edvilme Apr 17, 2025
a8dfe19
Run tool
edvilme Apr 17, 2025
630744c
Simplify code and translations
edvilme Apr 18, 2025
ee41f19
Update completions
edvilme Apr 18, 2025
f85411f
Add --interactive option
edvilme Apr 18, 2025
f14af9a
Update Completion snapshots
edvilme Apr 21, 2025
8d95185
tool-exec
edvilme Apr 21, 2025
90b197c
Update Completions
edvilme Apr 21, 2025
761ea9f
Merge branch 'main' into edvilme-toolx
edvilme Apr 22, 2025
6d2066a
Update cli snapshots
edvilme Apr 22, 2025
2573bad
Add version option
edvilme Apr 22, 2025
5de73ef
Pretty divider line
edvilme Apr 22, 2025
f4c558c
Address pr feedback
edvilme Apr 24, 2025
093f2e6
Merge branch 'main' into edvilme-toolx
edvilme Apr 28, 2025
45339d7
Remove prompt for normal and quiter verbosities
edvilme Apr 28, 2025
9d07213
Merge branch 'edvilme-toolx' of https://github.com/edvilme/sdk into e…
edvilme Apr 28, 2025
8936d4e
Store in nuget cache?
edvilme Apr 29, 2025
fcd119a
Merge branch 'main' into edvilme-toolx
edvilme Apr 30, 2025
76511ee
Merge branch 'main' into edvilme-toolx
edvilme May 13, 2025
52d9728
Remove null assignment
edvilme May 13, 2025
cbbf3da
Merge branch 'edvilme-toolx' of https://github.com/edvilme/sdk into e…
edvilme May 13, 2025
312f46f
Remove null assignment
edvilme May 13, 2025
0cb89cc
Merge branch 'main' into edvilme-toolx
marcpopMSFT Jun 6, 2025
62d20ad
Update based on PR feedback.
marcpopMSFT Jun 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Cli/dotnet/CliStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -803,4 +803,10 @@ For a list of locations searched, specify the "-d" option before the tool name.<
<value>Cannot specify --version when the package argument already contains a version.</value>
<comment>{Locked="--version"}</comment>
</data>
<data name="YesOptionDescription" xml:space="preserve">
<value>Overrides confirmation prompt with "yes" value. </value>
</data>
<data name="NoOptionDescription" xml:space="preserve">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still seems unused.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in follow-up PR: #49329

<value>Overrides confirmation prompt with "no" value.</value>
</data>
</root>
15 changes: 15 additions & 0 deletions src/Cli/dotnet/Commands/CliCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -2062,6 +2062,9 @@ If you would like to create a manifest, use the `--create-manifest-if-needed` fl
<data name="ToolRunCommandDescription" xml:space="preserve">
<value>Run a local tool. Note that this command cannot be used to run a global tool. </value>
</data>
<data name="ToolRunArguementsDescription" xml:space="preserve">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tiny nit:

Suggested change
<data name="ToolRunArguementsDescription" xml:space="preserve">
<data name="ToolRunArgumentsDescription" xml:space="preserve">

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in follow-up PR: #49329

<value>Arguments forwarded to the tool</value>
</data>
<data name="ToolSearchCommandDescription" xml:space="preserve">
<value>Search dotnet tools in nuget.org</value>
</data>
Expand Down Expand Up @@ -2483,6 +2486,18 @@ To display a value, specify the corresponding command-line option without provid
<data name="ZeroTestsRan" xml:space="preserve">
<value>Zero tests ran</value>
</data>
<data name="ToolExecuteCommandDescription" xml:space="preserve">
<value>Executes a tool from source without permanently installing it.</value>
</data>
<data name="ToolExecuteCommandMissingPackageId" xml:space="preserve">
<value>Missing package ID</value>
</data>
<data name="ToolRunFromSourceUserConfirmationPrompt" xml:space="preserve">
<value>Tool not found on the system. Do you want to run it from source? [y/n]</value>
</data>
<data name="ToolRunFromSourceUserConfirmationFailed" xml:space="preserve">
<value>Run from source approval denied by the user</value>
</data>
<data name="SolutionAddReferencedProjectsOptionDescription" xml:space="preserve">
<value>Recursively add projects' ReferencedProjects to solution</value>
</data>
Expand Down
106 changes: 106 additions & 0 deletions src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;
using Microsoft.DotNet.Cli.CommandFactory;
using Microsoft.DotNet.Cli.CommandFactory.CommandResolution;
using Microsoft.DotNet.Cli.Commands.Tool.Install;
using Microsoft.DotNet.Cli.ToolPackage;
using Microsoft.DotNet.Cli.Utils;
using NuGet.Common;
using NuGet.Packaging.Core;
using NuGet.Versioning;

namespace Microsoft.DotNet.Cli.Commands.Tool.Execute
{
internal class ToolExecuteCommand(ParseResult result) : CommandBase(result)
{
private readonly PackageIdentity? _packageToolIdentityArgument = result.GetValue(ToolExecuteCommandParser.PackageIdentityArgument);
private readonly IEnumerable<string> _forwardArguments = result.GetValue(ToolExecuteCommandParser.CommandArgument) ?? [];
private readonly bool _allowRollForward = result.GetValue(ToolExecuteCommandParser.RollForwardOption);
private readonly string? _configFile = result.GetValue(ToolExecuteCommandParser.ConfigOption);
private readonly string[] _sources = result.GetValue(ToolExecuteCommandParser.SourceOption) ?? [];
private readonly string[] _addSource = result.GetValue(ToolExecuteCommandParser.AddSourceOption) ?? [];
private readonly bool _ignoreFailedSources = result.GetValue(ToolCommandRestorePassThroughOptions.IgnoreFailedSourcesOption);
private readonly bool _interactive = result.GetValue(ToolExecuteCommandParser.InteractiveOption);
private readonly VerbosityOptions _verbosity = result.GetValue(ToolExecuteCommandParser.VerbosityOption);
private readonly bool _yes = result.GetValue(ToolExecuteCommandParser.YesOption);
private readonly bool _prerelease = result.GetValue(ToolExecuteCommandParser.PrereleaseOption);

public override int Execute()
{
if (_packageToolIdentityArgument is null)
{
// System.CommandLine will throw an error if the argument is not provided, but we can still check here for clarity.
return 1;
}

if (!UserAgreedToRunFromSource())
{
throw new GracefulException(CliCommandStrings.ToolRunFromSourceUserConfirmationFailed, isUserError: true);
}

if (_allowRollForward)
{
_forwardArguments.Append("--allow-roll-forward");
}

PackageId packageId = new PackageId(_packageToolIdentityArgument.Id);

VersionRange versionRange = _parseResult.GetVersionRange();

string tempDirectory = NuGetEnvironment.GetFolderPath(NuGetFolderPath.Temp);

ToolPackageStoreAndQuery toolPackageStoreAndQuery = new(new(tempDirectory));
ToolPackageDownloader toolPackageDownloader = new(toolPackageStoreAndQuery);

IToolPackage toolPackage = toolPackageDownloader.InstallPackage(
new PackageLocation(
nugetConfig: _configFile != null ? new(_configFile) : null,
sourceFeedOverrides: _sources,
additionalFeeds: _addSource),
packageId: packageId,
verbosity: _verbosity,
versionRange: versionRange,
isGlobalToolRollForward: _allowRollForward, // Needed to update .runtimeconfig.json
restoreActionConfig: new(
IgnoreFailedSources: _ignoreFailedSources,
Interactive: _interactive));

CommandSpec commandSpec = MuxerCommandSpecMaker.CreatePackageCommandSpecUsingMuxer(toolPackage.Command.Executable.ToString(), _forwardArguments);
var command = CommandFactoryUsingResolver.Create(commandSpec);
var result = command.Execute();
return result.ExitCode;
}

private bool UserAgreedToRunFromSource()
{
if (_yes)
{
return true;
}

if (!_interactive)
{
return false;
}

// TODO: Use a better way to ask for user input
Console.Write(CliCommandStrings.ToolRunFromSourceUserConfirmationPrompt);
bool userAccepted = Console.ReadKey().Key == ConsoleKey.Y;

if (_verbosity >= VerbosityOptions.detailed)
{
Console.WriteLine();
Console.WriteLine(new String('-', CliCommandStrings.ToolRunFromSourceUserConfirmationPrompt.Length));
}
else
{
// Clear the line
Console.Write("\r" + new string(' ', CliCommandStrings.ToolRunFromSourceUserConfirmationPrompt.Length + 1) + "\r");
}

return userAccepted;
}
}
}
66 changes: 66 additions & 0 deletions src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommandParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.CommandLine;
using System.Text;
using Microsoft.DotNet.Cli.Commands.Tool.Install;
using NuGet.Packaging.Core;

namespace Microsoft.DotNet.Cli.Commands.Tool.Execute
{
internal static class ToolExecuteCommandParser

{
public static readonly Argument<PackageIdentity?> PackageIdentityArgument = ToolInstallCommandParser.PackageIdentityArgument;

public static readonly Argument<IEnumerable<string>> CommandArgument = new("commandArguments")
{
Description = CliCommandStrings.ToolRunArguementsDescription
};

public static readonly Option<string> VersionOption = ToolInstallCommandParser.VersionOption;
public static readonly Option<bool> RollForwardOption = ToolInstallCommandParser.RollForwardOption;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have to define these here as opposed to just adding ToolInstallCommandParser.RollForwardOption in ToolExecuteCommandParser.ConstructCommand and using that in the Command?

public static readonly Option<bool> PrereleaseOption = ToolInstallCommandParser.PrereleaseOption;
public static readonly Option<string> ConfigOption = ToolInstallCommandParser.ConfigOption;
public static readonly Option<string[]> SourceOption = ToolInstallCommandParser.SourceOption;
public static readonly Option<string[]> AddSourceOption = ToolInstallCommandParser.AddSourceOption;
public static readonly Option<bool> IgnoreFailedSourcesOption = ToolCommandRestorePassThroughOptions.IgnoreFailedSourcesOption;
public static readonly Option<bool> InteractiveOption = CommonOptions.InteractiveOption();
public static readonly Option<bool> YesOption = CommonOptions.YesOption;
public static readonly Option<VerbosityOptions> VerbosityOption = ToolInstallCommandParser.VerbosityOption;


public static readonly Command Command = ConstructCommand();
public static Command GetCommand()
{
return Command;
}

private static Command ConstructCommand()
{
Command command = new("execute", CliCommandStrings.ToolExecuteCommandDescription);

command.Aliases.Add("exec");

command.Arguments.Add(PackageIdentityArgument);
command.Arguments.Add(CommandArgument);

command.Options.Add(VersionOption);
command.Options.Add(RollForwardOption);
command.Options.Add(PrereleaseOption);
command.Options.Add(ConfigOption);
command.Options.Add(SourceOption);
command.Options.Add(AddSourceOption);
command.Options.Add(IgnoreFailedSourcesOption);
command.Options.Add(InteractiveOption);
command.Options.Add(YesOption);
command.Options.Add(VerbosityOption);

command.SetAction((parseResult) => new ToolExecuteCommand(parseResult).Execute());

return command;
}
}
}
13 changes: 11 additions & 2 deletions src/Cli/dotnet/Commands/Tool/Install/ParseResultExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,17 @@ internal static class ParseResultExtension
{
public static VersionRange GetVersionRange(this ParseResult parseResult)
{
string packageVersion = parseResult.GetValue(ToolInstallCommandParser.PackageIdentityArgument)?.Version?.ToString() ??
parseResult.GetValue(ToolInstallCommandParser.VersionOption);
var packageVersionFromIdentityArgument = parseResult.GetValue(ToolInstallCommandParser.PackageIdentityArgument)?.Version?.ToString();
var packageVersionFromVersionOption = parseResult.GetValue(ToolInstallCommandParser.VersionOption);

// Check that only one of these has a value
if (!string.IsNullOrEmpty(packageVersionFromIdentityArgument) && !string.IsNullOrEmpty(packageVersionFromVersionOption))
{
throw new GracefulException(CliStrings.PackageIdentityArgumentVersionOptionConflict);
}

string packageVersion = packageVersionFromIdentityArgument ?? packageVersionFromVersionOption;

bool prerelease = parseResult.GetValue(ToolInstallCommandParser.PrereleaseOption);

if (!string.IsNullOrEmpty(packageVersion) && prerelease)
Expand Down
11 changes: 0 additions & 11 deletions src/Cli/dotnet/Commands/Tool/Install/ToolInstallCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,6 @@ internal class ToolInstallCommand(
private readonly string _framework = parseResult.GetValue(ToolInstallCommandParser.FrameworkOption);


internal static void EnsureNoConflictPackageIdentityVersionOption(ParseResult parseResult)
{
if (!string.IsNullOrEmpty(parseResult.GetValue(ToolInstallCommandParser.PackageIdentityArgument)?.Version?.ToString()) &&
!string.IsNullOrEmpty(parseResult.GetValue(ToolInstallCommandParser.VersionOption)))
{
throw new GracefulException(CliStrings.PackageIdentityArgumentVersionOptionConflict);
}
}

public override int Execute()
{
ToolAppliedOption.EnsureNoConflictGlobalLocalToolPathOption(
Expand All @@ -39,8 +30,6 @@ public override int Execute()
ToolAppliedOption.EnsureToolManifestAndOnlyLocalFlagCombination(
_parseResult);

EnsureNoConflictPackageIdentityVersionOption(_parseResult);

if (_global || !string.IsNullOrWhiteSpace(_toolPath))
{
return (_toolInstallGlobalOrToolPathCommand ?? new ToolInstallGlobalOrToolPathCommand(_parseResult)).Execute();
Expand Down
13 changes: 8 additions & 5 deletions src/Cli/dotnet/Commands/Tool/Run/ToolRunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
using System.CommandLine;
using Microsoft.DotNet.Cli.CommandFactory;
using Microsoft.DotNet.Cli.CommandFactory.CommandResolution;
using Microsoft.DotNet.Cli.Commands.Tool.Install;
using Microsoft.DotNet.Cli.NuGetPackageDownloader;
using Microsoft.DotNet.Cli.ToolManifest;
using Microsoft.DotNet.Cli.ToolPackage;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.EnvironmentAbstractions;

Expand All @@ -18,27 +21,27 @@ internal class ToolRunCommand(
ToolManifestFinder toolManifest = null) : CommandBase(result)
{
private readonly string _toolCommandName = result.GetValue(ToolRunCommandParser.CommandNameArgument);
private readonly LocalToolsCommandResolver _localToolsCommandResolver = localToolsCommandResolver ?? new LocalToolsCommandResolver();
private readonly IEnumerable<string> _forwardArgument = result.GetValue(ToolRunCommandParser.CommandArgument);

private readonly LocalToolsCommandResolver _localToolsCommandResolver = localToolsCommandResolver ?? new LocalToolsCommandResolver(toolManifest);
public bool _allowRollForward = result.GetValue(ToolRunCommandParser.RollForwardOption);
private readonly ToolManifestFinder _toolManifest = toolManifest ?? new ToolManifestFinder(new DirectoryPath(Directory.GetCurrentDirectory()));

public override int Execute()
{
CommandSpec commandspec = _localToolsCommandResolver.ResolveStrict(new CommandResolverArguments()
CommandSpec commandSpec = _localToolsCommandResolver.ResolveStrict(new CommandResolverArguments()
{
// since LocalToolsCommandResolver is a resolver, and all resolver input have dotnet-
CommandName = $"dotnet-{_toolCommandName}",
CommandArguments = _forwardArgument,

}, _allowRollForward);

if (commandspec == null)
if (commandSpec == null)
{
throw new GracefulException([string.Format(CliCommandStrings.CannotFindCommandName, _toolCommandName)], isUserError: false);
}

var result = CommandFactoryUsingResolver.Create(commandspec).Execute();
var result = CommandFactoryUsingResolver.Create(commandSpec).Execute();
return result.ExitCode;
}
}
2 changes: 1 addition & 1 deletion src/Cli/dotnet/Commands/Tool/Run/ToolRunCommandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal static class ToolRunCommandParser

public static readonly Argument<IEnumerable<string>> CommandArgument = new("toolArguments")
{
Description = "arguments forwarded to the tool"
Description = CliCommandStrings.ToolRunArguementsDescription
};

public static readonly Option<bool> RollForwardOption = new("--allow-roll-forward")
Expand Down
2 changes: 2 additions & 0 deletions src/Cli/dotnet/Commands/Tool/ToolCommandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#nullable disable

using System.CommandLine;
using Microsoft.DotNet.Cli.Commands.Tool.Execute;
using Microsoft.DotNet.Cli.Commands.Tool.Install;
using Microsoft.DotNet.Cli.Commands.Tool.List;
using Microsoft.DotNet.Cli.Commands.Tool.Restore;
Expand Down Expand Up @@ -37,6 +38,7 @@ private static Command ConstructCommand()
command.Subcommands.Add(ToolRunCommandParser.GetCommand());
command.Subcommands.Add(ToolSearchCommandParser.GetCommand());
command.Subcommands.Add(ToolRestoreCommandParser.GetCommand());
command.Subcommands.Add(ToolExecuteCommandParser.GetCommand());

command.SetAction((parseResult) => parseResult.HandleMissingCommand());

Expand Down
25 changes: 25 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf

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

Loading
Loading