pf-string-replace.ps1

function Update-String_Replace_Multiple {
    [CmdLetBinding()]
    param (
        [HashTable]$replacements, 
        [switch]$all,
        [Parameter(ValueFromPipeline=$true)]
        [String]$src
    )
    begin {
        $rep = @()
        if ($replacements) {
            $rep += $replacements.GetEnumerator() |  ForEach-Object { 
                [PSCustomObject]@{Find=[RegEx]::Escape($_.Key); ReplaceWith = $_.Value; length = $_.Key.Length } } 
            # Gives priority to longer values to replace
            $rep = $rep | Sort-Object Find -Descending | Sort-Object Length -Descending
        }
    }
    process {
        foreach ($r in $rep) {
            $result = $src -ireplace  $r.Find, $r.ReplaceWith
            if ( -not $all -and ( $result -ne $src) ) {
                return $result
            }
            $src = $result
        }
        return $src
    }
}
function Update-String_Replace_Multiple:::Test {
    'a/b/c/d/' | Update-String_Replace_Multiple | assert 'a/b/c/d/' 
    'a/b/c\d/' | Update-String_Replace_Multiple -all @{'b/'='2'; 'c\'='3'} | assert 'a/23d/'
    'a/b/c/d/' | Update-String_Replace_Multiple -all @{'b'='2'; d='4'} | assert 'a/2/c/4/'
    'a/b/c/d/' | Update-String_Replace_Multiple -all @{'B'='2'; d='4'} | assert 'a/2/c/4/'
} 

function Update-FileContent_Replace_Multiple {
    [CmdLetBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        $Path,
        [HashTable]$replacements, 
        [switch]$all,
        [switch]$PSExpressions,
        $destination
    )
    process {
        $path = Get-Path $path
        if ($path) {

            if (-not $destination) {
                $destination = $path
            }

            $enc = Get-FileEncoding -FullName $path
            $content = ( Get-Content -Raw -Path $path )
            $newContent = $content | Update-String_Replace_Multiple -replacements $replacements -all:$all.IsPresent
            if ($newContent -ne $content) {
                Write-Verbose "Replacing from '$path' "
                Set-Content -Path $destination $newContent -Encoding $enc -NoNewline
            }
        }
    }
}

#Replace content in a file without changing the encoding
function Update-FileContent_Replace {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        $find, 
        $replacement = '',
        [Parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
        $file
    )
    begin {
        $find = [regex]::Escape( $find )
        $find = '(?ms)' + $find.Replace('\*','(.*?)')
    }
    process {
        $path = Get-Path $file
        if ($path) {
            $enc = Get-FileEncoding -FullName $path
            $content = ( Get-Content -Raw -Path $path )
            $newContent = $content -replace $find, $replacement

            if ($PSExpressions) {
                $psExp = '"' + $newContent + '"'
                $newContent = Invoke-Expression -Command $psExp
            }

            if ($newContent -ne $content) {
                Write-Verbose "Replacing in '$path' , '$find' -> '$replacement' "
                Set-Content -Path $path $newContent -Encoding $enc -NoNewline
            }
        }
    }
}

function Update-FolderContent_Replace {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        $folder, 
        [Parameter(Mandatory=$true)]
        $currentValue, 
        [Parameter(Mandatory=$true)]
        $newValue, 
        $filter = '*.xml'
    )
    process {
        $folder = Get-Path $folder

        # Replace Content
        $matcheList = Get-ChildItem -Path $folder -Include $filter -Recurse | 
            Select-String -SimpleMatch $currentValue
        if ($matcheList) {
            $matcheList.Path | Update-FileContent_Replace $currentValue $newValue
        }

        # Rename files
        $torename = Get-ChildItem -Path $folder -fi "*$currentValue.*" -Recurse -File
        $torename | Rename-Item_Replace -currentValue $currentValue -newValue $newValue

        # Rename Folders
        $torename = Get-ChildItem -Path $folder -fi "*$currentValue.*" -Recurse -Directory
        $torename | Rename-Item_Replace -currentValue $currentValue -newValue $newValue
    }
}

function Invoke-PSTemplate {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline=$true)]
        $templateContent,
        $TemplateContext
    )
    process {
        $quotesScaped = $templateContent.Replace('`','``')
        $quotesScaped = $quotesScaped.Replace('"','`"')
        try {
            $Content = Invoke-Expression "`"$quotesScaped`"" -ErrorAction SilentlyContinue 
        }
        catch {
            # For now ignoring any errors
            $Content = $templateContent
        }
        return $Content
    }
}

#Replace content by executing Powershell expressions indicated in the file content
function Update-File_Replace_PSTemplate {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline=$true)]
        $file,
        [ScriptBlock]$fileNameRenamer,
        [switch]$NoRename,
        $TemplateContext
    )
    begin {
        if (-not $fileNameRenamer) {
            if ($NoRename) {
                $fileNameRenamer = { param($filename)
                    $filename 
                }
            }
            else {
                $fileNameRenamer = { param([string]$fullname) 
                    Get-NewName_Preextension $fullname
                }
            }
        }
    }
    process {
        $file = Get-Path $file
        $templateContent = Get-Content -Path $file -Raw
        $enc = Get-FileEncoding -FullName $file

        $Content = Invoke-PSTemplate -templateContent $templateContent -TemplateContext $TemplateContext

        $newFile = $fileNameRenamer.Invoke($file)
        Set-Content -Path $newFile -Value $Content -Encoding $enc -Verbose -NoNewline
        return $newFile
    }
}
function Update-File_Replace_PSTemplate:::Example {
    $testFolder = "$env:TEMP\Update-File_Replace_PSTemplate"
    New-Folder_EnsureExists -folder $testFolder -New
    $testFile = "$testFolder\input.sql.pstemplate"
    $testFile = "$testFolder\input"
    
    $templateContext = @{ A = 1; B = @{ C = 'AAA' } }
    Write-Debug $templateContext

    $content = @'
     $($templateContext.A)
     '$($templateContext.A)'
     "$($templateContext.A)"
     `$($templateContext.A)`
     $($templateContext.B.C)
'@


    Set-Content -Path $testFile -Value $content -NoNewline

    $newFile = $testFile | Update-File_Replace_PSTemplate -fileNameRenamer {
        param([string]$fullname) 
            Get-NewName_Preextension $fullname
        }
    Write-Host $newFile
    Write-Host (Get-Content $newFile -Raw)
}

function Update-Replace_Block {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        $startBlock,
        $endBlock,
        $replacement = '',
        [Parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
        $content
    )
    begin {
        $startGroup = 'start'
        $endGroup = 'end'
        $find = "(?<$startGroup>$startBlock\s*(`r?)`n)(?<mid>.*?)(?<$endGroup>(`r?)`n\s*$endBlock)"
        $regex = [Regex]$find
        $callback = {
            param($match)
            $startCaptured = $match.Groups[$startGroup].value
            $endCaptured = $match.Groups[$endGroup].value
            $startCaptured + $replacement + $endCaptured
        }
    }
    process {
        $newContent = $regex.Replace($content, $callback)
        return $newContent
    }
}
function Update-Replace_Block:::Test {
    $content = '
        line 1
        #start
        line mid
        #end
        line 2
    '

    $content | Update-Replace_Block -startBlock '#start' -endBlock '#end' -replacement 'replace'
}

function Update-FileContent_Replace_Block {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
        $file,
        [Parameter(Mandatory=$true)]
        $startBlock,
        $endBlock,
        $replacement = ''
    )
    begin {
        $startGroup = 'start'
        $endGroup = 'end'
        $find = "(?<$startGroup>$startBlock\s*(`r?)`n)(?<mid>.*?)(?<$endGroup>(`r?)`n\s*$endBlock)"
        $regex = [Regex]$find
        $callback = {
            param($match)
            $startCaptured = $match.Groups[$startGroup].value
            $endCaptured = $match.Groups[$endGroup].value
            $startCaptured + $replacement + $endCaptured
        }
    }
    process {
        $path = Get-Path $file
        if ($path) {
            $enc = Get-FileEncoding -FullName $path
            $content = [System.IO.File]::ReadAllText($path)
            $newContent = $regex.Replace($content, $callback)
            if ($newContent -ne $content) {
                Write-Verbose "Replacing in '$path' , '$find' -> '$replacement' "
                Set-Content -Path $path $newContent -Encoding $enc -NoNewline
            }
        }
    }
}
function Update-FileContent_Replace_Block:::Test {
    $testfile = "$env:TEMP\Update-FileContent_Replace_Block.txt"
    $content = '
        line 1
        #start
        line mid
        #end
        line 2
    '

    Set-Content -Value $content -Path $testfile -NoNewline
    Update-FileContent_Replace_Block -file $testfile -startBlock '#start' -endBlock '#end' -replacement 'replace'
    $expected = '
        line 1
        #start
replace
        #end
        line 2
    '

    $actual = Get-Content -Path $testfile -Raw
    $actual | assert -eq $expected
}

#Replace content in a file without changing the encoding
function Update-FileContent_Replace {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        $find, 
        $replacement = '',
        [Parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
        $file
    )
    begin {
        $find = [regex]::Escape( $find )
        $find = '(?ims)' + $find.Replace('\*','(.*?)')
        $regex = [Regex]$find
    }
    process {
        $path = Get-Path $file
        if ($path) {
            $enc = Get-FileEncoding -FullName $path
            $content = [System.IO.File]::ReadAllText($path)
            $newContent = $regex.Replace($content, $replacement)
            if ($newContent -ne $content) {
                Write-Verbose "Replacing in '$path' , '$find' -> '$replacement' "
                Set-Content -Path $path -Value $newContent -Encoding $enc -NoNewline
            }
        }
    }
}
function Update-FileContent_Replace:::Test {
    $testfile = "$env:TEMP\Update-FileContent_Replace.txt"
 
    function GenTestContent ($lines = 10) {
        begin { @(1..$lines) }
        process { $_ }    
        end { @(1..$lines) }
    }

    GenTestContent > $testfile 
    $replacement = 'AAAA'
    $testfile | Update-FileContent_Replace '5' $replacement
    $newContent = Get-Content $testfile -Raw
    $newContent.Contains($replacement) | Assert $true

    # on line with *
    'start between end' | GenTestContent > $testfile 
    $testfile | Update-FileContent_Replace 'start * end' 'start MID end'
    $newContent = Get-Content $testfile -Raw
    $newContent.Contains('start MID end') | Assert $true

    # Multiline with *
    'begin between inter', 'inter between close' | GenTestContent > $testfile 
    $testfile | Update-FileContent_Replace 'begin * close' 'begin MID close'
    $newContent = Get-Content $testfile  -Raw
    $newContent.Contains('begin MID close') | Assert $true

    # Multiline with *
    ' { between } ' | GenTestContent > $testfile 
    $testfile | Update-FileContent_Replace '{ * }' '{ MID }'
    $newContent = Get-Content $testfile -Raw
    $newContent.Contains('{ MID }') | Assert $true

    # Multiline with *
    ' <# inter ',  ' inter #> '| GenTestContent > $testfile 
    $testfile | Update-FileContent_Replace '<# * #>' '<# MID #>'
    $newContent = Get-Content $testfile -Raw
    $newContent.Contains('<# MID #>') | Assert $true

    Remove-Item $testfile -Force
}