9

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?

3 Answers 3

9

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:.*$',''
    }
Sign up to request clarification or add additional context in comments.

3 Comments

This will work, but is there any more universal/repeatable method that doesn't have a specific regex and match? Or am I just SOL writing a string parsing routine for each output? I have various exe's that have similar outputs & styles, but different keywords.
@AlexKwitny The output of exe's won't be objects, but just strings. So you are stuck with string parsing. There's plenty of other ways to parse strings. This way seemed most straight forward with the design parameters you gave. If you can get the data in a more structured way, WMI, .Net, Registry, etc. then you can use less brittle methods.
@Bill_Stewart You are correct that it isn't. But my personal preference is that operations that make changes or are resource intensive have their output cached, if the data isn't too large to be comfortably kept in memory. This way I do not need to rerun the operation if I need make a down level change, e.g. update a filter or export to a new location .
7

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

}

Comments

2

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.]+)\s*$') {
        [PSCustomObject]@{
            Name = $Matches['Name'].TrimEnd()
            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).

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.