PowerShell Problem Solver: Create Numbered Output Lists with PowerShell

I came across an interesting question the other day in a PowerShell forum. The poster wanted to include numbers for each line of output, which might look like this:

1.	Stopped  AdobeFlashPlaye... Adobe Flash Player Update Service
2.	Stopped  AeLookupSvc        Application Experience
3.	Stopped  ALG                Application Layer Gateway Service


I wasn’t sure at first why you would want to do this, as this feels like text parsing and not taking advantage of PowerShell’s object nature. But I started playing along. Here’s one approach.

get-service | foreach -Begin {$i=0} -Process {
$i++
"{0:D2}. {1} [{2}]" -f $i,$_.Name,$_.Status
}

Which gives you this:

A numbered output list created with Windows PowerShell. (Image Credit: Jeff Hicks)
A numbered output list created with Windows PowerShell. (Image Credit: Jeff Hicks)

That’s not bad I suppose if all you want to do is look at the list or save it to a text file because all you have is text. Although, you could use it to build an interactive console menu.

get-service | where {$_.status -eq 'running'} | foreach -Begin {$i=0} -Process {
$i++
"{0}. {1}" -f $i,$_.Name
} -outvariable menu
$r = Read-Host "Select a service to restart by number"
Write-Host "Restarting $($menu[$r-1])" -ForegroundColor Green
Restart-Service $menu[$r-1].Split()[1] -PassThru -force

I’m saving the results to $menu, so that I can access them later in the example.

An interactive console in Windows PowerShell. (Image Credit: Jeff Hicks)
An interactive console in Windows PowerShell. (Image Credit: Jeff Hicks)

That’s sorta cool. But because this is PowerShell, we should simply add a property.

$global:i=0
get-service | Select @{Name="Item#";Expression={$global:i++;$global:i}},Name,Displayname,Status

031215 1334 NumberedPow3
I am referencing the variable in the global scope because otherwise $i would be new for each object and everything would have a value of 1 in the Expression scriptblock. If I were to pipe this to Get-Member, I would see a new property called Item#. Given that, here’s my revised menu code:

$global:i=0
get-service | where {$_.status -eq 'running'} |
Select @{Name="Item";Expression={$global:i++;$global:i}},
Name -OutVariable menu | format-table -AutoSize
$r = Read-Host "Select a service to restart by number"
$svc = $menu | where {$_.item -eq $r}
Write-Host "Restarting $($svc.name)" -ForegroundColor Green
Restart-Service $svc.name -PassThru –force

Our revised output. (Image Credit: Jeff Hicks)
Our revised output. (Image Credit: Jeff Hicks)

This is all fun and proof of concept stuff. There’s no error handling, and if you run it again without setting $global:i back to 0, then your numbering will be off.

Everything I’ve shown you thus far is an ad-hoc approach, although certainly it lends itself to scripting. Another option would be to add your own type and formatting extensions. Let’s look at the formatting options. To create a custom format, you’ll need to create a .ps1xml. One way to get started is to export the current settings. Since I’m working with service objects, I can use an expression like this:

Get-FormatData -TypeName System.ServiceProcess.ServiceController | Export-FormatData -Path c:\scripts\myservice.ps1xml -IncludeScriptBlock -force

You can also open up $pshome\DotNetTypes.format.ps1xml in the ISE or copy and paste the relevant section into a new file. Now, you can edit and create your own formatting file. Here’s what I came up with:

<?xml version="1.0" encoding="utf-8"?>
<Configuration>
    <ViewDefinitions>
        <View>
        <Name>numbered</Name>
        <ViewSelectedBy>
         <TypeName>System.ServiceProcess.ServiceController</TypeName>
            </ViewSelectedBy>
            <TableControl>
                <TableHeaders>
                    <TableColumnHeader>
                        <Label>Item</Label>
                        <Width>5</Width>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Width>8</Width>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Width>18</Width>
                    </TableColumnHeader>
                    <TableColumnHeader>
                        <Width>38</Width>
                    </TableColumnHeader>
                </TableHeaders>
                <TableRowEntries>
                    <TableRowEntry>
                        <TableColumnItems>
                        <TableColumnItem>
                                <Scriptblock>$global:i++; $global:i</Scriptblock>
                            </TableColumnItem>
                            <TableColumnItem>
                                <PropertyName>Status</PropertyName>
                            </TableColumnItem>
                            <TableColumnItem>
                                <PropertyName>Name</PropertyName>
                            </TableColumnItem>
                            <TableColumnItem>
                                <PropertyName>DisplayName</PropertyName>
                            </TableColumnItem>
                        </TableColumnItems>
                    </TableRowEntry>
                </TableRowEntries>
            </TableControl>
        </View>
        </ViewDefinitions>
</Configuration>

I changed the Name setting because I will use this to reference this view. What I’m creating is in addition to the normal formatting. Next, I’ve created a new heading for the item number.

<TableColumnHeader>
     <Label>Item</Label>
     <Width>5</Width>
</TableColumnHeader>

Naturally I need to provide a value.

<TableColumnItem>
    <Scriptblock>$global:i++; $global:i</Scriptblock>
</TableColumnItem>

There is a concession here you have to accept. There is no easy way in the XML file to initialize $global:I every time you format the results.

The first time everything will look fine, but unless you reset $i it will keep increasing every time you format the output with this custom view. Once you have it complete, you can load it into your session and use it.

update-formatdata -AppendPath C:\scripts\myservice.ps1xml
get-service | format-table -View numbered

The get-service cmdlet in Windows PowerShell. (Image Credit: Jeff Hicks)
The get-service cmdlet in Windows PowerShell. (Image Credit: Jeff Hicks)

Because this is formatted output, it is pretty to look at or save to a text file, but that’s the limits of its usefulness. If you want the Item number to be a part of the object, then you’ll need a custom type extension. This is actually pretty easy since we are adding a single property.

Update-TypeData -TypeName System.ServiceProcess.ServiceController -MemberType ScriptProperty -MemberName Item -Value {$global:i++; $global:i} -force
get-service | select Item,name,displayname,status

However, there is the same caveat about $global:i.
031215 1334 NumberedPow6
Although if you were using this in a script or function you could handle initializing the variable and cleaning up at the end.
Finally, you could take all of this one more step and create your own custom object with its own type and formatting directives, but I’ll leave that fun exercise to you.