Last Update: Sep 04, 2024 | Published: Apr 15, 2015
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:
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.
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
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
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:scriptsmyservice.ps1xml -IncludeScriptBlock -force
You can also open up $pshomeDotNetTypes.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:scriptsmyservice.ps1xml
get-service | format-table -View numbered
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.
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.