Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
26 changes: 26 additions & 0 deletions src/Cli/dotnet/Commands/CliCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1501,6 +1501,32 @@ 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="CmdOptionKeepSourceDescription" xml:space="preserve">
<value>Whether to keep source files intact (copy) or delete them after conversion (move).</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="ProjectConvertAskForSourceFilesAction" xml:space="preserve">
<value>Source files action [{0}/{1}] ({0}):</value>
<comment>{0} is the copy action (the default choice). {1} is the move action.</comment>
</data>
<data name="ProjectConvertCopyAction" xml:space="preserve">
<value>copy</value>
<comment>One of the choices for the source files action. The user needs to type this to the console to select the option.</comment>
</data>
<data name="ProjectConvertMoveAction" xml:space="preserve">
<value>move</value>
<comment>One of the choices for the source files action. The user needs to type this to the console to select the option.</comment>
</data>
<data name="ProjectConvertInvalidSourceAction" xml:space="preserve">
<value>Please type '{0}' or '{1}'.</value>
</data>
<data name="ProjectConvertNeedsConfirmation" xml:space="preserve">
<value>Conversion command needs to confirm whether to keep source files. Run in interactive mode or use the "--source" command-line option to confirm.</value>
<comment>{Locked="--source"}</comment>
</data>
<data name="ProjectManifest" xml:space="preserve">
<value>PROJECT_MANIFEST</value>
</data>
Expand Down
109 changes: 100 additions & 9 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,15 @@ 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);
bool keepSourceFiles = ShouldKeepSourceFiles();

// Find directives (this can fail, so do this before creating the target directory).
var sourceFile = VirtualProjectBuildingCommand.LoadSourceFile(file);
Expand All @@ -40,8 +39,12 @@ public override int Execute()

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 (keepSourceFiles)
{
File.Copy(file, targetFile);
}
else if (directives.Length != 0)
{
VirtualProjectBuildingCommand.RemoveDirectivesFromFile(directives, sourceFile.Text, targetFile);
File.Delete(file);
Expand All @@ -57,7 +60,7 @@ public override int Execute()
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 @@ -70,7 +73,14 @@ public override int Execute()

string targetItemDirectory = Path.GetDirectoryName(targetItemFullPath)!;
Directory.CreateDirectory(targetItemDirectory);
File.Copy(item.FullPath, targetItemFullPath);
if (keepSourceFiles)
{
File.Copy(item.FullPath, targetItemFullPath);
}
else
{
File.Move(item.FullPath, targetItemFullPath);
}
}

return 0;
Expand Down Expand Up @@ -118,4 +128,85 @@ 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;
}

private bool ShouldKeepSourceFiles()
{
if (_parseResult.GetValue(ProjectConvertCommandParser.SourceOption) is { } source)
{
if (source is ProjectConvertCommandParser.SourceAction.copy)
{
return true;
}

if (source is ProjectConvertCommandParser.SourceAction.move)
{
return false;
}
}

return InteractiveConsole.Ask<bool>(
string.Format(CliCommandStrings.ProjectConvertAskForSourceFilesAction, CliCommandStrings.ProjectConvertCopyAction, CliCommandStrings.ProjectConvertMoveAction),
_parseResult,
(answer, out result, [NotNullWhen(returnValue: false)] out error) =>
{
if (answer is null || string.Equals(answer, CliCommandStrings.ProjectConvertCopyAction, StringComparison.OrdinalIgnoreCase))
{
result = true;
error = null;
return true;
}

if (string.Equals(answer, CliCommandStrings.ProjectConvertMoveAction, StringComparison.OrdinalIgnoreCase))
{
result = false;
error = null;
return true;
}

result = default;
error = string.Format(CliCommandStrings.ProjectConvertInvalidSourceAction, CliCommandStrings.ProjectConvertCopyAction, CliCommandStrings.ProjectConvertMoveAction);
return false;
},
out var result)
? result
: throw new GracefulException(CliCommandStrings.ProjectConvertNeedsConfirmation);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,27 @@ internal sealed class ProjectConvertCommandParser
Arity = ArgumentArity.Zero,
};

public static readonly Option<SourceAction?> SourceOption = new("--source")
{
Description = CliCommandStrings.CmdOptionKeepSourceDescription,
Arity = ArgumentArity.ExactlyOne,
};

public enum SourceAction
{
copy,
move,
}

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

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;
}
}
35 changes: 35 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.

35 changes: 35 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