Functions/GenXdev.FileSystem/Rename-InProject.ps1

################################################################################
<#
.SYNOPSIS
Performs case-sensitive text replacement throughout a project directory.
 
.DESCRIPTION
Performs find and replace operations across files and folders in a project.
Skips common binary files and repository folders (.git, .svn).
Always use -WhatIf first to validate planned changes.
 
.PARAMETER Source
The directory, filepath, or directory+searchmask to process.
 
.PARAMETER FindText
The case-sensitive text to find and replace.
 
.PARAMETER ReplacementText
The text to replace FindText with.
 
.PARAMETER WhatIf
Shows what would happen if the cmdlet runs.
 
.EXAMPLE
Rename-InProject -Source .\src\*.js -FindText "tsconfig.json" `
    -ReplacementText "typescript.configuration.json"
 
.EXAMPLE
rip .\src\ "MyClass" "MyNewClass" -WhatIf
#>

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 sensitive)"
        )]
        [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
        ########################################################################
    )

    begin {
        # store original preferences for restoration
        $originalVerbosePreference = $VerbosePreference
        $originalWhatIfPreference = $WhatIfPreference

        try {
            # normalize and validate source path
            $sourcePath = Expand-Path $Source
            $searchPattern = "*"

            # split path 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"
                }
            }

            Write-Verbose "Source path: $sourcePath"
            Write-Verbose "Search pattern: $searchPattern"

            # enable verbose in whatif mode
            if ($WhatIfPreference -or $WhatIf) {
                $VerbosePreference = "Continue"
            }

            # list of extensions to skip
            $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"
            )
        }
        catch {

            $WhatIfPreference = $originalWhatIfPreference
            throw
        }
    }

    process {
        try {

            # get all files recursively excluding repos
            function Get-ProjectFiles([string] $dir, [string] $mask) {

                $result = [System.Collections.Generic.List[string]]::new()

                # skip repo directories
                if ([IO.Path]::GetFileName($dir) -in @(".svn", ".git")) {
                    return $result
                }

                # add matching files
                [IO.Directory]::GetFiles($dir, $mask) | ForEach-Object {
                    $null = $result.Add($_)
                }

                # process subdirectories
                [IO.Directory]::GetDirectories($dir, "*") | ForEach-Object {

                    if ([IO.Path]::GetFileName($_) -notin @(".svn", ".git")) {
                        Get-ProjectFiles $_ $mask | ForEach-Object {
                            $result.Add($_)
                        } | Out-Null
                    }
                }

                return $result
            }

            # process files
            Get-ProjectFiles -dir $sourcePath -mask $searchPattern |
            Sort-Object -Descending |
            ForEach-Object {
                $filePath = $_
                $extension = [IO.Path]::GetExtension($filePath).ToLower()

                # skip binary files
                if ($extension -notin $skipExtensions) {

                    try {

                        Write-Verbose "Processing file: $filePath"

                        # read and replace content
                        $content = [IO.File]::ReadAllText($filePath, [Text.Encoding]::UTF8)
                        $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)
                                Write-Verbose "Updated content in: $filePath"
                            }
                        }
                    }
                    catch {
                        Write-Warning "Failed to update content in: $filePath`n$_"
                    }

                    # process filename
                    $oldName = [IO.Path]::GetFileName($filePath)
                    $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 {
                                Move-ItemWithTracking -Path $filePath -Destination $newPath
                                Write-Verbose "Renamed file: $filePath -> $newPath"
                            }
                            catch {
                                Write-Warning "Failed to rename file: $filePath`n$_"
                            }
                        }
                    }
                }
            }

            # process directories
            Get-ChildItem -Path $sourcePath -Directory -Recurse |
            Sort-Object -Descending |
            Where-Object {
                $_.FullName -notlike "*\.git\*" -and
                $_.FullName -notlike "*\.svn\*"
            } |
            ForEach-Object {
                $dir = $_
                $oldName = $dir.Name
                $newName = $oldName.Replace($FindText, $ReplacementText)

                if ($oldName -ne $newName) {

                    $newPath = 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
                            Start-RoboCopy -Source $dir.FullName `
                                -DestinationDirectory $newPath -Move
                            Remove-AllItems ($dir.FullName) -DeleteFolder
                            Write-Verbose "Merged directory: $($dir.FullName) -> $newPath"
                        }
                        else {
                            try {
                                Move-ItemWithTracking -Path $dir.FullName -Destination $newPath
                                Write-Verbose "Renamed directory: $($dir.FullName) -> $newPath"
                            }
                            catch {
                                Write-Warning "Failed to rename directory: $($dir.FullName)`n$_"
                            }
                        }
                    }
                }
            }
        }
        catch {

            $WhatIfPreference = $originalWhatIfPreference
            throw
        }
    }

    end {
        # restore preferences

        $WhatIfPreference = $originalWhatIfPreference
    }
}
################################################################################