Summary: Microsoft PowerShell MVP, Tobias Weltner, talks about Windows PowerShell and Excel interaction. Microsoft Scripting Guy, Ed Wilson, is here. Let’s welcome back guest blogger, Tobias Weltner… Microsoft Excel is a great tool to share Windows PowerShell data. However, the scripting tactics have changed from the good old VBScript times. Let’s take a look at how Windows PowerShell can output data to Excel fast, safe, and reliably.
Out-GridView: Built-in “mini-Excel”
Before I touch Excel, let’s first look at its little cousin, which ships with Windows PowerShell: Out-GridView. It’s trivial to use: simply pipe anything to Out-GridView, and it will happily display the content in a mini-spreadsheet:
Get-Process | Where-Object { $_.MainWindowTitle } | Out-GridView You can even use Out-GridView as a generic selection dialog box. Add the parameter –passThru (added in PowerShell 3.0). This parameter adds two new buttons to the lower right area of the grid view, and when you select something, it will be returned to you. Curious? This piece of script pipes all top-level applications to the grid view, and when you select one and click OK, it will be immediately deleted:
Get-Process |
Where-Object MainWindowTitle |
Out-GridView -Title ‘Select Program To Kill’ -PassThru |
Stop-Process That’s admittedly a bit rude (and dangerous, too, because you will lose all unsaved data). So here is a gentleman’s version that will actually ask if there is unsaved data. It also allows the program to clean up its resources before it closes.
Get-Process |
Where-Object MainWindowTitle |
Out-GridView -Title ‘Select Program To Kill’ -PassThru |
ForEach-Object { $_.CloseMainWindow() }
Excel: Out-GridView on steroids
So what about Excel? Why use it at all? Well, there are things that Out-GridView cannot do. You cannot save the data, and you cannot hand it over to someone else. And of course, you cannot use the many sophisticated formatting options that are found in Microsoft Excel spreadsheets. Out-GridView can show a maximum of 30 columns. So if an object has more than 30 properties, Out-GridView silently suppresses the rest. I’d call this a bug, but maybe it’s a feature, too. Anyhow, here is a script to prove the point:
1..100 |
ForEach-Object { $hash = [Ordered]@{} }{$hash.”Column$_” = $_}{ New-Object PSObject -Property $hash} | Out-GridView This creates an object with 100 properties. Out-GridView only displays the first 30. Time for Excel…the real thing. Wouldn’t it be nice to be able to do this:
Get-Process | Where-Object { $_.MainWindowTitle } | Out-Excel Or, for that matter, this (and finally get all object properties and not just the first 30):
1..100 |
ForEach-Object { $hash = [Ordered]@{} }{$hash.”Column$_” = $_}{ New-Object PSObject -Property $hash} |
Out-Excel Well, you can. Out-Excel actually is a ridiculously simple Windows PowerShell function:
function Out-Excel
{
param($Path = “$env:temp$(Get-Date -Format yyyyMMddHHmmss).csv”)
$input | Export-CSV -Path $Path -UseCulture -Encoding UTF8 -NoTypeInformation
Invoke-Item -Path $Path
}
Get-Process | Where-Object { $_.MainWindowTitle } | Out-Excel You get a report that includes all the technical data from all of your running main applications. With all of the object properties—nothing is left out. Basically, you can pipe anything into Out-Excel. It works pretty much like Out-GridView (except that you cannot turn Excel into a selection dialog, of course). $input is an automatic Windows PowerShell variable. It delivers all data that is piped from the upstream cmdlet, and this data is then written to a CSV file and opened with Excel. Because Excel always shows all properties, you may want to use Select-Object and select only the properties you really care about:
Get-Process |
Where-Object { $_.MainWindowTitle } |
Select-Object -Property Name, Company, Description, CPU |
Out-Excel
Writing directly to Excel
Going the CSV route is fast and robust, but cannot use color in cells or apply any other formatting. That’s why some years ago in VBScript, people started to use a COM object to access the Excel object model. Although this works in Windows PowerShell, too, it is not very reliable anymore. Excel lives in the old COM world, and Windows PowerShell lives in the modern .NET world. The underlying .NET Framework needs extensive marshalling to access the COM objects (which makes it very slow). In some scenarios, it fails altogether, for example, when type libraries do not fit together because operating system and office binaries use different locales. Even if everything works, and you waited for the code to finish, Excel will continue to linger in memory because of leaking handles. It’s just too complicated. Here is sample code that does access Excel’s object model directly. You have been warned. Don’t be mad at me if the code fails or is really, really slow:
$excel = New-Object -ComObject Excel.Application
$excel.Visible = $true
$workbook = $excel.Workbooks.Add()
$sheet = $workbook.ActiveSheet
$counter = 0
Get-Service |
ForEach-Object {
$counter++
$sheet.cells.Item($counter,1) = $_.Name
$sheet.cells.Item($counter,2) = $_.DisplayName
$sheet.cells.Item($counter,3) = $_.Status
} Here is the fast and clean way that is used by Windows PowerShell to create the same report:
Get-Service |
Select-Object -Property Name, DisplayName, Status |
Export-Csv -Path $env:tempreport.csv -Encoding UTF8 -UseCulture -NoTypeInformation
Invoke-Item -Path $env:tempreport.csv Actually, Windows PowerShell is not even touching the internals of Excel; but instead, it is producing CSV files that can then be opened by Excel.
Formatting data with colors
Unfortunately, importing CSV data is a black and white world. You cannot use color in cells or format the spreadsheet otherwise. Well actually, you can. This will create the very same report of services. This time, running services will show in green and stopped services in red.
# write HTML intro code:
$begin =
{
‘<table>’
‘<tr>’
‘<th>DisplayName</th><th>Status</th><th>Required</th><th>Dependent</th>’
‘</tr>’
}
# this is executed for each data object:
$process =
{
if ($_.Status -eq ‘Running’)
{
$style = ‘<td style=”color:green; ; font-family:Segoe UI; font-size:14pt”>’
}
else
{
$style = ‘<td style=”color:red; font-family:Segoe UI; font-size:14pt”>’
}
‘<tr>’
‘{0}{1}</td><td>{2}</td><td>{3}</td><td>{4}</td>’ -f $style, $_.DisplayName, $_.Status, ($_.RequiredServices -join ‘,’), ($_.DependentServices -join ‘,’)
‘</tr>’
}
# finish HTML fragment:
$end =
{
‘</table>’
}
$Path = “$env:temptempfile.html”
# get all services and create custom HTML report:
Get-Service |
ForEach-Object -Begin $begin -Process $process -End $end |
Set-Content -Path $Path -Encoding UTF8
# feed HTML report into Excel:
Start-Process -FilePath ‘C:Program Files*Microsoft OfficeOffice*EXCEL.EXE’ -ArgumentList $Path The trick is to create an HTML report that gives you all the formatting opportunities you need. To create a Excel spreadsheet with color, the HTML file needs to be fed directly into excel.exe. Note how Windows PowerShell automatically identifies the Excel version you use. Start-Process accepts a file path that can contain wildcard characters. By replacing version numbers in the path, Windows PowerShell can launch any Excel version that happens to be installed. If you wanted to know that path, too, then simply ask Resolve-Path:
Resolve-Path -Path ‘C:Program Files*Microsoft OfficeOffice*EXCEL.EXE’ |
Select-Object -ExpandProperty Path -First 1
Turning arrays into string
Take a look at the Excel spreadsheet that uses color again. Look at the columns RequiredServices and DependentServices. They are just fine—which might surprise you. Because typically, when Windows PowerShell exports arrays of information, they do not display well in Excel. Check this out, for example… The next piece of script outputs the 10 newest error events from your System event log to Excel. When you look at the data, the columns Data and ReplacementStrings contain unreadable stuff. To make array content display correctly in Excel, arrays need to be converted to strings. Here’s sample script that illustrates how easily that can be done:
$Path = “$env:temp$(Get-Date -Format yyyyMMddHHmmss)report.csv”
Get-EventLog -LogName System -EntryType Error -Newest 10 |
Select-Object -Property * |
ForEach-Object {
$_.ReplacementStrings = $_.ReplacementStrings -join ‘,’
$_.Data = $_.Data -join ‘,’
$_
} |
Export-CSV -Path $Path -UseCulture -Encoding UTF8 -NoTypeInformation
Invoke-Item -Path $Path When you run the script, the columns now reveal their content. And here is why… The script first piped the data to Select-Object –Property *. Typically, you would use Select-Object to select the columns you want to have in your report. Select-Object will also do another thing: It copies the data into a new object, and this new object is yours. You can do whatever you want with it. That’s the prerequisite for the subsequent Foreach-Object loop. Inside of it, you can fine tune, change, or revise the data in any way you wish. So the script takes the properties that contain arrays, ReplacementStrings, and Data, and converts these arrays to strings by using the –join operator. That’s it. Just don’t forget to put back the $_ variable inside Foreach-Object so that the downstream cmdlets can receive it. Thank you, Tobias. Awesome script as usual. 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
Regarding the 30 column limit it seems to be fine in PS5.1. I ran the script and saw 100 columns.