Weekend Scripter: Simplify to Troubleshoot PowerShell Script

Doctor Scripto

Summary: Microsoft Scripting Guy, Ed Wilson, helps an old friend fix his script.

Microsoft Scripting Guy, Ed Wilson, is here. I am a big believer in simplicity. I enjoy simple things: a nice cup of hot tea, reading a good book, watching a sunset on the beach, taking a train ride through the Alps, or spending an evening conversing with friends. Simplicity is one of the things I like about Windows PowerShell. It can take a complex operation and make it simple. Desire State Configuration (DSC) is one such tool.

When I see a Windows PowerShell script that is complex, I naturally begin to look at it to see if there are areas of improvement. Most of the time, there is. Occasionally and unfortunately, the answer is, "Nope." This is because Windows PowerShell is ever evolving, and it continues to improve. We know there are things that we still need to simplify. To see what we are currently working on, check out Windows Management Framework 5.0 Preview.

At times, a source of the complexity is the approach to solving the problem. As I stated in my book Windows PowerShell Best Practices, when I begin with a technology, I can back myself into a corner. Instead, I begin with a desired outcome, and then I try to see the best way to get there. The following examples demonstrate the difference.

     A. “How can I use WMI to find the IP address of my active network adapter?” (This assumes that I will need to use WMI to find the answer.)

As opposed to:

     B. “I need to find the IP address of my active network adapter. How can I do this?” (This approach states the task, and then searches for the best solution.)

Practical example

I received an email the other day, and the writer said he was having problems outputting the information he was getting from WMI about his network adapters to a .csv file. Here is his script:

##Code##

# truncated function.

# provide variables

$this = @() # initialise results table as an array.

$ip = "mylaptop.mydomain.com" # some box with –gt 1 Nic. 

foreach ($connected_nic in ($Adapter = Get-WmiObject -computer $ip win32_networkadapter -filter "NetConnectionStatus='2'" | where {$_.PNPDeviceID -notmatch "1394"})) { # Find electrically connected adapters, which are not firewire.

# Write-host -ForegroundColor Green $Connected_nic

if ($cfg=($dns=(Get-WmiObject -ComputerName $ip Win32_NetworkAdapterConfiguration -filter "Index = '$($connected_nic.Index)'")).IPaddress -like "192.168.*") { # test each connected adapter to see if it's IP = 192.168.X.X

$ips = $dns | select –expandproperty Ipaddress

# $dns # check input is as expected

$results = [ordered] @{

"Netbios Name" = $dns.DNSHostname

"IPv4 Address" = $ips

"Subnet Mask" = [String]$dns.IpSubnet

"Default Gateway" = [String]$dns.DefaultIPGateway

"Primary DNS Server" = $dns.DNSServerSearchOrder[0]

"Secondary DNS Server" = $dns.DNSServerSearchOrder[1]

"MAC Address" = $dns.MACaddress

}

$dnsout = New-Object PSObject -Property $results # Create a new row for the report from our hash table.

$this += $dnsout # Add this row to the main report.

} # Configurations of interest test.

} # End connected adapter test

$this

A few changes to the script

One of the first changes I make to a script, if I can, is I change Get-WmiObject to Get-CimInstance. Since Windows PowerShell 3.0, I can use Get-CimInstance. It is faster and more robust, and it permits lots of cool things for retrieving data (such as using Cim-Sessions). So this change is always a no-brainer for me.

The next thing I noticed is that he makes a WMI query, and then he pipes the results to Where-Object to do additional filtering. Here is that portion of the script:

$Adapter = Get-WmiObject -computer $ip win32_networkadapter -filter "NetConnectionStatus='2'" | where {$_.PNPDeviceID -notmatch "1394"}

I imagine the reason he did this is because he does not know how to make a compound where clause in a WMI query. Here is my revision of this portion of the script:

Get-CimInstance Win32_NetworkAdapter -Filter "NetConnectionStatus = 2 AND PNPDeviceID != 1394"

The next thing he does is look to see if an IP address is similar to a particular range. He stores several different levels in different variables, and then he uses the Select-Object cmdlet to expand the IPaddress property. Here is his command:

if ($cfg=($dns=(Get-WmiObject -ComputerName $ip Win32_NetworkAdapterConfiguration -filter "Index = '$($connected_nic.Index)'")).IPaddress -like "192.168.*") { # test each connected adapter to see if it's IP = 192.168.X.X

$ips = $dns | select –expandproperty Ipaddress

# $dns # check input is as expected

This is part of the "Why are you doing this?" type of question. Part of his problem was outputting to a .csv file. If you output to a .csv file, and you have objects stored inside other objects, you end up with properties that say System.String[] or System.Object[], instead of the actual information you are seeking. Here is a sample of that output:

Image of command output

Often, I do not have to have a .csv file. This is because most applications accept XML input. If I have XML, it knows how to handle embedded objects, and they expand as different nodes. I could even output to XML, and then read the XML to create a .csv file if I had to have one. It might be simpler than creating the objects in code.

Because I decided that I did not have to output to XML, I was able to simplify this portion of the script to another single line (% is an alias for the Foreach-Object cmdlet):

% {Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "Index = $($_.index)"

Now he builds up his output object. First he creates an ordered list (this is how I knew that I could use Get-CimInstance). In the ordered list, he chooses the properties he wants to keep. After he does this, he then creates another PSObject, and then he uses += to add the objects together. Finally, he outputs the objects, and as he told me in the email, he was going to write the output to a .csv file. Here is his script:

$results = [ordered] @{ 

"Netbios Name" = $dns.DNSHostname

"IPv4 Address" = $ips

"Subnet Mask" = [String]$dns.IpSubnet

"Default Gateway" = [String]$dns.DefaultIPGateway

"Primary DNS Server" = $dns.DNSServerSearchOrder[0]

"Secondary DNS Server" = $dns.DNSServerSearchOrder[1]

"MAC Address" = $dns.MACaddress

}

$dnsout = New-Object PSObject -Property $results # Create a new row for the report from our hash table.

$this += $dnsout # Add this row to the main report. 

} # Configurations of interest test.

} # End connected adapter test

$this

He did not need to do all of that. I created a custom object, but I used my preferred method for doing this—the Select-Object cmdlet. I choose the properties he was interested in obtaining, and then I output the objects to the .xml file. Here is my command:

Select DNSHostname, IPSubnet, defaultIPGateWay, DNSServerSearchOrder, MacAddress
} | Export-Clixml -Depth 10 -Path c:\fso\ipreport.xml

That is it. Here is my complete script. It is one logical line of code:

Get-CimInstance Win32_NetworkAdapter -Filter "NetConnectionStatus = 2 AND PNPDeviceID != 1394" |

% {Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "Index = $($_.index)" |

Select DNSHostname, IPSubnet, defaultIPGateWay, DNSServerSearchOrder, MacAddress} |

Export-Clixml -Depth 10 -Path c:\fso\ipreport.xml

In addition to being shorter and easier to understand, my revised script works. If I had stayed with the limitation to output to a .csv file, I would have doomed myself to writing something similar to what the original query wanted to do. I could shorten it a bit, but it still would be rather complex. However, by freeing myself from assumptions as to what the solution would be, I was able to come up with a better, easier solution.

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 

0 comments

Discussion is closed.

Feedback usabilla icon