Parse HOSTS file with PowerShell

In a recent discussion in the PowerShell forum at ScriptingAnswers.com, a member was trying to parse the HOSTS file on a number of desktops. This is a thankless but probably useful task that seems perfectly suited to a scripted solution. What we came up with was a way to turn entries in the HOSTS file to objects in PowerShell. Once we have an object then we can do all sorts of fun things with it.  Here is an enhanced version of the original solution.

#PARSE-HOSTS.PS1
Param([string]$computername=$env:computername,
          [switch]$progress)
          
  function TestPing([string]$address) {
    $wmi = Get-WmiObject -query "SELECT * FROM Win32_PingStatus WHERE Address = '$address'"
    if ($wmi.statuscode -eq 0) {
        $true
    } else {
        $false
    }
  }
    
if ($progress) {
    $activity="Analyzing Hosts file"
    $status=("...on {0}" -f $computername.ToUpper())
    Write-Progress -Activity $activity -status $status
}
 
if (TestPing $computername) {
    #only proceed if computer can be pinged
    $Hosts= "\\$computername\admin$\System32\drivers\etc\hosts"
    if ((Get-Item $Hosts -ea "SilentlyContinue").Exists) {
        #if hosts file is found, then parse it.
        
        if ($progress) {
            $current="Parsing $hosts"
            Write-Progress -Activity $activity -status $status -current $current
        }
        
        #define a regex to return first NON-whitespace character
        [regex]$r="\S"
        #strip out any lines beginning with # and blank lines
        $HostsData = Get-Content $Hosts | where {
         (($r.Match($_)).value -ne "#") -and ($_ -notmatch "^\s+$") -and ($_.Length -gt 0)
         }
        if ($HostsData){
            #only process if something was found in HOSTS
            $HostsData | foreach {
                #created named values
                $_ -match "(?<IP>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+(?<HOSTNAME>\S+)" | Out-Null
                $ip=$matches.ip
                $hostname=$matches.hostname  
                
                #get a comment if defined for the line
                if ($_.contains("#")) {
                    $comment=$_.substring($_.indexof("#")+1)
                }
                else {
                    $comment=$null
                   }
                
                #create a blank object
                $obj = New-Object PSObject
                
                #the computername property could be the name of the computer whose hosts file
                #you are enumerating
                $obj | Add-Member Noteproperty -name "Computername" -Value $computername.ToUpper()
                $obj | Add-Member Noteproperty -name "IP" -Value $ip
                $obj | Add-Member Noteproperty -name "Hostname" -Value $hostname
                $obj | Add-Member Noteproperty -name "Comment" -Value $comment
                
                write $obj
               } #end ForEach
         } #end If $HostsData
         else {
            Write-Host ("{0} has no entries in its HOSTS file." -f $computername.toUpper()) -foreground Magenta
         }
    } #end If Hosts exists
    else {
        Write-Warning "Failed to find $hosts"
      }
 } #end If TestPing
else {
    Write-Warning ("Failed to connect to {0}" -f $computername.ToUpper())
}
 
if ($progress) {
    Write-Progress -Activity $activity -status $status -completed
  }   
#end of script

This is a complete script that you would run, not a simple function. The script takes a computer name as a parameter, although it defaults to the local computer. It opens up the HOSTS file via the ADMIN$ share (so file sharing must be enabled) and parses it. Any entries are converted to objects with the IP address, name and comment as properties. The script also has a -progress parameter. If this is specified, then the script will use the Write-Progress cmdlet to let you know what it is doing.  Very useful when you want to use the script like this:

PS C:\> get-content desktops.txt | foreach { c:\scripts\parse-hosts.ps1 $_ -progress} | Sort Computername | Export-CSV hostsreport.csv

Let’s look at the script in a little more detail because there are some features you might want to use in other scripts.

First off, the script has a nested TestPing function.

function TestPing([string]$address) {
    $wmi = Get-WmiObject -query "SELECT * FROM Win32_PingStatus WHERE Address = '$address'"
    if ($wmi.statuscode -eq 0) {
        $true
    } else {
        $false
    }
  }

Each computer is pinged and processed only if the ping is successful.

if (TestPing $computername) {
    #only proceed if computer can be pinged
    $Hosts= "\\$computername\admin$\System32\drivers\etc\hosts"

The script then checks to see if a hosts file even exists.

if ((Get-Item $Hosts -ea "SilentlyContinue").Exists) {
    #if hosts file is found, then parse it.

I have to use the -ErrorAction common parameter with Get-Item to suppress an error message if the file doesn’t exist. Weird, but trust me. Now the fun part.

I needed a way to skip any lines that are comments or blank. To solve that I turned to regular expressions.

#define a regex to return first NON-whitespace character
        [regex]$r="\S"
        #strip out any lines beginning with # and blank lines
        $HostsData = Get-Content $Hosts | where {
         (($r.Match($_)).value -ne "#") -and ($_ -notmatch "^\s+$") -and ($_.Length -gt 0)
         }

First I created a regular expression object with the pattern “\S” which indicates any non-whitespace character. So when I pipe the contents of HOSTS to Where-Object, one of the things I’m looking for is a match on the first non-whitespace character. I use the Match() method of the REGEX object, $r. If there is a match, the resulting object’s Value property will indicate what it matched. If it doesn’t equal the comment character “#”, I’m good. The second part of the Where statement is to not match on another regular expression pattern, “^\s+$”. This pattern is looking for a line that has nothing but whitespace like spaces and tabs. This is different from the last check which is simply looking for lines that have a length greater than 0. This eliminates completely blank lines.

After all of that, assuming anything was returned what I should have is a line like:

172.16.10.1     mydomain.com   #test domain

What I need to do is parse that using regular expressions to extract the IP, hostname and comment.

$HostsData | foreach {
    #created named values
    $_ -match "(?<IP>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+(?<HOSTNAME>\S+)" | Out-Null
    $ip=$matches.ip
    $hostname=$matches.hostname  

I know that the first part will be an IPv4 address so I’ll use a simple regular expression (d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}). After that there can be any number of whitespace characters including spaces and tabs (\s+) followed by the hostname which is a series of non-whitespace characters (\S+).  I’m not worried about the comment character because the regular expression will not match on the space after the domain name. But what is that <IP> and <HOSTNAME> stuff?

In my regular expression pattern I am defining named values for the matches. Recall that PowerShell will automatically create $matches with the results of the regular expression comparison. I’m simply saying assign the IP address pattern to IP and the name pattern to HOSTNAME. This allows me to easily retrieve the matching values which I assign to variables.

If the line contains a comment it will be at the end of the line.

if ($_.contains("#")) {
    $comment=$_.substring($_.indexof("#")+1)
}
else {
    $comment=$null
   }

All I need to do is find the # character’s position in the line, and use the Substring() method to retrieve the last part of the line. That’s basically it. All that remains is to create a custom object, define it’s properties and write it to the pipeline.

#create a blank object
$obj = New-Object PSObject
 
#the computername property could be the name of the computer whose hosts file
#you are enumerating
$obj | Add-Member Noteproperty -name "Computername" -Value $computername.ToUpper()
$obj | Add-Member Noteproperty -name "IP" -Value $ip
$obj | Add-Member Noteproperty -name "Hostname" -Value $hostname
$obj | Add-Member Noteproperty -name "Comment" -Value $comment
 
write $obj

The rest of the script is self-explanatory error handling.

I tried to take a lot of things into account. One thing that this script won’t do is return any entries using IPv6 addresses, but since HOSTS is a legacy file I’m trusting that won’t be an issue.

Even if you don’t need to monitor HOSTS file on your network, I trust you found some useful techniques for your own PowerShell projects.

Download Parse-Hosts.ps1 here.