My Solution to Iron Scripter 2019 Preparatory Challenge 1

Anyone who has competed in the scripting games before knows that I'm always looking for a challenge when it comes to writing PowerShell code. While the scripting games haven't been held in the last several years, they've somewhat been replaced by the Iron Scripter competition at the PowerShell + DevOps Global Summit and 2019 is shaping up to be no different. Think you've got skills? Bring them on! and Get-Involved.

Whether you've previously competed or not, you should definitely head over to IronScripter.us to see what it's all about. I'm not sure if there will be more than one practice scenario or not since I'm in no way officially affiliated with the competition other than being a participant. This particular scenario deals with the Get-Counter cmdlet which I used in my chapter on Finding Performance Bottlenecks with PowerShell in The PowerShell Conference Book (you can read my chapter in its entirety by downloading the free sample).

The challenge is to create a PowerShell function that Get-Counter can be piped to which produces friendlier output and returns specific properties. I used a regular expression in my chapter from the previously referenced book. I simply reused it for this portion of the challenge.

 1function Format-MrCounter {
 2
 3    [CmdletBinding()]
 4    param (
 5        [Parameter(Mandatory,
 6                    ValueFromPipeline)]
 7        [Microsoft.PowerShell.Commands.GetCounter.PerformanceCounterSampleSet]$CounterSampleSet
 8    )
 9
10    foreach ($Counter in $CounterSampleSet.CounterSamples){
11
12        $ComputerName = $Counter.Path -replace '^\\\\|\\.*$'
13
14        [pscustomobject]@{
15            DateTime = $Counter.Timestamp
16            ComputerName = $ComputerName
17            CounterSet = $Counter.Path -replace "^\\\\$ComputerName\\|\\.*$"
18            Counter = $Counter.Path -replace '^.*\\'
19            Value = $Counter.CookedValue
20        }
21
22    }
23
24}

ironscripter2019prep1a.jpg

I try to avoid the ComputerName parameter of commands when targeting remote systems because they're usually implemented via older DCOM protocols which are typically blocked by firewalls on modern systems. I simply use Invoke-Command to run commands locally on remote systems via PowerShell remoting since WinRM is much more firewall friendly. Because of this design philosophy, I never tested my regular expressions when targeting remote systems with Get-Counter's ComputerName parameter. Well, they don't work because of an extra slash that exists after the computer name in the results when targeting a remote system.

1(Get-Counter).CounterSamples.Path
2(Get-Counter -ComputerName DC01).CounterSamples.Path

ironscripter2019prep2a.jpg

While I could rework the regex to take that into account, I've been there and done that so I thought I would take a different approach and eliminate the regular expressions altogether. While I'm at it, I'll also tackle the advanced portion of the challenge which wants a custom type and view defined to return four specific properties by default in a table.

I'm writing my code in a way that requires PowerShell version 3.0 or higher and since Get-Counter only exists in Windows PowerShell, I'll define both of those requirements in a requires statement. I'm using an approved verb along with a singular noun and the command name is in Pascal case. This is followed by comment based help. CmdletBinding is declared which makes it an advanced function, the output type is declared, and then the single InputObject parameter which only accepts the type of object that Get-Counter produces. The parameter is mandatory and accepts input via the pipeline by value (what I call by type).

 1#Requires -Version 3.0 -PSEdition Desktop
 2function Format-MrCounter {
 3
 4<#
 5.SYNOPSIS
 6    Formats the output of the Get-Counter cmdlet into a friendlier format.
 7
 8.DESCRIPTION
 9    The Format-MrCounter function accepts input from the Get-Counter function
10    and formats it into a much more useable and more object oriented format.
11
12.PARAMETER InputObject
13    Accepts the output of the results from the Get-Counter cmdlet. It expects a
14    Microsoft.PowerShell.Commands.GetCounter.PerformanceCounterSampleSet object type.
15
16.EXAMPLE
17    Get-Counter | Format-MrCounter
18
19.EXAMPLE
20    Get-Counter -ComputerName Server01, Server02 | Format-MrCounter
21
22.INPUTS
23    Microsoft.PowerShell.Commands.GetCounter.PerformanceCounterSampleSet
24
25.OUTPUTS
26    Mr.CounterInfo
27
28.NOTES
29    Author:  Mike F Robbins
30    Website: http://mikefrobbins.com
31    Twitter: @mikefrobbins
32#>
33
34    [CmdletBinding()]
35    [OutputType('Mr.CounterInfo')]
36    param (
37        [Parameter(Mandatory,
38                   ValueFromPipeline)]
39        [Microsoft.PowerShell.Commands.GetCounter.PerformanceCounterSampleSet[]]$InputObject
40    )
41
42    PROCESS {
43
44        foreach ($Counter in $InputObject.CounterSamples){
45
46            $CounterInfo = $Counter.Path.Split('\\', [System.StringSplitOptions]::RemoveEmptyEntries)
47
48            for ($i = 0; $i -lt $CounterInfo.Count; $i += 3){
49                [pscustomobject]@{
50                    DateTime = $Counter.Timestamp
51                    ComputerName = $CounterInfo[$i]
52                    CounterSet = $CounterInfo[$i+1]
53                    Counter = $CounterInfo[$i+2]
54                    Value = $Counter.CookedValue
55                    PSTypeName = 'Mr.CounterInfo'
56                } |
57                Add-Member -MemberType MemberSet -Name PSStandardMembers -Value (
58                    [System.Management.Automation.PSMemberInfo[]]@(
59                        New-Object -TypeName System.Management.Automation.PSPropertySet(
60                            'DefaultDisplayPropertySet',[string[]]@(
61                                'ComputerName', 'CounterSet', 'Counter', 'Value'
62                            )
63                        )
64                    )
65                ) -PassThru
66            }
67
68        }
69
70    }
71
72}

I'm splitting the Path property on backslashes, but since there are some double backslashes, I'm using the Split method instead of the Split operator so I can take advantage of the .NET option to remove empty entries. A For loop is used to iterate through the entries. PSCustomObject is used to create a custom object and a type name is specified within the PSCustomObject to define a custom object type. Finally, since only four properties are required in the default output and four or fewer properties are displayed in a table by default, a Default Display Property Set is defined instead of going through the trouble of creating a module, manifest, and custom format file in XML.

By default, the output is the four properties required by the advanced challenge specifications. All of the properties can always be viewed in either a list (or table) when piping to either Format-List, Format-Table, or Select-Object and specifying the asterisk wildcard (*) as the value for the Property parameter.

1Get-Counter -ComputerName DC01 | Format-MrCounter
2Get-Counter -ComputerName DC01 | Format-MrCounter | Format-List -Property *

ironscripter2019prep3a.jpg

The Format-MrCounter function shown in this blog article can be downloaded from my IronScripter repository on GitHub.

If you enjoyed this blog article and the techniques I used in my solution, then I hope to see you in my Finding Performance Bottlenecks with PowerShell session at the PowerShell + DevOps Global Summit 2019.

µ