Functions/Set-HostsEntry.ps1

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

function Set-HostsEntry
{
    <#
    .SYNOPSIS
    Sets a hosts entry in a hosts file.
     
    .DESCRIPTION
    Sets the IP address for a given hostname. If the hostname doesn't exist in the hosts file, appends a new entry to the end. If the hostname does exist, its IP address gets updated. If you supply a description, it is appended to the line as a comment.
     
    If any duplicate hosts entries are found, they are commented out; Windows uses the first duplicate entry.
     
    This function scans the entire hosts file. If you have a large hosts file, and are updating multiple entries, this function will be slow.
     
    You can operate on a custom hosts file, too. Pass its path with the `Path` parameter.
 
    Sometimes the system's hosts file is in use and locked when you try to update it. The `Set-HostsEntry` function tries 10 times to set a hosts entry before giving up and writing an error. It waits a random amount of time (from 0 to 1000 milliseconds) between each attempt.
     
    .EXAMPLE
    Set-HostsEntry -IPAddress 10.2.3.4 -HostName 'myserver' -Description "myserver's IP address"
     
    If your hosts file contains the following:
     
        127.0.0.1 localhost
         
    After running this command, it will contain the following:
     
        127.0.0.1 localhost
        10.2.3.4 myserver # myserver's IP address
 
    .EXAMPLE
    Set-HostsEntry -IPAddress 10.5.6.7 -HostName 'myserver'
     
    If your hosts file contains the following:
     
        127.0.0.1 localhost
        10.2.3.4 myserver # myserver's IP address
     
    After running this command, it will contain the following:
     
        127.0.0.1 localhost
        10.5.6.7 myserver
     
    #>

    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter(Mandatory=$true)]
        [Net.IPAddress]
        # The IP address for the hosts entry.
        $IPAddress,

        [Parameter(Mandatory=$true)]
        [string]
        # The hostname for the hosts entry.
        $HostName,

        [string]
        # An optional description of the hosts entry.
        $Description,

        [string]
        # The path to the hosts file where the entry should be set. Defaults to the local computer's hosts file.
        $Path = (Get-PathToHostsFile)
    )

    Set-StrictMode -Version 'Latest'

    Use-CallerPreference -Cmdlet $PSCmdlet -Session $ExecutionContext.SessionState
 
    $matchPattern = '^(?<IP>[0-9a-f.:]+)\s+(?<HostName>[^\s#]+)(?<Tail>.*)$'  
    $lineFormat = "{0,-16}{1}{2}"
    
    if(-not (Test-Path $Path))
    {
        Write-Warning "Creating hosts file at: $Path"
        New-Item $Path -ItemType File
    }
     
    [string[]]$lines = @()
    
    $succeeded = $false
    for( $idx = 0; $idx -lt 20; ++$idx )
    {
        $exception = $false
        $getHostsEntryError = @()
        try
        {
            $lines = Get-Content -Path $Path -ErrorAction SilentlyContinue -ErrorVariable 'getHostsEntryError'
            $succeeded = $true
        }
        catch
        {
            if( $Global:Error.Count -gt 0 )
            {
                $Global:Error.RemoveAt(0)
            }
            $exception = $true
        }

        if( $exception -or $getHostsEntryError )
        {
            $succeeded = $false
            Write-Debug -Message ('Failed to get hosts entries from ''{0}''. Retrying in 500 milliseconds.' -f $Path)
            Start-Sleep -Milliseconds 500
        }

        if( $succeeded )
        {
            break
        }
    }

    if( -not $succeeded )
    {
        Write-Error ('Failed to read hosts file ''{0}''.' -f $Path)
        return
    }

    $outLines = New-Object 'Collections.ArrayList'
    $found = $false
    $lineNum = 0
    $updateHostsFile = $false
     
    foreach($line in $lines)
    {
        $lineNum += 1
        
        if($line.Trim().StartsWith("#") -or ($line.Trim() -eq '') )
        {
            [void] $outlines.Add($line)
        }
        elseif($line -match $matchPattern)
        {
            $ip = $matches["IP"]
            $hn = $matches["HostName"]
            $tail = $matches["Tail"].Trim()
            if( $HostName -eq $hn )
            {
                if($found)
                {
                    #this is a duplicate so, let's comment it out
                    [void] $outlines.Add("#$line")
                    $updateHostsFile = $true
                    continue
                }
                $ip = $IPAddress
                $tail = if( $Description ) { "`t# $Description" } else { '' }
                $found = $true   
            }
            else
            {
                $tail = "`t{0}" -f $tail
            }
           
            if( $tail.Trim() -eq "#" )
            {
                $tail = ""
            }

            $outline = $lineformat -f $ip, $hn, $tail
            $outline = $outline.Trim()
            if( $outline -ne $line )
            {
                $updateHostsFile = $true
            }

            [void] $outlines.Add($outline)
                
        }
        else
        {
            Write-Warning ("Hosts file {0}: line {1}: invalid entry: {2}" -f $Path,$lineNum,$line)
            $outlines.Add( ('# {0}' -f $line) )
        }

    }
     
    if(-not $found)
    {
       #add a new entry
       $tail = "`t# $Description"
       if($tail.Trim() -eq "#")
       {
           $tail = ""
       }
           
       $outline = $lineformat -f $IPAddress, $HostName, $tail
       $outline = $outline.Trim()
       [void] $outlines.Add($outline)
       $updateHostsFile = $true
    }

    if( -not $updateHostsFile )
    {
        return
    }
    
    Write-Verbose -Message ('[HOSTS] [{0}] {1,-15} {2}' -f $Path,$IPAddress,$HostName)
    $succeeded = $false
    $maxTries = 10
    $rng = New-Object 'Random'
    for( $idx = 0; $idx -lt $maxTries; ++$idx )
    {
        $exception = $false
        $setHostsEntryError = @()
        try
        {
            $outlines | Set-Content -Path $Path -ErrorAction SilentlyContinue -ErrorVariable 'setHostsEntryError'
            $succeeded = $true
        }
        catch
        {
            if( $Global:Error.Count -gt 0 )
            {
                $Global:Error.RemoveAt(0)
            }
            $exception = $true
        }

        if( $exception -or $setHostsEntryError )
        {
            $succeeded = $false
            $timeout = $rng.Next(0,1000)
            Write-Debug -Message ('Failed to set hosts entry ''{0} {1}'' in ''{2}'': waiting {3} milliseconds to try again.' -f $HostName,$IPAddress,$Path,$timeout)
            Start-Sleep -Milliseconds $timeout
        }

        if( $succeeded )
        {
            break
        }
    }

    if( -not $succeeded )
    {
        Write-Error ('Failed to set hosts entry ''{0} {1}'' in ''{2}'': looks like the hosts file is in use.' -f $HostName,$IPAddress,$Path)
    }
}