Functions/GenXdev.FileSystem/Rename-InProject.ps1
|
############################################################################### <# .SYNOPSIS Performs text replacement throughout a project directory. .DESCRIPTION Recursively searches through files and directories in a project to perform text replacements. Handles both file/directory names and file contents. Skips common binary files and repository folders (.git, .svn) to avoid corruption. Uses UTF-8 encoding without BOM for file operations. Supports both case-sensitive and case-insensitive replacement modes. .LICENSE Copyright (C) 2026 René Vaessen / GenXdev This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. .PARAMETER Source The directory, filepath, or directory+searchmask to process. Defaults to current directory if not specified. .PARAMETER FindText The text pattern to search for in filenames and content. Case sensitivity is controlled by the CaseInsensitive parameter. .PARAMETER ReplacementText The text to replace all instances of FindText with. .PARAMETER CaseInsensitive Perform case-insensitive text replacement. When specified, matching is done without regard to case. .PARAMETER WhatIf Shows what changes would occur without actually making them. .EXAMPLE Rename-InProject -Source .\src\*.js -FindText "oldName" ` -ReplacementText "newName" .EXAMPLE rip . "MyClass" "MyNewClass" -WhatIf .EXAMPLE rip . "OLDNAME" "NewName" -CaseInsensitive #> function Rename-InProject { [CmdletBinding(SupportsShouldProcess = $true)] [Alias('rip')] param( ######################################################################## [Parameter( Mandatory = $false, Position = 0, ValueFromPipeline = $false, HelpMessage = 'The directory, filepath, or directory+searchmask' )] [Alias('src', 's')] [PSDefaultValue(Value = '.\')] [string] $Source, ######################################################################## [Parameter( Mandatory = $true, Position = 1, ValueFromPipeline = $false, HelpMessage = 'The text to find (case sensitivity controlled by CaseInsensitive parameter)' )] [Alias('find', 'what', 'from')] [ValidateNotNullOrEmpty()] [string] $FindText, ######################################################################## [Parameter( Mandatory = $true, Position = 2, ValueFromPipeline = $false, HelpMessage = 'The text to replace matches with' )] [Alias('into', 'txt', 'to')] [ValidateNotNull()] [string] $ReplacementText, ######################################################################## [Parameter( Mandatory = $false, ValueFromPipeline = $false, HelpMessage = 'Perform case-insensitive text replacement' )] [switch] $CaseInsensitive ######################################################################## ) begin { try { # normalize path and extract search pattern if specified $sourcePath = GenXdev\Expand-Path $Source $searchPattern = '*' # split source into path and pattern if not a directory if (![System.IO.Directory]::Exists($sourcePath)) { $searchPattern = [System.IO.Path]::GetFileName($sourcePath) $sourcePath = [System.IO.Path]::GetDirectoryName($sourcePath) if (![System.IO.Directory]::Exists($sourcePath)) { throw "Source directory not found: $sourcePath" } } Microsoft.PowerShell.Utility\Write-Verbose "Processing source path: $sourcePath" Microsoft.PowerShell.Utility\Write-Verbose "Using search pattern: $searchPattern" # extensions to skip to avoid corrupting binary files $skipExtensions = @( '.jpg', '.jpeg', '.gif', '.bmp', '.png', '.tiff', '.exe', '.dll', '.pdb', '.so', '.wav', '.mp3', '.avi', '.mkv', '.wmv', '.tar', '.7z', '.zip', '.rar', '.apk', '.ipa', '.cer', '.crt', '.pkf', '.db', '.bin' ) } catch { throw } } process { try { # recursive function to get all project files excluding repos function Get-ProjectFiles { [CmdletBinding()] [OutputType([System.Collections.Generic.List[string]])] [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseSingularNouns', 'Get-ProjectFiles' )] param( [string] $Dir, [string] $Mask ) $result = [System.Collections.Generic.List[string]]::new() # skip version control directories if ([IO.Path]::GetFileName($Dir) -in @('.svn', '.git')) { return $result } # collect matching files in current directory [IO.Directory]::GetFiles($Dir, $Mask) | Microsoft.PowerShell.Core\ForEach-Object -ErrorAction Continue { $null = $result.Add($_) } # recursively process subdirectories [IO.Directory]::GetDirectories($Dir, '*') | Microsoft.PowerShell.Core\ForEach-Object -ErrorAction Continue { if ([IO.Path]::GetFileName($_) -notin @('.svn', '.git')) { $null = Get-ProjectFiles $_ $Mask | Microsoft.PowerShell.Core\ForEach-Object -ErrorAction Continue { $null = $result.Add($_) } } } return $result } # process files in reverse order to handle renames safely Get-ProjectFiles -dir $sourcePath -mask $searchPattern | Microsoft.PowerShell.Utility\Sort-Object -Descending | Microsoft.PowerShell.Core\ForEach-Object -ErrorAction Continue { $filePath = $_ $extension = [IO.Path]::GetExtension($filePath).ToLower() # only process text files if ($extension -notin $skipExtensions) { try { Microsoft.PowerShell.Utility\Write-Verbose "Processing file: $filePath" # replace text in file contents $content = [IO.File]::ReadAllText($filePath, [Text.Encoding]::UTF8) if ($CaseInsensitive) { $newContent = $content.Replace($FindText, $ReplacementText, [StringComparison]::OrdinalIgnoreCase) } else { $newContent = $content.Replace($FindText, $ReplacementText) } if ($content -ne $newContent) { if ($PSCmdlet.ShouldProcess($filePath, 'Replace content')) { $utf8 = [Text.UTF8Encoding]::new($false) [IO.File]::WriteAllText($filePath, $newContent, $utf8) Microsoft.PowerShell.Utility\Write-Verbose "Updated content in: $filePath" } } } catch { Microsoft.PowerShell.Utility\Write-Warning "Failed to update content in: $filePath`n$_" } # handle filename changes $oldName = [IO.Path]::GetFileName($filePath) if ($CaseInsensitive) { $newName = $oldName.Replace($FindText, $ReplacementText, [StringComparison]::OrdinalIgnoreCase) } else { $newName = $oldName.Replace($FindText, $ReplacementText) } if ($oldName -ne $newName) { $newPath = [IO.Path]::Combine( [IO.Path]::GetDirectoryName($filePath), $newName) if ($PSCmdlet.ShouldProcess($filePath, 'Rename file')) { try { if ("$filePath".ToLowerInvariant() -eq "$newPath".ToLowerInvariant()) { $newPath = "$newPath.$([DateTime]::Now.Ticks).tmp" $null = GenXdev\Move-ItemWithTracking -Path $filePath ` -Destination "$newPath.tmp12389" Microsoft.PowerShell.Utility\Write-Verbose "Renamed file: $filePath -> $newPath" $filePath = $newPath } $null = GenXdev\Move-ItemWithTracking -Path $filePath ` -Destination $newPath Microsoft.PowerShell.Utility\Write-Verbose "Renamed file: $filePath -> $newPath" } catch { Microsoft.PowerShell.Utility\Write-Warning "Failed to rename file: $filePath`n$_" } } } } } # process directories in reverse order Microsoft.PowerShell.Management\Get-ChildItem -LiteralPath $sourcePath -Directory -Recurse | Microsoft.PowerShell.Utility\Sort-Object -Descending | Microsoft.PowerShell.Core\Where-Object { $_.FullName -notlike '*\.git\*' -and $_.FullName -notlike '*\.svn\*' } | Microsoft.PowerShell.Core\ForEach-Object -ErrorAction Continue { $dir = $_ $oldName = $dir.Name if ($CaseInsensitive) { $newName = $oldName.Replace($FindText, $ReplacementText, [StringComparison]::OrdinalIgnoreCase) } else { $newName = $oldName.Replace($FindText, $ReplacementText) } if ($oldName -ne $newName) { $newPath = GenXdev\Expand-Path ( [IO.Path]::Combine($dir.Parent.FullName, $newName)) if ($PSCmdlet.ShouldProcess($dir.FullName, 'Rename directory')) { if ([IO.Directory]::Exists($newPath)) { # merge directories if target exists GenXdev\Start-RoboCopy -Source $dir.FullName ` -DestinationDirectory $newPath -Move if (Microsoft.PowerShell.Management\Test-Path $dir.FullName) { cmd.exe /c RD /S /Q $dir.FullName } Microsoft.PowerShell.Utility\Write-Verbose "Merged directory: $($dir.FullName) -> $newPath" } else { try { $null = GenXdev\Move-ItemWithTracking -Path $dir.FullName ` -Destination $newPath Microsoft.PowerShell.Utility\Write-Verbose "Renamed directory: $($dir.FullName) -> $newPath" } catch { Microsoft.PowerShell.Utility\Write-Warning "Failed to rename directory: $($dir.FullName)`n$_" } } } } } } catch { throw } } end { } } |