'How to parse/pipe cmd line output using Powershell to an object

I have an *.exe that outputs this data when I run this PowerShell command:

& $myExe list

Where $myExe is something like C:\Temp\MyExe.exe and list is an argument.

List of Runbook ID on the system: 



List of services installed on the system: 

ALMService   Version: 7.0.4542.16189
AOSService   Version: 7.0.4542.16189
BIService    Version: 7.0.4542.16189
DevToolsService  Version: 7.0.4542.16189
DIXFService  Version: 7.0.4542.16189
MROneBox     Version: 7.1.1541.3036
PayrollTaxModule     Version: 7.1.1541.3036
PerfSDK  Version: 7.0.4542.16189
ReportingService     Version: 7.0.4542.16189
RetailCloudPos   Version: 7.1.1541.3036
RetailHQConfiguration    Version: 7.1.1541.3036
RetailSDK    Version: 7.1.1541.3036
RetailSelfService    Version: 7.1.1541.3036
RetailServer     Version: 7.1.1541.3036
RetailStorefront     Version: 7.1.1541.3036
SCMSelfService   Version: 7.1.1541.3036

The data I'm looking for is the first column of the table, but it has things like List of Runbook ID... at the top. Is there a good way in PowerShell to parse this data so I can get just the table data?



Solution 1:[1]

You could save the output in a variable, use Where-Object to filter just the lines that have Version in it, then remove all the unwanted characters with a -replace regex.

$myExeOutput = & $myExe list
$myExeOutput |
    Where-Object {$_ -match 'Version:'} |
    ForEach-Object {
        $_ -replace '\s+Version:.*$',''
    }

Solution 2:[2]

BenH's helpful answer works well with your particular input and he makes a good point in general: when you call external utilities (command-line applications), all you get back are lines of text, unlike with PowerShell-native commands that pass objects around.

Parsing strings (text) will always be more brittle than dealing with objects (which is why PowerShell's fundamental object orientation represents a great evolutionary leap in shell design).

That said, if you can make certain assumptions about the formatting of the strings you receive, PowerShell offers great tools to help even with that:

Imagine a function Select-Column that selects whitespace-separated fields (column values) by index from each input line (vaguely akin to awk):

@'
List of Runbook ID on the system: 



List of services installed on the system: 

ALMService   Version: 7.0.4542.16189
AOSService   Version: 7.0.4542.16189
BIService    Version: 7.0.4542.16189
DevToolsService  Version: 7.0.4542.16189
DIXFService  Version: 7.0.4542.16189
MROneBox     Version: 7.1.1541.3036
PayrollTaxModule     Version: 7.1.1541.3036
PerfSDK  Version: 7.0.4542.16189
ReportingService     Version: 7.0.4542.16189
RetailCloudPos   Version: 7.1.1541.3036
RetailHQConfiguration    Version: 7.1.1541.3036
RetailSDK    Version: 7.1.1541.3036
RetailSelfService    Version: 7.1.1541.3036
RetailServer     Version: 7.1.1541.3036
RetailStorefront     Version: 7.1.1541.3036
SCMSelfService   Version: 7.1.1541.3036
'@ -split '\r?\n' | 
  Select-Column -Index 0 -RequiredCount 3

The above, due to selecting the 1st column (-Index 0 - multiple indices supported) from only those lines that have exactly 3 fields (-RequiredCount 3), would yield:

ALMService
AOSService
BIService
DevToolsService
DIXFService
MROneBox
PayrollTaxModule
PerfSDK
ReportingService
RetailCloudPos
RetailHQConfiguration
RetailSDK
RetailSelfService
RetailServer
RetailStorefront
SCMSelfService

Select-Column source code:

Note that if you specify multiple (0-based) column indices, the output fields are tab-separated by default, which you can change with the -OutFieldSeparator parameter.

Function Select-Column {
  [cmdletbinding(PositionalBinding=$False)]
  param(
    [Parameter(ValueFromPipeline, Mandatory)]
    $InputObject,

    [Parameter(Mandatory, Position=0)]
    [int[]] $Index,

    [Parameter(Position=1)]
    [int] $RequiredCount,

    [Parameter(Position=2)]
    [string] $OutFieldSeparator = "`t"

  )

  process {
    if (($fields = -split $InputObject) -and ($RequiredCount -eq 0 -or $RequiredCount -eq $fields.Count)) {
      $fields[$Index] -join $OutFieldSeparator
    }
  }

}

Solution 3:[3]

This will parse it into objects:

$String = @'
List of Runbook ID on the system: 



List of services installed on the system: 

ALMService   Version: 7.0.4542.16189
AOSService   Version: 7.0.4542.16189
BIService    Version: 7.0.4542.16189
DevToolsService  Version: 7.0.4542.16189
DIXFService  Version: 7.0.4542.16189
MROneBox     Version: 7.1.1541.3036
PayrollTaxModule     Version: 7.1.1541.3036
PerfSDK  Version: 7.0.4542.16189
ReportingService     Version: 7.0.4542.16189
RetailCloudPos   Version: 7.1.1541.3036
RetailHQConfiguration    Version: 7.1.1541.3036
RetailSDK    Version: 7.1.1541.3036
RetailSelfService    Version: 7.1.1541.3036
RetailServer     Version: 7.1.1541.3036
RetailStorefront     Version: 7.1.1541.3036
SCMSelfService   Version: 7.1.1541.3036
'@

$String -split '\r?\n' | Select-Object -Skip 6 | ForEach-Object {
    if ($_ -match '^\s*(?<Name>.+?)Version:\s*(?<Version>[\d.]+)$') {
        [PSCustomObject]@{
            Name = $Matches['Name']
            Version = $Matches['Version']
        }
    }
    else {
        Write-Verbose -Verbose "Line didn't match. (Line: '$_')"
    }
}

Output from my system:

PS /home/joakim/Documents> ./exe_output.ps1


Name                      Version
----                      -------
ALMService                7.0.4542.16189
AOSService                7.0.4542.16189
BIService                 7.0.4542.16189
DevToolsService           7.0.4542.16189
DIXFService               7.0.4542.16189
MROneBox                  7.1.1541.3036
PayrollTaxModule          7.1.1541.3036
PerfSDK                   7.0.4542.16189
ReportingService          7.0.4542.16189
RetailCloudPos            7.1.1541.3036
RetailHQConfiguration     7.1.1541.3036
RetailSDK                 7.1.1541.3036
RetailSelfService         7.1.1541.3036
RetailServer              7.1.1541.3036
RetailStorefront          7.1.1541.3036
SCMSelfService            7.1.1541.3036

There are some assumptions. The main one is that a "version" can only consist of digits or periods. Include other characters in the character class as needed. The character class here is "[\d.]" - and remember that you need to escape periods to match literal periods rather than "any character" (except newlines (without the (?s) flag)) outside of a character class (stuff between "[" and "]", a meta language within the regex language).

Otherwise, it should be quite robust to minor changes.

For instance, if somehow a space or tab shows up at the beginning of the strings, this is handled with the always-matching "\s*" first ("paranoid parsing", where this kind of "paranoia" is beneficial for robust code).

Solution 4:[4]

@mklement0 This is brilliant! I used it to parse the output of "winrm get winrm/config/service" (which is a mess) to check whether or not Basic Authentication is allowed. I called your function with:

$cmd = "winrm get winrm/config/service"
$keys = @(Invoke-Expression $cmd | Select-Column -Index 0 -RequiredCount 4)
$values = @(Invoke-Expression $cmd | Select-Column -Index 2 -RequiredCount 4)
foreach ($key in $keys) {
    Write-Host "$($key) = $($values)" -ForegroundColor Green
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 BenH
Solution 2 mklement0
Solution 3 Svendsen Tech
Solution 4 visumancer