'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 |