functions/Replace-InFile.ps1

<#
.Synopsis
Replaces text in files while preserving the encoding
.Description
Replaces text in files while preserving the encoding. Optionally, the file's time information is kept as well.
The result is output to the pipeline if no -Overwrite is specified.
If the encoding can not be determined, ASCII is assumed.
(Obviously this is no Set but a Replace operation, but there is no verb for Replace actions in the Powershell naming guidelines)
.Parameter Pattern
Search as a regular expression
.Parameter Replacement
Replaced text
.Parameter Path
File or directory name
.Parameter Recurse
Work files recursively in subdirectories
.Parameter CaseSensitive
Case-sensitive search
.Parameter Unix
For search patterns with end of line, this is searched for as a Unix line end (only NewLine instead of LineFeed + NewLine)
.Parameter Overwrite
The edited texts are copied to the original files (instead of given to the pipeline)
.Parameter Force
For -Overwrite: Even unchanged texts are written to the original files
.Parameter PreserveDate
The modified files retain the original time stamps
.Parameter Quiet
No output at the console
.Parameter OEM
Files in ASCII format are processed with OEM character set 850 (instead of Windows character set 1252)
.Parameter Encoding
Force writing of files in this encoding
.Inputs
File names can be passed over the pipeline
.Outputs
Edited texts, if not -Overwrite is specified
.Example
Replace-InFile -Pattern "Mister" -Replacement "Lady" -Path Test.txt -Quiet > result.txt
 
Replaces Mister with Lady in the file "Test.txt" and writes the result to result.txt
.Example
dir | Replace-InFile -Pattern "12" -Replacement "34" -Overwrite
 
Replaces 12 with 34 in all files of the current directory
.Example
gci | Replace-InFile -Pattern "late" -Replacement "later" -CaseSensitive -Recurse -OEM
 
Replaces late with later in all files of the current directory and all subdirectories.
The case is case-sensitive. ASCII files are interpreted as OEM files.
The result is not written back to the files, but is output to the pipeline.
.Example
Get-ChildItem "*.txt" | Replace-InFile -Pattern "t$" -Replacement "T" -Encoding UNICODE -Overwrite
 
Replaces t at the end of the line with T in all txt files of the current directory.
The files are written in UNICODE encoding.
.Example
Get-ChildItem "*.txt" | Replace-InFile -Pattern "t\r\n" -Replacement "T\r\n" -Overwrite
 
Replaces t at the end of the line with T in all txt files of the current directory.
.Example
"*.txt" | Replace-InFile -Pattern "<NL>" -Replacement "`n" -enc "Ascii" -VERBOSE
 
Replaces <NL> with an end of line in all txt files of the current directory.
The result is output in ASCII encoding. There is a verbose output.
.Example
Replace-InFile -Pattern "away" -Path "*.txt" -Overwrite -PreserveDate -WhatIf
 
Removes the expression away in all txt files of the current directory, preserving the time stamp of the
files. No change is made because of the switch -WhatIf.
.Example
Replace-InFile "*" -patt "Search" -repl "Replace" -rec -enc UTF8 -u -over
 
Replaces Search with Replace in all files of the current directory and all subdirectories.
UTF8 is written as encoding, line breaks are interpreted as Unix line breaks.
.Notes
Author: Markus Scholtes
Version: 1.0
Date: 2017-02-06
#>

function Replace-InFile
{
    [CmdletBinding(SupportsShouldProcess=$TRUE)]
    param(
        [parameter(Mandatory=$TRUE, Position=0)] [STRING]$Pattern,
        [parameter(Position=1)] [STRING][AllowEmptyString()]$Replacement,
        [parameter(Mandatory=$FALSE, Position=2, ValueFromPipeline=$TRUE)] [STRING][AllowEmptyString()]$Path,
        [SWITCH]$CaseSensitive,
        [SWITCH]$Unix,
        [SWITCH]$Overwrite,
        [SWITCH]$Force,
        [SWITCH]$Recurse,
        [SWITCH]$PreserveDate,
        [SWITCH]$Quiet,
        [SWITCH]$OEM,
        [Microsoft.PowerShell.Commands.FileSystemCmdletProviderEncoding]$Encoding = "Unknown"
    )


    BEGIN
    {
        function Get-FileEncoding {
        ##############################################################################
        ##
        ## Get-FileEncoding
        ##
        ## From Windows PowerShell Cookbook (O'Reilly)
        ## by Lee Holmes (http://www.leeholmes.com/guide)
        ##
        ## expandend and switched to FileSystemCmdletProviderEncoding
        ## by Markus Scholtes, 2014
        ##
        ##############################################################################
        <#
 
        .SYNOPSIS
 
        Gets the encoding of a file
 
        .EXAMPLE
 
        Get-FileEncoding.ps1 .\UnicodeScript.ps1
 
        Unicode
 
        .EXAMPLE
 
        Get-ChildItem *.ps1 | Select FullName, @{n='Encoding';e={Get-FileEncoding $_.FullName}} | where {$_.Encoding -ne 'ASCII'}
 
        This command gets ps1 files in current directory where encoding is not ASCII
 
        .EXAMPLE
 
        Get-ChildItem *.ps1 | Select FullName, @{n='Encoding';e={Get-FileEncoding $_.FullName}} | where {$_.Encoding -ne 'ASCII'} | foreach {(Get-Content $_.FullName) | Set-Content $_.FullName -Encoding ASCII}
 
        Same as previous example but fixes encoding using set-content
 
        #>


        ## The path of the file to get the encoding of.
        param($PATH)

        # Markus Scholtes, 2014
        # converts Encoding value of type [System.Text.Encoding] to type [Microsoft.Powershell.Commands.FileSystemCmdletProviderEncoding]
        #
        # Example
        # Convert-EncodingType ([System.Text.Encoding]::'ASCII')
        # Types can be output by using:
        #[Microsoft.Powershell.Commands.FileSystemCmdletProviderEncoding] | gm -Static -MemberType Property
        #[System.Text.Encoding] | gm -Static -MemberType Property

        function Convert-EncodingType([System.Text.Encoding]$KODIERUNG)
        {
            if ($KODIERUNG)
            {    [Microsoft.Powershell.Commands.FileSystemCmdletProviderEncoding]$KODIERUNG.BodyName.ToUpper().Replace("US-","").Replace("-","").Replace("UNICODEFFFE","BigEndianUnicode").Replace("UTF16BE","BigEndianUnicode").Replace("UTF16","Unicode").Replace("ISO88591","Default") }
            else
            { $NULL }
        }

        Set-StrictMode -Version Latest

        ## The hashtable used to store our mapping of encoding bytes to their
        ## name. For example, "255-254 = Unicode"
        $ENCODINGS = @{}

        ## Find all of the encodings understood by the .NET Framework. For each,
        ## determine the bytes at the start of the file (the preamble) that the .NET
        ## Framework uses to identify that encoding.
        $ENCODINGMEMBERS = [System.Text.Encoding] | Get-Member -Static -MemberType Property

        $ENCODINGMEMBERS | Foreach-Object {
            $ENCODINGBYTES = [System.Text.Encoding]::($_.Name).GetPreamble() -join '-'
            $ENCODINGS[$ENCODINGBYTES] = $_.Name
        }

        ## Find out the lengths of all of the preambles.
        $ENCODINGLENGTHS = $ENCODINGS.Keys | Where-Object { $_ } | Foreach-Object { ($_ -split "-").Count }

        ## Assume the encoding is ASCII by default
        $RESULT = "ASCII"

        if (($NULL -ne $PATH) -and ($PATH -ne "")) {
            if (Get-Content $PATH)
            {
                ## Go through each of the possible preamble lengths, read that many bytes
                ## from the file, and then see if it matches one of the encodings we know
                ## about.
                foreach ($ENCODINGLENGTH in $ENCODINGLENGTHS | Sort-Object -Descending)
                {
                    $BYTES = (Get-Content -Encoding BYTE -Readcount $ENCODINGLENGTH $PATH)[0]
                    $ENCODING = $ENCODINGS[$BYTES -join '-']

                    ## If we found an encoding that had the same preamble bytes, save that
                    ## output and break.
                    if ($ENCODING)
                    {
                        $RESULT = $ENCODING
                        break
                    }
                }
            }
        }

        ## Finally, output the encoding.
        Convert-EncodingType ([System.Text.Encoding]::$RESULT)
        }

        # Working function
        function Request-File([STRING]$Name)
        {
            if (!$Quiet) { Write-Output "Processing file '$Name'" }

            if ($PreserveDate)
            {    $Datei = Get-Item "$Name"
                $LastAccess = $Datei.LastAccessTime
                $Creation = $Datei.CreationTime
                $LastWrite = $Datei.LastWriteTime
            }

            # determine the current encoding of the source file
            $ActualEncoding = Get-FileEncoding "$Name"

            # ReadAllText must be used to read the contents of the file
            # so it is inserted into a string and not a string array
            try {
                Write-Verbose "Reading file $Name."
                if ($ActualEncoding -eq "ASCII")
                { # ASCII-Encoding, select the right codepage for the umlauts
                    if ($OEM)
                    { # DOS codepage
                        $Inhalt = [IO.File]::ReadAllText($Name, [System.Text.Encoding]::GetEncoding(850))
                    }
                    else
                    { # Windows codepage
                        $Inhalt = [IO.File]::ReadAllText($Name, [System.Text.Encoding]::GetEncoding(1252))
                    }
                }
                else
                { # other encoding, ReadAllText recognizes the correct one
                    $Inhalt = [IO.File]::ReadAllText($Name)
                }
                Write-Verbose "Read from file $Name finished."
            }
            catch [Management.Automation.MethodInvocationException]
            {
                Write-Error $ERROR[0]
                return
            }

            if (!$Quiet) { Write-Output "$($RegEx.Matches($Inhalt).Count) matches" }

            if (-not $Overwrite)
            {
                if (-not $WHATIFPREFERENCE)
                {
                    $RegEx.Replace($Inhalt, $Replacement)
                }
                return
            }

            if ($Force -or ($Inhalt -cne $RegEx.Replace($Inhalt, $Replacement)))
            {
                Write-Verbose "Writing to $Name."
                if (-not $WHATIFPREFERENCE)
                {
                    try
                    {
                        if ($Encoding -eq "Unknown")
                        { # if parameter Encoding is not set, keep current encoding
                            Write-Verbose "Using current encoding $ActualEncoding"
                            $TargetEncoding = $ActualEncoding
                        }
                        else
                        { # use chosen encoding
                            $TargetEncoding = $Encoding
                        }
                        if ($TargetEncoding -eq "ASCII")
                        { # ASCII-Encoding, select the right codepage for the umlauts
                            if ($OEM)
                            { # DOS codepage
                                [IO.File]::WriteAllText("$Name", $RegEx.Replace($Inhalt, $Replacement), [System.Text.Encoding]::GetEncoding(850))
                            }
                            else
                            { # Windows codepage
                                [IO.File]::WriteAllText("$Name", $RegEx.Replace($Inhalt, $Replacement), [System.Text.Encoding]::GetEncoding(1252))
                            }
                        }
                        else
                        { # andere Kodierung
                            [IO.File]::WriteAllText("$Name", $RegEx.Replace($Inhalt, $Replacement), [System.Text.Encoding]::$TargetEncoding)
                        }
                    }
                    catch [Management.Automation.MethodInvocationException]
                    {
                        Write-Error $ERROR[0]
                        return
                    }
                }

                if ($PreserveDate)
                {    # Set time stamp to original
                    Write-Verbose "Setting original time stamp of the file."
                    if (-not $WHATIFPREFERENCE) {
                        $Datei.LastAccessTime = $LastAccess
                        $Datei.CreationTime = $Creation
                        $Datei.LastWriteTime = $LastWrite
                    }
                }
                Write-Verbose "Writing to $Name finished."
            }
            else
            {
                if (!$Quiet) {
                    if ($Inhalt -eq "")
                    { Write-Output "File is empty." } else { Write-Output "No change in text." }
                }
            }
        }

        # Replace "$" with "\r$" in regular expression (not if -Unix is set)
        # \$ has to be preserved
        if (-not $Unix)
        {
            $NewPattern = $Pattern -replace '(?<!\\)\$', '\r$'
        }
        else
        {
            Write-Verbose 'Search for "Unix" linefeed.'
            $NewPattern = $Pattern
        }

        # create array of Regex options and the RegEx object
        $Options = @()
        $Options += "Multiline"
        if (-not $CaseSensitive)
        { $Options += "IgnoreCase" } else { Write-Verbose 'Case-sensitive search.' }

        $RegEx = New-Object Text.RegularExpressions.Regex $NewPattern, $Options
        if ($Encoding -eq "Unknown")
        { Write-Verbose "Use current encoding." } else { Write-Verbose "Use encoding $Encoding." }

    }

    PROCESS
    {
        # parameter or pipeline object is handed to $Path
        if ($_ -ne $Null)
        { # when there is a name in the pipeline, use it
            $Name = $_
        } else {
            $Name = $Path
        }

        if (($Name -is [System.IO.FileInfo]) -or ($Name -is [System.IO.DirectoryInfo]))
        { # if it is a FileInfo object or a DirectoryInfo object, determine path name
            $FileObject = $Name
        }
        else
        { # if it is a String, determine complete path name and object
            $FileObject = Get-Item $Name -ErrorAction 'SilentlyContinue'

            # no file or directory found, end function
            if ($Null -eq $FileObject) { return }

            # more than one object found
            if ($FileObject -is [System.Array])
            { # call Replace-InFile for every file
                foreach ($FileName in $FileObject) { Replace-InFile -Pattern "$Pattern" -Replacement "$Replacement" -Path "$FileName" -CaseSensitive:$CaseSensitive -Unix:$Unix -Overwrite:$Overwrite -Force:$Force -PreserveDate:$PreserveDate -OEM:$OEM -Encoding $Encoding -Recurse:$Recurse -Quiet:$Quiet }
                # and end function
                return
            }
        }
        $Name = $FileObject.FullName

        if (Test-Path $Name)
        { # if the path exists

            # is it a directory?
            if ($FileObject.PsIsContainer)
            { # recursion?
                # yes -> then call Replace-InFile with child objects
                # no -> do nothing
                if ($Recurse)
                { Get-ChildItem $Name | Replace-InFile -Pattern "$Pattern" -Replacement "$Replacement" -CaseSensitive:$CaseSensitive -Unix:$Unix -Overwrite:$Overwrite -Force:$Force -PreserveDate:$PreserveDate -OEM:$OEM -Encoding $Encoding -Recurse -Quiet:$Quiet }
            }
            else
            { # it is a file -> call working function
                Request-File $Name
            }
        }
    }
}