Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions src/Cli/dotnet/Commands/CliCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1501,6 +1501,37 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man
<data name="ProjectConvertAppFullName" xml:space="preserve">
<value>Convert a file-based program to a project-based program.</value>
</data>
<data name="ProjectConvertAskForOutputDirectory" xml:space="preserve">
<value>Specify the output directory ({0}):</value>
<comment>{0} is the default value</comment>
</data>
<data name="ProjectConvertDryRun" xml:space="preserve">
<value>Determines changes without actually modifying the file system</value>
</data>
<data name="ProjectConvertWouldCreateDirectory" xml:space="preserve">
<value>Dry run: would create directory: {0}</value>
<comment>{0} is the directory full path.</comment>
</data>
<data name="ProjectConvertWouldCopyFile" xml:space="preserve">
<value>Dry run: would copy file '{0}' to '{1}'.</value>
<comment>{0} and {1} are file full paths.</comment>
</data>
<data name="ProjectConvertWouldConvertFile" xml:space="preserve">
<value>Dry run: would remove file-level directives from file: {0}</value>
<comment>{0} is the file full path.</comment>
</data>
<data name="ProjectConvertWouldMoveFile" xml:space="preserve">
<value>Dry run: would move file '{0}' to '{1}'.</value>
<comment>{0} and {1} are file full paths.</comment>
</data>
<data name="ProjectConvertWouldDeleteFile" xml:space="preserve">
<value>Dry run: would delete file: {0}</value>
<comment>{0} is the file full path.</comment>
</data>
<data name="ProjectConvertWouldCreateFile" xml:space="preserve">
<value>Dry run: would create file: {0}</value>
<comment>{0} is the file full path.</comment>
</data>
<data name="ProjectManifest" xml:space="preserve">
<value>PROJECT_MANIFEST</value>
</data>
Expand Down
106 changes: 89 additions & 17 deletions src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.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 System.Diagnostics.CodeAnalysis;
using Microsoft.Build.Evaluation;
using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.Cli.Utils;
Expand All @@ -17,17 +18,14 @@ internal sealed class ProjectConvertCommand(ParseResult parseResult) : CommandBa

public override int Execute()
{
// Check the entry point file path.
string file = Path.GetFullPath(_file);
if (!VirtualProjectBuildingCommand.IsValidEntryPointPath(file))
{
throw new GracefulException(CliCommandStrings.InvalidFilePath, file);
}

string targetDirectory = _outputDirectory ?? Path.ChangeExtension(file, null);
if (Directory.Exists(targetDirectory))
{
throw new GracefulException(CliCommandStrings.DirectoryAlreadyExists, targetDirectory);
}
string targetDirectory = DetermineOutputDirectory(file);

// Find directives (this can fail, so do this before creating the target directory).
var sourceFile = VirtualProjectBuildingCommand.LoadSourceFile(file);
Expand All @@ -36,28 +34,37 @@ public override int Execute()
// Find other items to copy over, e.g., default Content items like JSON files in Web apps.
var includeItems = FindIncludedItems().ToList();

Directory.CreateDirectory(targetDirectory);
bool dryRun = _parseResult.GetValue(ProjectConvertCommandParser.DryRunOption);

CreateDirectory(targetDirectory);

var targetFile = Path.Join(targetDirectory, Path.GetFileName(file));

// If there were any directives, remove them from the file.
if (directives.Length != 0)
// Process the entry point file.
if (dryRun)
{
VirtualProjectBuildingCommand.RemoveDirectivesFromFile(directives, sourceFile.Text, targetFile);
File.Delete(file);
Reporter.Output.WriteLine(CliCommandStrings.ProjectConvertWouldCopyFile, file, targetFile);
Reporter.Output.WriteLine(CliCommandStrings.ProjectConvertWouldConvertFile, targetFile);
}
else
{
File.Move(file, targetFile);
VirtualProjectBuildingCommand.RemoveDirectivesFromFile(directives, sourceFile.Text, targetFile);
}

// Create project file.
string projectFile = Path.Join(targetDirectory, Path.GetFileNameWithoutExtension(file) + ".csproj");
using var stream = File.Open(projectFile, FileMode.Create, FileAccess.Write);
using var writer = new StreamWriter(stream, Encoding.UTF8);
VirtualProjectBuildingCommand.WriteProjectFile(writer, directives, isVirtualProject: false);
if (dryRun)
{
Reporter.Output.WriteLine(CliCommandStrings.ProjectConvertWouldCreateFile, projectFile);
}
else
{
using var stream = File.Open(projectFile, FileMode.Create, FileAccess.Write);
using var writer = new StreamWriter(stream, Encoding.UTF8);
VirtualProjectBuildingCommand.WriteProjectFile(writer, directives, isVirtualProject: false);
}

// Copy over included items.
// Copy or move over included items.
foreach (var item in includeItems)
{
string targetItemFullPath = Path.Combine(targetDirectory, item.RelativePath);
Expand All @@ -69,12 +76,39 @@ public override int Execute()
}

string targetItemDirectory = Path.GetDirectoryName(targetItemFullPath)!;
Directory.CreateDirectory(targetItemDirectory);
File.Copy(item.FullPath, targetItemFullPath);
CreateDirectory(targetItemDirectory);
CopyFile(item.FullPath, targetItemFullPath);
}

return 0;

void CreateDirectory(string path)
{
if (dryRun)
{
if (!Directory.Exists(path))
{
Reporter.Output.WriteLine(CliCommandStrings.ProjectConvertWouldCreateDirectory, path);
}
}
else
{
Directory.CreateDirectory(path);
}
}

void CopyFile(string source, string target)
{
if (dryRun)
{
Reporter.Output.WriteLine(CliCommandStrings.ProjectConvertWouldCopyFile, source, target);
}
else
{
File.Copy(source, target);
}
}

IEnumerable<(string FullPath, string RelativePath)> FindIncludedItems()
{
string entryPointFileDirectory = PathUtility.EnsureTrailingSlash(Path.GetDirectoryName(file)!);
Expand Down Expand Up @@ -118,4 +152,42 @@ public override int Execute()
}
}
}

private string DetermineOutputDirectory(string file)
{
string defaultValue = Path.ChangeExtension(file, null);
string defaultValueRelative = Path.GetRelativePath(relativeTo: Environment.CurrentDirectory, defaultValue);
string targetDirectory = _outputDirectory
?? TryAskForOutputDirectory(defaultValueRelative)
?? defaultValue;
if (Directory.Exists(targetDirectory))
{
throw new GracefulException(CliCommandStrings.DirectoryAlreadyExists, targetDirectory);
}

return targetDirectory;
}

private string? TryAskForOutputDirectory(string defaultValueRelative)
{
return InteractiveConsole.Ask<string?>(
string.Format(CliCommandStrings.ProjectConvertAskForOutputDirectory, defaultValueRelative),
_parseResult,
(path, out result, [NotNullWhen(returnValue: false)] out error) =>
{
if (Directory.Exists(path))
{
result = null;
error = string.Format(CliCommandStrings.DirectoryAlreadyExists, Path.GetFullPath(path));
return false;
}

result = path is null ? null : Path.GetFullPath(path);
error = null;
return true;
},
out var result)
? result
: null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,21 @@ internal sealed class ProjectConvertCommandParser
Arity = ArgumentArity.Zero,
};

public static readonly Option<bool> DryRunOption = new("--dry-run")
{
Description = CliCommandStrings.ProjectConvertDryRun,
Arity = ArgumentArity.Zero,
};

public static Command GetCommand()
{
Command command = new("convert", CliCommandStrings.ProjectConvertAppFullName)
{
FileArgument,
SharedOptions.OutputOption,
ForceOption,
CommonOptions.InteractiveOption(),
DryRunOption,
};

command.SetAction((parseResult) => new ProjectConvertCommand(parseResult).Execute());
Expand Down
43 changes: 1 addition & 42 deletions src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ internal class ToolExecuteCommand(ParseResult result, ToolManifestFinder? toolMa
private readonly string[] _addSource = result.GetValue(ToolExecuteCommandParser.AddSourceOption) ?? [];
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 IToolPackageDownloader _toolPackageDownloader = ToolPackageFactory.CreateToolPackageStoresAndDownloader().downloader;

private readonly RestoreActionConfig _restoreActionConfig = new RestoreActionConfig(DisableParallel: result.GetValue(ToolCommandRestorePassThroughOptions.DisableParallelOption),
Expand Down Expand Up @@ -128,47 +127,7 @@ public override int Execute()

private bool UserAgreedToRunFromSource(PackageId packageId, NuGetVersion version, PackageSource source)
{
if (_yes)
{
return true;
}

if (!_interactive)
{
return false;
}

string promptMessage = string.Format(CliCommandStrings.ToolDownloadConfirmationPrompt, packageId, version.ToString(), source.Source);

static string AddPromptOptions(string message)
{
return $"{message} [{CliCommandStrings.ConfirmationPromptYesValue}/{CliCommandStrings.ConfirmationPromptNoValue}] ({CliCommandStrings.ConfirmationPromptYesValue}): ";
}

Console.Write(AddPromptOptions(promptMessage));

static bool KeyMatches(ConsoleKeyInfo pressedKey, string valueKey)
{
// Apparently you can't do invariant case insensitive comparison on a char directly, so we have to convert it to a string.
// The resource string should be a single character, but we take the first character just to be sure.
return pressedKey.KeyChar.ToString().ToLowerInvariant().Equals(
valueKey.ToLowerInvariant().Substring(0, 1));
}

while (true)
{
var key = Console.ReadKey();
Console.WriteLine();
if (key.Key == ConsoleKey.Enter || KeyMatches(key, CliCommandStrings.ConfirmationPromptYesValue))
{
return true;
}
if (key.Key == ConsoleKey.Escape || KeyMatches(key, CliCommandStrings.ConfirmationPromptNoValue))
{
return false;
}

Console.Write(AddPromptOptions(string.Format(CliCommandStrings.ConfirmationPromptInvalidChoiceMessage, CliCommandStrings.ConfirmationPromptYesValue, CliCommandStrings.ConfirmationPromptNoValue)));
}
return InteractiveConsole.Confirm(promptMessage, _parseResult, acceptEscapeForFalse: true) == true;
}
}
40 changes: 40 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.

40 changes: 40 additions & 0 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf

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

Loading
Loading