diff --git a/src/Aspire.ProjectTemplates/Aspire.ProjectTemplates.csproj b/src/Aspire.ProjectTemplates/Aspire.ProjectTemplates.csproj index 8e8798f1f2f..5428797b2cf 100644 --- a/src/Aspire.ProjectTemplates/Aspire.ProjectTemplates.csproj +++ b/src/Aspire.ProjectTemplates/Aspire.ProjectTemplates.csproj @@ -82,6 +82,8 @@ $(_VersionBase.Split('.')[0]) $(_VersionBase.Split('.')[1]) $(VersionMajor).$(VersionMinor) + <_RspFilePath>$(IntermediateOutputPath)replace-text-args.rsp + <_ReplaceTextScriptPath>$(RepoRoot)tools/scripts/replace-text.cs @@ -121,22 +123,25 @@ - - "@(SourceFiles, '" "')" - "@(Replacements, '" "')" - - + - - - - - - - + <_RspLines Include="--files" /> + <_RspLines Include="@(SourceFiles)" /> + <_RspLines Include="--replacements" /> + <_RspLines Include="@(Replacements)" /> - + + + + + diff --git a/tools/scripts/replace-text.cs b/tools/scripts/replace-text.cs index 205b5f36aec..dcfc1222c13 100644 --- a/tools/scripts/replace-text.cs +++ b/tools/scripts/replace-text.cs @@ -1,211 +1,183 @@ #:package Microsoft.Extensions.FileSystemGlobbing +#:package System.CommandLine using System.Collections.Concurrent; +using System.CommandLine; using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.FileSystemGlobbing.Abstractions; -if (args.Length == 0 || args.Contains("--help") || args.Contains("-h")) +var filesOption = new Option("--files") { - PrintUsage(); - return args.Length == 0 ? 1 : 0; -} + Description = "One or more file paths, directory paths, or glob patterns", + AllowMultipleArgumentsPerToken = true, + Required = true +}; -// Parse arguments -var paths = new List(); -var replacements = new List<(string Find, string Replace)>(); -var currentMode = (string?)null; -var replacementBuffer = new List(); - -for (var i = 0; i < args.Length; i++) -{ - var arg = args[i]; - - if (arg == "--files") - { - currentMode = "files"; - continue; - } - else if (arg == "--replacements") - { - currentMode = "replacements"; - continue; - } - - if (currentMode == "files") - { - paths.Add(arg); - } - else if (currentMode == "replacements") - { - replacementBuffer.Add(arg); - } - else - { - Console.Error.WriteLine($"Error: Unexpected argument '{arg}'. Use --files or --replacements to specify argument type."); - return 1; - } -} - -// Validate paths -if (paths.Count == 0) -{ - Console.Error.WriteLine("Error: No file paths provided. Use --files to specify paths."); - PrintUsage(); - return 1; -} - -// Validate and parse replacements -if (replacementBuffer.Count == 0) +var replacementsOption = new Option("--replacements") { - Console.Error.WriteLine("Error: No replacements provided. Use --replacements to specify find/replace pairs."); - PrintUsage(); - return 1; -} + Description = "Pairs of find/replace text values", + AllowMultipleArgumentsPerToken = true, + Required = true +}; -if (replacementBuffer.Count % 2 != 0) +var rootCommand = new RootCommand { - Console.Error.WriteLine($"Error: Replacement arguments must be provided in pairs (find, replace)."); - Console.Error.WriteLine($" Received {replacementBuffer.Count} arguments after --replacements."); - return 1; -} + filesOption, + replacementsOption +}; -for (var i = 0; i < replacementBuffer.Count; i += 2) +rootCommand.SetAction(result => { - var find = replacementBuffer[i]; - var replace = replacementBuffer[i + 1]; - - if (string.IsNullOrEmpty(find)) + try { - Console.Error.WriteLine($"Error: Find text at position {i + 1} in --replacements cannot be empty."); - return 1; - } + var paths = result.GetValue(filesOption) ?? Array.Empty(); + var replacementArgs = result.GetValue(replacementsOption) ?? Array.Empty(); - replacements.Add((find, replace)); -} + // Validate and parse replacements + if (replacementArgs.Length == 0) + { + Console.Error.WriteLine("Error: No replacements provided. Use --replacements to specify find/replace pairs."); + return 1; + } -Console.WriteLine($"Paths: {paths.Count}"); -foreach (var path in paths) -{ - Console.WriteLine($" '{path}'"); -} -Console.WriteLine($"Replacements: {replacements.Count}"); -foreach (var (find, replace) in replacements) -{ - Console.WriteLine($" '{find}' -> '{replace}'"); -} -Console.WriteLine(); + if (replacementArgs.Length % 2 != 0) + { + Console.Error.WriteLine($"Error: Replacement arguments must be provided in pairs (find, replace)."); + Console.Error.WriteLine($" Received {replacementArgs.Length} arguments after --replacements."); + return 1; + } -var filesToProcess = new HashSet(StringComparer.OrdinalIgnoreCase); + var replacements = new List<(string Find, string Replace)>(); + for (var i = 0; i < replacementArgs.Length; i += 2) + { + var find = replacementArgs[i]; + var replace = replacementArgs[i + 1]; -var matcher = new Matcher(); -var hasGlobPatterns = false; + if (string.IsNullOrEmpty(find)) + { + Console.Error.WriteLine($"Error: Find text at position {i + 1} in --replacements cannot be empty."); + return 1; + } -foreach (var pathValue in paths) -{ - if (File.Exists(pathValue)) - { - // If it's a direct file path add it as is - filesToProcess.Add(Path.GetFullPath(pathValue)); - } - else if (Directory.Exists(pathValue)) - { - // If it's a directory, include all files within it - matcher.AddInclude(Path.Combine(pathValue, "**/*")); - hasGlobPatterns = true; - } - else if (pathValue.Contains('*') || pathValue.Contains('?')) - { - matcher.AddInclude(pathValue); - hasGlobPatterns = true; - } -} + replacements.Add((find, replace)); + } -var currentDirectory = Directory.GetCurrentDirectory(); + Console.WriteLine($"Paths: {paths.Length}"); + foreach (var path in paths) + { + Console.WriteLine($" '{path}'"); + } + Console.WriteLine($"Replacements: {replacements.Count}"); + foreach (var (find, replace) in replacements) + { + Console.WriteLine($" '{find}' -> '{replace}'"); + } + Console.WriteLine(); -if (hasGlobPatterns) -{ - // Collect files from glob matching - var directoryInfo = new DirectoryInfoWrapper(new DirectoryInfo(currentDirectory)); - var matchResult = matcher.Execute(directoryInfo); + var filesToProcess = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var file in matchResult.Files) - { - filesToProcess.Add(Path.GetFullPath(Path.Combine(currentDirectory, file.Path))); - } -} - -if (filesToProcess.Count == 0) -{ - Console.WriteLine("No files matched the provided paths."); - return 0; -} + var matcher = new Matcher(); + var hasGlobPatterns = false; -Console.WriteLine($"Found {filesToProcess.Count} file(s) matching the provided paths."); -Console.WriteLine(); + foreach (var pathValue in paths) + { + if (File.Exists(pathValue)) + { + // If it's a direct file path add it as is + filesToProcess.Add(Path.GetFullPath(pathValue)); + } + else if (Directory.Exists(pathValue)) + { + // If it's a directory, include all files within it + matcher.AddInclude(Path.Combine(pathValue, "**/*")); + hasGlobPatterns = true; + } + else if (pathValue.Contains('*') || pathValue.Contains('?')) + { + matcher.AddInclude(pathValue); + hasGlobPatterns = true; + } + } -var processedCount = 0; -var modifiedCount = 0; -var errorCount = 0; -var errors = new ConcurrentBag<(string File, string Error)>(); + var currentDirectory = Directory.GetCurrentDirectory(); -Parallel.ForEach(filesToProcess, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, filePath => -{ - try - { - var content = File.ReadAllText(filePath); - var originalContent = content; + if (hasGlobPatterns) + { + // Collect files from glob matching + var directoryInfo = new DirectoryInfoWrapper(new DirectoryInfo(currentDirectory)); + var matchResult = matcher.Execute(directoryInfo); + + foreach (var file in matchResult.Files) + { + filesToProcess.Add(Path.GetFullPath(Path.Combine(currentDirectory, file.Path))); + } + } - foreach (var (find, replace) in replacements) + if (filesToProcess.Count == 0) { - content = content.Replace(find, replace, StringComparison.Ordinal); + Console.WriteLine("No files matched the provided paths."); + return 0; } - if (!string.Equals(content, originalContent, StringComparison.Ordinal)) + Console.WriteLine($"Found {filesToProcess.Count} file(s) matching the provided paths."); + Console.WriteLine(); + + var processedCount = 0; + var modifiedCount = 0; + var errorCount = 0; + var errors = new ConcurrentBag<(string File, string Error)>(); + + Parallel.ForEach(filesToProcess, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }, filePath => { - File.WriteAllText(filePath, content); - Interlocked.Increment(ref modifiedCount); - Console.WriteLine($"Modified: {Path.GetRelativePath(currentDirectory, filePath)}"); + try + { + var content = File.ReadAllText(filePath); + var originalContent = content; + + foreach (var (find, replace) in replacements) + { + content = content.Replace(find, replace, StringComparison.Ordinal); + } + + if (!string.Equals(content, originalContent, StringComparison.Ordinal)) + { + File.WriteAllText(filePath, content); + Interlocked.Increment(ref modifiedCount); + Console.WriteLine($"Modified: {Path.GetRelativePath(currentDirectory, filePath)}"); + } + + Interlocked.Increment(ref processedCount); + } + catch (Exception ex) + { + Interlocked.Increment(ref errorCount); + errors.Add((filePath, ex.Message)); + } + }); + + Console.WriteLine(); + Console.WriteLine($"Processed: {processedCount} file(s)"); + Console.WriteLine($"Modified: {modifiedCount} file(s)"); + + if (errorCount > 0) + { + Console.WriteLine($"Errors: {errorCount} file(s)"); + Console.WriteLine(); + Console.Error.WriteLine("Errors encountered:"); + foreach (var (file, error) in errors) + { + Console.Error.WriteLine($" {Path.GetRelativePath(currentDirectory, file)}: {error}"); + } + return 1; } - Interlocked.Increment(ref processedCount); + return 0; } catch (Exception ex) { - Interlocked.Increment(ref errorCount); - errors.Add((filePath, ex.Message)); + Console.Error.WriteLine($"Unexpected error: {ex.Message}"); + return 1; } }); -Console.WriteLine(); -Console.WriteLine($"Processed: {processedCount} file(s)"); -Console.WriteLine($"Modified: {modifiedCount} file(s)"); - -if (errorCount > 0) -{ - Console.WriteLine($"Errors: {errorCount} file(s)"); - Console.WriteLine(); - Console.Error.WriteLine("Errors encountered:"); - foreach (var (file, error) in errors) - { - Console.Error.WriteLine($" {Path.GetRelativePath(currentDirectory, file)}: {error}"); - } - return 1; -} - -return 0; - -static void PrintUsage() -{ - Console.Error.WriteLine(""" - Usage: dotnet replace-text.cs --files [path2] ... --replacements [ ...] - - Arguments: - --files One or more file paths, directory paths, or glob patterns - --replacements Pairs of find/replace text values - - Examples: - dotnet replace-text.cs --files "./src/**/*.cs" "./src/**/*.csproj" --replacements "!!VERSION!!" "7.0.5" "!!MAJOR_MINOR!!" "7.0" - dotnet replace-text.cs --files ./path/to/file.cs --replacements "oldText" "newText" - """); -} - +return rootCommand.Parse(args).Invoke();