As an aside: For a given utility, you may be able to
tackle the problem at the source, by using its options to emit a machine-parseable data format rather than pretty-printed text (e.g, JSON), which PowerShell may be able to parse into objects (e.g., ConvertFrom-Json
), which you can then easily select properties from and format for display with PowerShell's Format-*
cmdlets.
Parsing and transforming fixed-width columnar text output in PowerShell:
Generally, it's best to parse the input text into custom objects ([pscustomobject]
instances), which makes them suitable for further programmatic processing and also reformatting using PowerShell's flexible Format-*
cmdlets.
To what extent this parsing can be automated without advance knowledge depends on the specifics of a given utility's fixed-width output (the assumption is that trailing whitespace in each column should be trimmed):
In the case of kubectl
, the columns are separated by 2 spaces, whereas the column names contain only 1 space, and the values none (in your sample output), so the prerequisite for automated parsing is met.
Custom function Select-Column
, whose source code is below, can parse the text into custom objects ([pscustomobject]
instances) via regex +
as the column separator expression (2 or more spaces), and the resulting objects are suitable for further programmatic processing and formatting in PowerShell.
You can simply pipe kubectl
output to it.
With up to 4 selected columns, the resulting objects are implicitly formatted as a table (implied Format-Table
); with 5 or more, as a list (implicit Format-List
).
You can pipe to Format-*
cmdlets explicitly to control the formatting.
# Parse all columns except 'Age' into custom objects and format them as a table:
kubectl ... | Select-Column ' +' -Exclude Age | Format-Table
The above yields:
NAME READY STATUS RESTARTS IP NODE NOMINATED NODE READINESS GATES
---- ----- ------ -------- -- ---- -------------- ---------------
me-pod-name 2/2 Running 0 10.0.0.10 node1 <none> <none>
me-pod-name-2 1/1 Running 0 10.0.1.20 node2 <none> <none>
me-pod-name-3 1/1 Running 0 10.0.0.30 node3 <none> <none>
You could wrap such calls in a custom function, including piping to Format-Table
, but note that doing the latter means that the output is again not suitable for programmatic processing, because Format-*
calls output formatting instructions, not data.
If you wanted the output to be table-formatted by default even for 5 or more properties, more work would be needed: you'd have to assign a distinct (virtual) type name to the output objects and define formatting data for that type.
Select-Column
source code:
function Select-Column {
<#
.SYNOPSIS
Parses columnar text data into custom objects.
.DESCRIPTION
Parses line-based columnar text data into custom objects, based on a
column-separator pattern specified as a regular expression.
By default, the values from all columns are returned as properties of the output
objects.
Use -Name to extract only given columns, or -ExcludeName to exclude columns.
.PARAMETER SeparatorPattern
A regular expression specifying what text separates the column names / values
in the input text.
The default is ' +', i.e. any run of one or more spaces, which works with
fixed-width columns whose column names and values have no embedded spaces.
.PARAMETER Name
The names of one more columns to extract from the input text.
These names must match existing columns.
By default, all columns are returned.
.PARAMETER Exclude
The names of one more input columns to exclude from the properties of the output
objects.
These names must match existing columns.
.PARAMETER InputObject
This parameter receives the individual lines of input text via the pipeline.
the pipeline.
Alternatively, you can pass the input text as a single multi-line string, both
via the pipeline and directly to this parameter.
.EXAMPLE
'col1 col2', 'val1 val2' | Select-Column
Converts the line-by-line input into custom objects with properties 'col1'
and 'col2' whose values are 'val1' and 'val2', based on runs of 1 or more
spaces acting as separators.
.EXAMPLE
'col 1 col 2', 'val1 val2' | Select-Column ' {2,}' -Exclude 'col 2'
Converts the line-by-line input into custom objects based on two or more spaces
as separators, excluding values from column 'col 2'.
#>
[CmdletBinding(DefaultParameterSetName = 'All')]
param(
[Parameter(Position = 0)]
[Alias('s')]
[string[]] $SeparatorPattern = ' +'
,
[Parameter(ParameterSetName = 'Include', Position = 1)]
[Alias('n')]
[string[]] $Name
,
[Parameter(ParameterSetName = 'Exclude')]
[Alias('x')]
[string[]] $ExcludeName
,
[Parameter(Mandatory, ValueFromPipeline)]
[string] $InputObject
)
begin {
Set-StrictMode -Version 1
$lineIndex = 0
}
process {
foreach ($line in $InputObject -split '\r?\n' -ne '') {
# Split the line into colum names / fields.
$fields = $line -split $SeparatorPattern
# Process the header row
if ($lineIndex++ -eq 0) {
$ndx = 0
# Map column names to their indices.
$nameToIndex = @{ }
foreach ($n in $fields) {
$nameToIndex[$n] = $ndx++
}
# Based on the given column names, build a list of indices
# to extract, and create an ordered hashtable with the specified
# column names to serve as a template for the output objects.
$unknownName = $null
if ($Name) {
# only the specified columns
$unknownName = (Compare-Object -PassThru $fields $Name).Where({ $_.SideIndicator -eq '=>' })
}
elseif ($ExcludeName) {
# all but the specified columns
$Name, $unknownName = (Compare-Object -PassThru $fields $ExcludeName).Where({ $_.SideIndicator -eq '<=' }, 'Split')
}
else {
$Name = $fields # default to all columns
}
if ($unknownName) { Throw "Unknown column name(s): $unknownName" }
if (-not $Name) { Throw "You have selected no output columns." }
$oht = [ordered] @{ }
$outColIndices = foreach ($n in $Name) {
$oht[$n] = $null
$nameToIndex[$n] # output the index
}
}
# Process a data row.
else {
# Fill in the ordered hashtable with this line's field values...
$ndx = 0
foreach ($n in $Name) {
$oht[$n] = $fields[$outColIndices[$ndx++]]
}
# ... and construct and output a custom object from it.
[pscustomobject] $oht
}
}
}
}