diff --git a/src/Cli/dotnet/Commands/CliCommandStrings.resx b/src/Cli/dotnet/Commands/CliCommandStrings.resx index 75c76f009030..f967ccdc2e1a 100644 --- a/src/Cli/dotnet/Commands/CliCommandStrings.resx +++ b/src/Cli/dotnet/Commands/CliCommandStrings.resx @@ -1501,6 +1501,37 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man Convert a file-based program to a project-based program. + + Specify the output directory ({0}): + {0} is the default value + + + Determines changes without actually modifying the file system + + + Dry run: would create directory: {0} + {0} is the directory full path. + + + Dry run: would copy file '{0}' to '{1}'. + {0} and {1} are file full paths. + + + Dry run: would remove file-level directives from file: {0} + {0} is the file full path. + + + Dry run: would move file '{0}' to '{1}'. + {0} and {1} are file full paths. + + + Dry run: would delete file: {0} + {0} is the file full path. + + + Dry run: would create file: {0} + {0} is the file full path. + PROJECT_MANIFEST diff --git a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs index d4ae5207be00..7f8f6a0ee747 100644 --- a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs +++ b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs @@ -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; @@ -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); @@ -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); @@ -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)!); @@ -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.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; + } } diff --git a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommandParser.cs b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommandParser.cs index a933c8068652..c3104e995cd3 100644 --- a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommandParser.cs +++ b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommandParser.cs @@ -20,6 +20,12 @@ internal sealed class ProjectConvertCommandParser Arity = ArgumentArity.Zero, }; + public static readonly Option DryRunOption = new("--dry-run") + { + Description = CliCommandStrings.ProjectConvertDryRun, + Arity = ArgumentArity.Zero, + }; + public static Command GetCommand() { Command command = new("convert", CliCommandStrings.ProjectConvertAppFullName) @@ -27,6 +33,8 @@ public static Command GetCommand() FileArgument, SharedOptions.OutputOption, ForceOption, + CommonOptions.InteractiveOption(), + DryRunOption, }; command.SetAction((parseResult) => new ProjectConvertCommand(parseResult).Execute()); diff --git a/src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs b/src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs index cb74f08d05ff..bdfdacb42d58 100644 --- a/src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Execute/ToolExecuteCommand.cs @@ -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), @@ -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; } } diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf index fcab6e61ce88..651d84d4c430 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf @@ -2290,6 +2290,46 @@ Nástroj {1} (verze {2}) se úspěšně nainstaloval. Do souboru manifestu {3} s Umožňuje převést program na bázi souboru na program na bázi projektu. + + Specify the output directory ({0}): + Specify the output directory ({0}): + {0} is the default value + + + Determines changes without actually modifying the file system + Determines changes without actually modifying the file system + + + + Dry run: would remove file-level directives from file: {0} + Dry run: would remove file-level directives from file: {0} + {0} is the file full path. + + + Dry run: would copy file '{0}' to '{1}'. + Dry run: would copy file '{0}' to '{1}'. + {0} and {1} are file full paths. + + + Dry run: would create directory: {0} + Dry run: would create directory: {0} + {0} is the directory full path. + + + Dry run: would create file: {0} + Dry run: would create file: {0} + {0} is the file full path. + + + Dry run: would delete file: {0} + Dry run: would delete file: {0} + {0} is the file full path. + + + Dry run: would move file '{0}' to '{1}'. + Dry run: would move file '{0}' to '{1}'. + {0} and {1} are file full paths. + PROJECT_MANIFEST PROJECT_MANIFEST diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf index f70cb9600a52..1f6d15287918 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf @@ -2290,6 +2290,46 @@ Das Tool "{1}" (Version {2}) wurde erfolgreich installiert. Der Eintrag wird der Konvertieren Sie ein dateibasiertes Programm in ein projektbasiertes Programm. + + Specify the output directory ({0}): + Specify the output directory ({0}): + {0} is the default value + + + Determines changes without actually modifying the file system + Determines changes without actually modifying the file system + + + + Dry run: would remove file-level directives from file: {0} + Dry run: would remove file-level directives from file: {0} + {0} is the file full path. + + + Dry run: would copy file '{0}' to '{1}'. + Dry run: would copy file '{0}' to '{1}'. + {0} and {1} are file full paths. + + + Dry run: would create directory: {0} + Dry run: would create directory: {0} + {0} is the directory full path. + + + Dry run: would create file: {0} + Dry run: would create file: {0} + {0} is the file full path. + + + Dry run: would delete file: {0} + Dry run: would delete file: {0} + {0} is the file full path. + + + Dry run: would move file '{0}' to '{1}'. + Dry run: would move file '{0}' to '{1}'. + {0} and {1} are file full paths. + PROJECT_MANIFEST PROJECT_MANIFEST diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf index e349ccd9aae5..5091be25b583 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.es.xlf @@ -2290,6 +2290,46 @@ La herramienta "{1}" (versión "{2}") se instaló correctamente. Se ha agregado Convertir un programa basado en archivos en un programa basado en proyecto. + + Specify the output directory ({0}): + Specify the output directory ({0}): + {0} is the default value + + + Determines changes without actually modifying the file system + Determines changes without actually modifying the file system + + + + Dry run: would remove file-level directives from file: {0} + Dry run: would remove file-level directives from file: {0} + {0} is the file full path. + + + Dry run: would copy file '{0}' to '{1}'. + Dry run: would copy file '{0}' to '{1}'. + {0} and {1} are file full paths. + + + Dry run: would create directory: {0} + Dry run: would create directory: {0} + {0} is the directory full path. + + + Dry run: would create file: {0} + Dry run: would create file: {0} + {0} is the file full path. + + + Dry run: would delete file: {0} + Dry run: would delete file: {0} + {0} is the file full path. + + + Dry run: would move file '{0}' to '{1}'. + Dry run: would move file '{0}' to '{1}'. + {0} and {1} are file full paths. + PROJECT_MANIFEST PROJECT_MANIFEST diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf index 9243b52795f5..457afcb8a738 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.fr.xlf @@ -2290,6 +2290,46 @@ L'outil '{1}' (version '{2}') a été correctement installé. L'entrée est ajou Convertissez un programme basé sur des fichiers en programme basé sur un projet. + + Specify the output directory ({0}): + Specify the output directory ({0}): + {0} is the default value + + + Determines changes without actually modifying the file system + Determines changes without actually modifying the file system + + + + Dry run: would remove file-level directives from file: {0} + Dry run: would remove file-level directives from file: {0} + {0} is the file full path. + + + Dry run: would copy file '{0}' to '{1}'. + Dry run: would copy file '{0}' to '{1}'. + {0} and {1} are file full paths. + + + Dry run: would create directory: {0} + Dry run: would create directory: {0} + {0} is the directory full path. + + + Dry run: would create file: {0} + Dry run: would create file: {0} + {0} is the file full path. + + + Dry run: would delete file: {0} + Dry run: would delete file: {0} + {0} is the file full path. + + + Dry run: would move file '{0}' to '{1}'. + Dry run: would move file '{0}' to '{1}'. + {0} and {1} are file full paths. + PROJECT_MANIFEST PROJECT_MANIFEST diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf index a383e84c2284..9af4a5bae904 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.it.xlf @@ -2290,6 +2290,46 @@ Lo strumento '{1}' versione '{2}' è stato installato. La voce è stata aggiunta Converti un programma basato su file in un programma basato su progetto. + + Specify the output directory ({0}): + Specify the output directory ({0}): + {0} is the default value + + + Determines changes without actually modifying the file system + Determines changes without actually modifying the file system + + + + Dry run: would remove file-level directives from file: {0} + Dry run: would remove file-level directives from file: {0} + {0} is the file full path. + + + Dry run: would copy file '{0}' to '{1}'. + Dry run: would copy file '{0}' to '{1}'. + {0} and {1} are file full paths. + + + Dry run: would create directory: {0} + Dry run: would create directory: {0} + {0} is the directory full path. + + + Dry run: would create file: {0} + Dry run: would create file: {0} + {0} is the file full path. + + + Dry run: would delete file: {0} + Dry run: would delete file: {0} + {0} is the file full path. + + + Dry run: would move file '{0}' to '{1}'. + Dry run: would move file '{0}' to '{1}'. + {0} and {1} are file full paths. + PROJECT_MANIFEST PROJECT_MANIFEST diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf index 12b1a2d0d6e0..22a6f15f811c 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ja.xlf @@ -2290,6 +2290,46 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man ファイルベースのプログラムをプロジェクトベースのプログラムに変換します。 + + Specify the output directory ({0}): + Specify the output directory ({0}): + {0} is the default value + + + Determines changes without actually modifying the file system + Determines changes without actually modifying the file system + + + + Dry run: would remove file-level directives from file: {0} + Dry run: would remove file-level directives from file: {0} + {0} is the file full path. + + + Dry run: would copy file '{0}' to '{1}'. + Dry run: would copy file '{0}' to '{1}'. + {0} and {1} are file full paths. + + + Dry run: would create directory: {0} + Dry run: would create directory: {0} + {0} is the directory full path. + + + Dry run: would create file: {0} + Dry run: would create file: {0} + {0} is the file full path. + + + Dry run: would delete file: {0} + Dry run: would delete file: {0} + {0} is the file full path. + + + Dry run: would move file '{0}' to '{1}'. + Dry run: would move file '{0}' to '{1}'. + {0} and {1} are file full paths. + PROJECT_MANIFEST PROJECT_MANIFEST diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf index 6f1e0be285b6..e32cb9dfbeba 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ko.xlf @@ -2290,6 +2290,46 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man 파일 기반 프로그램을 프로젝트 기반 프로그램으로 변환합니다. + + Specify the output directory ({0}): + Specify the output directory ({0}): + {0} is the default value + + + Determines changes without actually modifying the file system + Determines changes without actually modifying the file system + + + + Dry run: would remove file-level directives from file: {0} + Dry run: would remove file-level directives from file: {0} + {0} is the file full path. + + + Dry run: would copy file '{0}' to '{1}'. + Dry run: would copy file '{0}' to '{1}'. + {0} and {1} are file full paths. + + + Dry run: would create directory: {0} + Dry run: would create directory: {0} + {0} is the directory full path. + + + Dry run: would create file: {0} + Dry run: would create file: {0} + {0} is the file full path. + + + Dry run: would delete file: {0} + Dry run: would delete file: {0} + {0} is the file full path. + + + Dry run: would move file '{0}' to '{1}'. + Dry run: would move file '{0}' to '{1}'. + {0} and {1} are file full paths. + PROJECT_MANIFEST PROJECT_MANIFEST diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf index 950656ce22f2..c0bbbc1caa67 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pl.xlf @@ -2290,6 +2290,46 @@ Narzędzie „{1}” (wersja „{2}”) zostało pomyślnie zainstalowane. Wpis Konwertuj program oparty na plikach na program oparty na projekcie. + + Specify the output directory ({0}): + Specify the output directory ({0}): + {0} is the default value + + + Determines changes without actually modifying the file system + Determines changes without actually modifying the file system + + + + Dry run: would remove file-level directives from file: {0} + Dry run: would remove file-level directives from file: {0} + {0} is the file full path. + + + Dry run: would copy file '{0}' to '{1}'. + Dry run: would copy file '{0}' to '{1}'. + {0} and {1} are file full paths. + + + Dry run: would create directory: {0} + Dry run: would create directory: {0} + {0} is the directory full path. + + + Dry run: would create file: {0} + Dry run: would create file: {0} + {0} is the file full path. + + + Dry run: would delete file: {0} + Dry run: would delete file: {0} + {0} is the file full path. + + + Dry run: would move file '{0}' to '{1}'. + Dry run: would move file '{0}' to '{1}'. + {0} and {1} are file full paths. + PROJECT_MANIFEST PROJECT_MANIFEST diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf index d8bb4763fa06..979a8397c694 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.pt-BR.xlf @@ -2290,6 +2290,46 @@ A ferramenta '{1}' (versão '{2}') foi instalada com êxito. A entrada foi adici Converta um programa baseado em arquivo em um programa baseado em projeto. + + Specify the output directory ({0}): + Specify the output directory ({0}): + {0} is the default value + + + Determines changes without actually modifying the file system + Determines changes without actually modifying the file system + + + + Dry run: would remove file-level directives from file: {0} + Dry run: would remove file-level directives from file: {0} + {0} is the file full path. + + + Dry run: would copy file '{0}' to '{1}'. + Dry run: would copy file '{0}' to '{1}'. + {0} and {1} are file full paths. + + + Dry run: would create directory: {0} + Dry run: would create directory: {0} + {0} is the directory full path. + + + Dry run: would create file: {0} + Dry run: would create file: {0} + {0} is the file full path. + + + Dry run: would delete file: {0} + Dry run: would delete file: {0} + {0} is the file full path. + + + Dry run: would move file '{0}' to '{1}'. + Dry run: would move file '{0}' to '{1}'. + {0} and {1} are file full paths. + PROJECT_MANIFEST PROJECT_MANIFEST diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf index b605c45c2d9e..6cda527f7daf 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.ru.xlf @@ -2290,6 +2290,46 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man Преобразование программы на основе файла в программу на основе проекта. + + Specify the output directory ({0}): + Specify the output directory ({0}): + {0} is the default value + + + Determines changes without actually modifying the file system + Determines changes without actually modifying the file system + + + + Dry run: would remove file-level directives from file: {0} + Dry run: would remove file-level directives from file: {0} + {0} is the file full path. + + + Dry run: would copy file '{0}' to '{1}'. + Dry run: would copy file '{0}' to '{1}'. + {0} and {1} are file full paths. + + + Dry run: would create directory: {0} + Dry run: would create directory: {0} + {0} is the directory full path. + + + Dry run: would create file: {0} + Dry run: would create file: {0} + {0} is the file full path. + + + Dry run: would delete file: {0} + Dry run: would delete file: {0} + {0} is the file full path. + + + Dry run: would move file '{0}' to '{1}'. + Dry run: would move file '{0}' to '{1}'. + {0} and {1} are file full paths. + PROJECT_MANIFEST PROJECT_MANIFEST diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf index 7eb51cb17ed3..258e628a4f27 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.tr.xlf @@ -2290,6 +2290,46 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man Dosya tabanlı bir programı proje tabanlı bir programa dönüştür. + + Specify the output directory ({0}): + Specify the output directory ({0}): + {0} is the default value + + + Determines changes without actually modifying the file system + Determines changes without actually modifying the file system + + + + Dry run: would remove file-level directives from file: {0} + Dry run: would remove file-level directives from file: {0} + {0} is the file full path. + + + Dry run: would copy file '{0}' to '{1}'. + Dry run: would copy file '{0}' to '{1}'. + {0} and {1} are file full paths. + + + Dry run: would create directory: {0} + Dry run: would create directory: {0} + {0} is the directory full path. + + + Dry run: would create file: {0} + Dry run: would create file: {0} + {0} is the file full path. + + + Dry run: would delete file: {0} + Dry run: would delete file: {0} + {0} is the file full path. + + + Dry run: would move file '{0}' to '{1}'. + Dry run: would move file '{0}' to '{1}'. + {0} and {1} are file full paths. + PROJECT_MANIFEST PROJECT_MANIFEST diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf index 3d1c446d846d..5a0e30d879ac 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hans.xlf @@ -2290,6 +2290,46 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man 将基于文件的程序转换为基于项目的程序。 + + Specify the output directory ({0}): + Specify the output directory ({0}): + {0} is the default value + + + Determines changes without actually modifying the file system + Determines changes without actually modifying the file system + + + + Dry run: would remove file-level directives from file: {0} + Dry run: would remove file-level directives from file: {0} + {0} is the file full path. + + + Dry run: would copy file '{0}' to '{1}'. + Dry run: would copy file '{0}' to '{1}'. + {0} and {1} are file full paths. + + + Dry run: would create directory: {0} + Dry run: would create directory: {0} + {0} is the directory full path. + + + Dry run: would create file: {0} + Dry run: would create file: {0} + {0} is the file full path. + + + Dry run: would delete file: {0} + Dry run: would delete file: {0} + {0} is the file full path. + + + Dry run: would move file '{0}' to '{1}'. + Dry run: would move file '{0}' to '{1}'. + {0} and {1} are file full paths. + PROJECT_MANIFEST PROJECT_MANIFEST diff --git a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf index af3ea934d94f..7d66f8e0a97d 100644 --- a/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf +++ b/src/Cli/dotnet/Commands/xlf/CliCommandStrings.zh-Hant.xlf @@ -2290,6 +2290,46 @@ Tool '{1}' (version '{2}') was successfully installed. Entry is added to the man 將檔案型程式轉換成專案型程式。 + + Specify the output directory ({0}): + Specify the output directory ({0}): + {0} is the default value + + + Determines changes without actually modifying the file system + Determines changes without actually modifying the file system + + + + Dry run: would remove file-level directives from file: {0} + Dry run: would remove file-level directives from file: {0} + {0} is the file full path. + + + Dry run: would copy file '{0}' to '{1}'. + Dry run: would copy file '{0}' to '{1}'. + {0} and {1} are file full paths. + + + Dry run: would create directory: {0} + Dry run: would create directory: {0} + {0} is the directory full path. + + + Dry run: would create file: {0} + Dry run: would create file: {0} + {0} is the file full path. + + + Dry run: would delete file: {0} + Dry run: would delete file: {0} + {0} is the file full path. + + + Dry run: would move file '{0}' to '{1}'. + Dry run: would move file '{0}' to '{1}'. + {0} and {1} are file full paths. + PROJECT_MANIFEST PROJECT_MANIFEST diff --git a/src/Cli/dotnet/CommonOptions.cs b/src/Cli/dotnet/CommonOptions.cs index 0e9de478c89b..92a43cc2ef58 100644 --- a/src/Cli/dotnet/CommonOptions.cs +++ b/src/Cli/dotnet/CommonOptions.cs @@ -240,23 +240,25 @@ public static Argument DefaultToCurrentDirectory(this Argument a private static bool IsCIEnvironmentOrRedirected() => new Telemetry.CIEnvironmentDetectorForTelemetry().IsCIEnvironment() || Console.IsOutputRedirected; + public const string InteractiveOptionName = "--interactive"; + /// /// A 'template' for interactive usage across the whole dotnet CLI. Use this as a base and then specialize it for your use cases. /// Despite being a 'forwarded option' there is no default forwarding configured, so if you want forwarding you can add it on a per-command basis. /// /// Whether the option accepts an boolean argument. If false, the option will be a flag. /// - // If not set by a user, this will default to true if the user is not in a CI environment as detected by . - // If this is set to function as a flag, then there is no simple user-provided way to circumvent the behavior. - // + /// If not set by a user, this will default to true if the user is not in a CI environment as detected by . + /// If this is set to function as a flag, then there is no simple user-provided way to circumvent the behavior. + /// public static ForwardedOption InteractiveOption(bool acceptArgument = false) => - new("--interactive") - { - Description = CliStrings.CommandInteractiveOptionDescription, - Arity = acceptArgument ? ArgumentArity.ZeroOrOne : ArgumentArity.Zero, - // this default is called when no tokens/options are passed on the CLI args - DefaultValueFactory = (ar) => !IsCIEnvironmentOrRedirected() - }; + new(InteractiveOptionName) + { + Description = CliStrings.CommandInteractiveOptionDescription, + Arity = acceptArgument ? ArgumentArity.ZeroOrOne : ArgumentArity.Zero, + // this default is called when no tokens/options are passed on the CLI args + DefaultValueFactory = (ar) => !IsCIEnvironmentOrRedirected() + }; public static Option InteractiveMsBuildForwardOption = InteractiveOption(acceptArgument: true).ForwardAsSingle(b => $"--property:NuGetInteractive={(b ? "true" : "false")}"); diff --git a/src/Cli/dotnet/InteractiveConsole.cs b/src/Cli/dotnet/InteractiveConsole.cs new file mode 100644 index 000000000000..2b6a6fdcbd58 --- /dev/null +++ b/src/Cli/dotnet/InteractiveConsole.cs @@ -0,0 +1,101 @@ +// 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 System.Diagnostics.CodeAnalysis; +using Microsoft.DotNet.Cli.Commands; + +namespace Microsoft.DotNet.Cli; + +public static class InteractiveConsole +{ + /// + /// If you need to confirm an action, Escape means "cancel" and that is fine. + /// If you need an answer where the "no" might mean something dangerous, Escape should not be used as implicit "no". + /// + /// + /// if the user confirmed the action, + /// if the user declined it, + /// if the user could not answer because --no-interactive was specified. + /// + public static bool? Confirm(string message, ParseResult parseResult, bool acceptEscapeForFalse) + { + if (parseResult.GetValue(CommonOptions.YesOption)) + { + return true; + } + + if (!parseResult.GetValue(CommonOptions.InteractiveOptionName)) + { + return null; + } + + Console.Write(AddPromptOptions(message)); + + while (true) + { + var key = Console.ReadKey(); + Console.WriteLine(); + + if (key.Key == ConsoleKey.Enter || KeyMatches(key, CliCommandStrings.ConfirmationPromptYesValue)) + { + return true; + } + + if ((acceptEscapeForFalse && key.Key == ConsoleKey.Escape) || KeyMatches(key, CliCommandStrings.ConfirmationPromptNoValue)) + { + return false; + } + + Console.Write(AddPromptOptions(string.Format(CliCommandStrings.ConfirmationPromptInvalidChoiceMessage, CliCommandStrings.ConfirmationPromptYesValue, CliCommandStrings.ConfirmationPromptNoValue))); + } + + static string AddPromptOptions(string message) + { + return $"{message} [{CliCommandStrings.ConfirmationPromptYesValue}/{CliCommandStrings.ConfirmationPromptNoValue}] ({CliCommandStrings.ConfirmationPromptYesValue}): "; + } + + 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)); + } + } + + public delegate bool Validator( + string? answer, + out TResult? result, + [NotNullWhen(returnValue: false)] out string? error); + + public static bool Ask( + string question, + ParseResult parseResult, + Validator validate, + out TResult? result) + { + if (!parseResult.GetValue(CommonOptions.InteractiveOptionName)) + { + result = default; + return false; + } + + while (true) + { + Console.Write(question); + Console.Write(' '); + + string? answer = Console.ReadLine(); + answer = string.IsNullOrWhiteSpace(answer) ? null : answer.Trim(); + if (!validate(answer, out result, out var error)) + { + Console.WriteLine(error); + } + else + { + return true; + } + } + } +} diff --git a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs index 229f81f857e0..1b88615bdcaf 100644 --- a/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs +++ b/test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs @@ -33,7 +33,7 @@ public void SameAsTemplate() new DirectoryInfo(dotnetProjectConvert) .EnumerateFileSystemInfos().Select(d => d.Name).Order() - .Should().BeEquivalentTo(["Program"]); + .Should().BeEquivalentTo(["Program", "Program.cs"]); new DirectoryInfo(Path.Join(dotnetProjectConvert, "Program")) .EnumerateFileSystemInfos().Select(f => f.Name).Order() @@ -140,7 +140,7 @@ public void MultipleEntryPointFiles() new DirectoryInfo(testInstance.Path) .EnumerateFileSystemInfos().Select(d => d.Name).Order() - .Should().BeEquivalentTo(["Program1", "Program2.cs"]); + .Should().BeEquivalentTo(["Program1", "Program1.cs", "Program2.cs"]); new DirectoryInfo(Path.Join(testInstance.Path, "Program1")) .EnumerateFileSystemInfos().Select(f => f.Name).Order() @@ -208,7 +208,7 @@ public void ExtensionCasing() new DirectoryInfo(testInstance.Path) .EnumerateFileSystemInfos().Select(f => f.Name).Order() - .Should().BeEquivalentTo(["Program"]); + .Should().BeEquivalentTo(["Program", "Program.CS"]); new DirectoryInfo(Path.Join(testInstance.Path, "Program")) .EnumerateFileSystemInfos().Select(f => f.Name).Order() @@ -230,7 +230,10 @@ public void FileContent(string content) new DirectoryInfo(testInstance.Path) .EnumerateFileSystemInfos().Select(f => f.Name).Order() - .Should().BeEquivalentTo(["Program"]); + .Should().BeEquivalentTo(["Program", "Program.cs"]); + + File.ReadAllText(Path.Join(testInstance.Path, "Program.cs")) + .Should().Be(content); new DirectoryInfo(Path.Join(testInstance.Path, "Program")) .EnumerateFileSystemInfos().Select(f => f.Name).Order() @@ -255,7 +258,7 @@ public void NestedDirectory() new DirectoryInfo(Path.Join(testInstance.Path, "app")) .EnumerateFileSystemInfos().Select(f => f.Name).Order() - .Should().BeEquivalentTo(["Program"]); + .Should().BeEquivalentTo(["Program", "Program.cs"]); new DirectoryInfo(Path.Join(testInstance.Path, "app", "Program")) .EnumerateFileSystemInfos().Select(f => f.Name).Order() @@ -285,7 +288,7 @@ public void DefaultItems() new DirectoryInfo(testInstance.Path) .EnumerateFileSystemInfos().Select(f => f.Name).Order() - .Should().BeEquivalentTo(["Program", "Resources.resx", "Util.cs", "my.json", "subdir"]); + .Should().BeEquivalentTo(["Program", "Program.cs", "Resources.resx", "Util.cs", "my.json", "subdir"]); new DirectoryInfo(Path.Join(testInstance.Path, "Program")) .EnumerateFileSystemInfos().Select(f => f.Name).Order() @@ -315,7 +318,7 @@ public void DefaultItems_MoreIncluded() new DirectoryInfo(testInstance.Path) .EnumerateFileSystemInfos().Select(f => f.Name).Order() - .Should().BeEquivalentTo(["Program", "Resources.resx", "Util.cs", "my.json"]); + .Should().BeEquivalentTo(["Program", "Program.cs", "Resources.resx", "Util.cs", "my.json"]); new DirectoryInfo(Path.Join(testInstance.Path, "Program")) .EnumerateFileSystemInfos().Select(f => f.Name).Order() @@ -341,7 +344,7 @@ public void DefaultItems_MoreExcluded() new DirectoryInfo(testInstance.Path) .EnumerateFileSystemInfos().Select(f => f.Name).Order() - .Should().BeEquivalentTo(["Program", "Resources.resx", "Util.cs", "my.json"]); + .Should().BeEquivalentTo(["Program", "Program.cs", "Resources.resx", "Util.cs", "my.json"]); new DirectoryInfo(Path.Join(testInstance.Path, "Program")) .EnumerateFileSystemInfos().Select(f => f.Name).Order() @@ -378,7 +381,7 @@ public void DefaultItems_ExcludedViaMetadata() new DirectoryInfo(testInstance.Path) .EnumerateFileSystemInfos().Select(f => f.Name).Order() - .Should().BeEquivalentTo(["Directory.Build.targets", "Program", "Resources.resx", "Util.cs", "my.json", "second.json"]); + .Should().BeEquivalentTo(["Directory.Build.targets", "Program", "Program.cs", "Resources.resx", "Util.cs", "my.json", "second.json"]); // `second.json` is excluded from the conversion. new DirectoryInfo(Path.Join(testInstance.Path, "Program")) @@ -420,7 +423,7 @@ class Util { public static string GetText() => "Hi from Util"; } new DirectoryInfo(testInstance.Path) .EnumerateFileSystemInfos().Select(f => f.Name).Order() - .Should().BeEquivalentTo(["Directory.Build.props", "Program", "Util.cs"]); + .Should().BeEquivalentTo(["Directory.Build.props", "Program", "Program.cs", "Util.cs"]); // Directory.Build.props is included as it's a None item. new DirectoryInfo(Path.Join(testInstance.Path, "Program")) @@ -471,7 +474,7 @@ class Util { public static string GetText() => "Hi from Util"; } new DirectoryInfo(subdir) .EnumerateFileSystemInfos().Select(f => f.Name).Order() - .Should().BeEquivalentTo(["Program", "Util.cs"]); + .Should().BeEquivalentTo(["Program", "Program.cs", "Util.cs"]); new DirectoryInfo(Path.Join(subdir, "Program")) .EnumerateFileSystemInfos().Select(f => f.Name).Order() @@ -521,7 +524,7 @@ class Util { public static string GetText() => "Hi from Util"; } new DirectoryInfo(subdir) .EnumerateFileSystemInfos().Select(f => f.Name).Order() - .Should().BeEquivalentTo(["Program"]); + .Should().BeEquivalentTo(["Program", "Program.cs"]); new DirectoryInfo(Path.Join(subdir, "Program")) .EnumerateFileSystemInfos().Select(f => f.Name).Order() @@ -585,10 +588,11 @@ public void ProcessingFails_Evaluation() public void ProcessingSucceeds() { var testInstance = _testAssetsManager.CreateTestDirectory(); - File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + var originalSource = """ #:package Humanizer@2.14.1 Console.WriteLine(); - """); + """; + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), originalSource); new DotnetCommand(Log, "project", "convert", "Program.cs") .WithWorkingDirectory(testInstance.Path) @@ -597,7 +601,10 @@ public void ProcessingSucceeds() new DirectoryInfo(testInstance.Path) .EnumerateFileSystemInfos().Select(f => f.Name).Order() - .Should().BeEquivalentTo(["Program"]); + .Should().BeEquivalentTo(["Program", "Program.cs"]); + + File.ReadAllText(Path.Join(testInstance.Path, "Program.cs")) + .Should().Be(originalSource); new DirectoryInfo(Path.Join(testInstance.Path, "Program")) .EnumerateFileSystemInfos().Select(f => f.Name).Order() diff --git a/test/dotnet.Tests/CompletionTests/snapshots/bash/DotnetCliSnapshotTests.VerifyCompletions.verified.sh b/test/dotnet.Tests/CompletionTests/snapshots/bash/DotnetCliSnapshotTests.VerifyCompletions.verified.sh index 1e61c0df86d4..4745292a88c3 100644 --- a/test/dotnet.Tests/CompletionTests/snapshots/bash/DotnetCliSnapshotTests.VerifyCompletions.verified.sh +++ b/test/dotnet.Tests/CompletionTests/snapshots/bash/DotnetCliSnapshotTests.VerifyCompletions.verified.sh @@ -1178,7 +1178,7 @@ _testhost_project_convert() { prev="${COMP_WORDS[COMP_CWORD-1]}" COMPREPLY=() - opts="--output --force --help" + opts="--output --force --interactive --dry-run --help" if [[ $COMP_CWORD == "$1" ]]; then COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) diff --git a/test/dotnet.Tests/CompletionTests/snapshots/pwsh/DotnetCliSnapshotTests.VerifyCompletions.verified.ps1 b/test/dotnet.Tests/CompletionTests/snapshots/pwsh/DotnetCliSnapshotTests.VerifyCompletions.verified.ps1 index 60fbc9bdda7f..74c47f83f124 100644 --- a/test/dotnet.Tests/CompletionTests/snapshots/pwsh/DotnetCliSnapshotTests.VerifyCompletions.verified.ps1 +++ b/test/dotnet.Tests/CompletionTests/snapshots/pwsh/DotnetCliSnapshotTests.VerifyCompletions.verified.ps1 @@ -680,6 +680,8 @@ Register-ArgumentCompleter -Native -CommandName 'testhost' -ScriptBlock { [CompletionResult]::new('--output', '--output', [CompletionResultType]::ParameterName, "Location to place the generated output.") [CompletionResult]::new('--output', '-o', [CompletionResultType]::ParameterName, "Location to place the generated output.") [CompletionResult]::new('--force', '--force', [CompletionResultType]::ParameterName, "Force conversion even if there are malformed directives.") + [CompletionResult]::new('--interactive', '--interactive', [CompletionResultType]::ParameterName, "Allows the command to stop and wait for user input or action (for example to complete authentication).") + [CompletionResult]::new('--dry-run', '--dry-run', [CompletionResultType]::ParameterName, "Determines changes without actually modifying the file system") [CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, "Show command line help.") [CompletionResult]::new('--help', '-h', [CompletionResultType]::ParameterName, "Show command line help.") ) diff --git a/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh b/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh index fa12d3005129..ebcf7c958169 100644 --- a/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh +++ b/test/dotnet.Tests/CompletionTests/snapshots/zsh/DotnetCliSnapshotTests.VerifyCompletions.verified.zsh @@ -676,6 +676,8 @@ _testhost() { '--output=[Location to place the generated output.]: :_files' \ '-o=[Location to place the generated output.]: :_files' \ '--force[Force conversion even if there are malformed directives.]' \ + '--interactive[Allows the command to stop and wait for user input or action (for example to complete authentication).]' \ + '--dry-run[Determines changes without actually modifying the file system]' \ '--help[Show command line help.]' \ '-h[Show command line help.]' \ ':file -- Path to the file-based program.: ' \