FormatColumn.psm1
using namespace System.Management.Automation function Format-Column { <# .SYNOPSIS Format-Column formats object data as columns, ordering data column by column as default. .DESCRIPTION Format-Column function outputs object data into columns, similarly to built-in cmdlet Format-Wide. It can order output data column by column in addition to row by row, as is the only option in Format-Wide. .PARAMETER Property Name of object property to be displayed. The value of the Property parameter can also be a calculated property: - a hash table. Valid syntaxes are: - @{Expression=<string>|{<scriptblock>}} - @{FormatString=<string>} - @{Expression=<string>|{<scriptblock>}; FormatString=<string>} - a script block: {<scriptblock>} Property parameter is optional. However, if omitted for data containing properties, but missing DefaultDisplayProperty or name property, no comprehensible data output will be produced. .PARAMETER ColumnCount Number of columns to display (CustomSize mode). If ColumnCount parameter is omitted the number of columns is calculated automatically (AutoSize mode). .PARAMETER MaxColumnCount Maximum number of columns to display in AutoSize mode. Optional. Cannot be combined with ColumnCount parameter. .PARAMETER MinRowCount Minimum number of rows to display in AutoSize mode. Optional. Cannot be combined with ColumnCount parameter. .PARAMETER GroupBy Formats the output in groups based on a shared property or value. Optional. Can be the name of a property or it can be a calculated property: - a hash table. Valid syntaxes are: - @{Expression=<string>|{<scriptblock>}} - @{Label/Name=<string>; Expression=<string>|{<scriptblock>}} - a script block: {<scriptblock>} .PARAMETER OrderBy Determines data order in column output. Default value is Column. Valid values are: - Column: Orders data column by column. - Row: Orders data row by row. .PARAMETER InputObject Object to format for display. Accepts pipeline input. .EXAMPLE 1..100 | Format-Column -MinRowCount 20 -OrderBy Row .EXAMPLE Format-Column -Property @{FormatString='{0:000}'} -ColumnCount 3 -InputObject (1..125) .EXAMPLE Get-Process | Format-Column -Property @{Expr='Id'; FormatStr='{0:00000}'} .EXAMPLE # The following Property syntaxes are all equivalent: Get-Process | Format-Column -Property ProcessName # name (string) Get-Process | Format-Column -Property {$_.ProcessName} # scriptblock Get-Process | Format-Column -Property @{Expr='ProcessName'} # hashtable string expression Get-Process | Format-Column -Property @{Expr={$_.ProcessName}} # hashtable scriptblock expression .INPUTS You can pipe any object to Format-Column. .OUTPUTS Format-Column returns strings that represent the output table. .NOTES Included alias for Format-Column is 'fcol'. .LINK Online version: https://github.com/loxia01/FormatColumn #> [CmdletBinding(DefaultParameterSetName='AutoSize')] [Alias('fcol')] param ( [Parameter(Position=0)] [SupportsWildcards()] [Object]$Property, [Parameter(ParameterSetName='CustomSize', Mandatory)] [ValidateScript({$_ -gt 0})] [int]$ColumnCount, [Parameter(ParameterSetName='AutoSize')] [ValidateScript({$_ -gt 0})] [int]$MaxColumnCount, [Parameter(ParameterSetName='AutoSize')] [ValidateScript({$_ -gt 0})] [int]$MinRowCount, [Parameter()] [SupportsWildcards()] [Object]$GroupBy, [Parameter()] [ValidateSet('Column','Row')] [string]$OrderBy = 'Column', [Parameter(ValueFromPipeline)] [psobject]$InputObject ) if ($input) { $InputObject = $input } if ($null -eq $InputObject) { return } # Property and GroupBy validation and processing $properties = $InputObject[0].PSObject.Properties if ($Property) { if ($Property -is [hashtable]) { $Property.Keys | ForEach-Object { if ($_ -match '^e(x(p(r(e(s(s(i(on?)?)?)?)?)?)?)?)?$') { $pExpr = $Property.$_ } elseif ($_ -match '^f(o(r(m(a(t(s(t(r(i(ng?)?)?)?)?)?)?)?)?)?)?$') { $pFormatStr = $Property.$_ } else { $exception = New-Object PSArgumentException "Invalid key '${_}' in Property hashtable." $PSCmdlet.ThrowTerminatingError((New-Object ErrorRecord -Args $exception, 'DictionaryKeyIllegal', 5, $null)) } } if ($pFormatStr -and $pFormatStr -isnot [string]) { $exception = New-Object PSArgumentException "Formatstring key in Property hashtable is not of type String." $PSCmdlet.ThrowTerminatingError((New-Object ErrorRecord -Args $exception, 'DictionaryKeyIllegalValue', 5, $null)) } if ($pExpr) { if ($pExpr -is [string]) { if ([wildcardpattern]::ContainsWildcardCharacters($pExpr)) { $pExpr = $properties | Where-Object Name -Like $pExpr | Select-Object -ExpandProperty Name -First 1 } if ($pFormatStr) { $propertySelect = {$pFormatStr -f ($_.$pExpr -join ", ")} } else { $propertySelect = {$_.$pExpr -join ", "} } } elseif ($pExpr -is [scriptblock]) { $pExpr = [scriptblock]::Create("@(${pExpr}) -join ', '") if ($pFormatStr) { $propertySelect = {$pFormatStr -f (& $pExpr)} } else { $propertySelect = $pExpr } } else { $exception = New-Object PSArgumentException "Expression key in Property hashtable is not of type String or ScriptBlock." $PSCmdlet.ThrowTerminatingError((New-Object ErrorRecord -Args $exception, 'DictionaryKeyIllegalValue', 5, $null)) } } else { $exception = New-Object PSArgumentException "Property hashtable is missing mandatory expression key." $PSCmdlet.ThrowTerminatingError((New-Object ErrorRecord -Args $exception, 'DictionaryKeyMandatoryEntry', 5, $null)) } } elseif ($Property -is [string]) { if ([wildcardpattern]::ContainsWildcardCharacters($Property)) { $Property = $properties | Where-Object Name -Like $Property | Select-Object -ExpandProperty Name -First 1 } $propertySelect = {$_.$Property -join ", "} } elseif ($Property -is [scriptblock]) { $propertySelect = [scriptblock]::Create("@(${Property}) -join ', '") } else { $exception = New-Object PSArgumentException "Property parameter value is not of type String, ScriptBlock or Hashtable." $PSCmdlet.ThrowTerminatingError((New-Object ErrorRecord -Args $exception, 'ArgumentUnknownType', 5, $null)) } } else { if ($InputObject[0].PSStandardMembers.DefaultDisplayPropertySet -or $InputObject[0].PSStandardMembers.DefaultDisplayProperty) { $defaultDisplayProperty = if ($InputObject[0].PSStandardMembers.DefaultDisplayPropertySet) { if ($InputObject[0].PSStandardMembers.DefaultDisplayPropertySet.ReferencedPropertyNames -contains 'Name') { 'Name' } elseif ($InputObject[0].PSStandardMembers.DefaultDisplayPropertySet.ReferencedPropertyNames -like '*Name') { $InputObject[0].PSStandardMembers.DefaultDisplayPropertySet | Where-Object ReferencedPropertyNames -Like '*Name' | Select-Object -ExpandProperty ReferencedPropertyNames -First 1 } else { $InputObject[0].PSStandardMembers.DefaultDisplayPropertySet.ReferencedPropertyNames[0] } } else { $InputObject[0].PSStandardMembers.DefaultDisplayProperty } $propertySelect = {$_.$defaultDisplayProperty -join ", "} } else { $displayProperty = if ($properties.Name -contains 'Name') { 'Name' } elseif ($properties.Name -like '*Name') { $properties | Where-Object Name -Like '*Name' | Select-Object -ExpandProperty Name -First 1 } elseif (-not ($properties.Name -match '^(Count|Length)$')) { @($properties.Name)[0] } else { $false } if ($displayProperty) { $propertySelect = {$_.$displayProperty -join ", "} } else { $propertySelect = {$_ -join ", "} } } } if (-not $GroupBy) { try { $outputData = $InputObject | ForEach-Object $propertySelect } catch { $exception = New-Object PSArgumentException -Args $_.Exception.Message, $_.Exception $PSCmdlet.ThrowTerminatingError((New-Object ErrorRecord -Args $exception, 'ArgumentException', 5, $null)) } } else { if ($GroupBy -is [hashtable]) { $GroupBy.Keys | ForEach-Object { if ($_ -match '^n(a(me?)?)?$|^l(a(b(el?)?)?)?$') { $gLabel = $GroupBy.$_ } elseif ($_ -match '^e(x(p(r(e(s(s(i(on?)?)?)?)?)?)?)?)?$') { $gExpr = $GroupBy.$_ } else { $exception = New-Object PSArgumentException "Invalid key '${_}' in GroupBy hashtable." $PSCmdlet.ThrowTerminatingError((New-Object ErrorRecord -Args $exception, 'DictionaryKeyIllegal', 5, $null)) } } if ($gLabel -and $gLabel -isnot [string]) { $exception = New-Object PSArgumentException "Label/Name key in GroupBy hashtable is not of type String." $PSCmdlet.ThrowTerminatingError((New-Object ErrorRecord -Args $exception, 'DictionaryKeyIllegalValue', 5, $null)) } if ($gExpr) { if ($gExpr -is [string]) { if ([wildcardpattern]::ContainsWildcardCharacters($gExpr)) { $gExpr = $properties | Where-Object Name -Like $gExpr | Select-Object -ExpandProperty Name -First 1 } else { $gExpr = $properties | Where-Object Name -EQ $gExpr | Select-Object -ExpandProperty Name } $groupSelect = {$_.$gExpr -join ", "} } elseif ($gExpr -is [scriptblock]) { $groupSelect = [scriptblock]::Create("@(${gExpr}) -join ', '") } else { $exception = New-Object PSArgumentException "Expression key in GroupBy hashtable is not of type String or ScriptBlock." $PSCmdlet.ThrowTerminatingError((New-Object ErrorRecord -Args $exception, 'DictionaryKeyIllegalValue', 5, $null)) } } else { $exception = New-Object PSArgumentException "GroupBy hashtable is missing mandatory expression key." $PSCmdlet.ThrowTerminatingError((New-Object ErrorRecord -Args $exception, 'DictionaryKeyMandatoryEntry', 5, $null)) } } elseif ($GroupBy -is [string]) { if ([wildcardpattern]::ContainsWildcardCharacters($GroupBy)) { $GroupBy = $properties | Where-Object Name -Like $GroupBy | Select-Object -ExpandProperty Name -First 1 } else { $GroupBy = $properties | Where-Object Name -EQ $GroupBy | Select-Object -ExpandProperty Name } $groupSelect = {$_.$GroupBy -join ", "} } elseif ($GroupBy -is [scriptblock]) { $groupSelect = [scriptblock]::Create("@(${GroupBy}) -join ', '") } else { $exception = New-Object PSArgumentException "GroupBy parameter value is not of type String, ScriptBlock or Hashtable." $PSCmdlet.ThrowTerminatingError((New-Object ErrorRecord -Args $exception, 'ArgumentUnknownType', 5, $null)) } try { $outputData = $InputObject | ForEach-Object { [pscustomobject]@{$propertySelect = & $propertySelect; $groupSelect = & $groupSelect} } } catch { $exception = New-Object PSArgumentException -Args $_.Exception.Message, $_.Exception $PSCmdlet.ThrowTerminatingError((New-Object ErrorRecord -Args $exception, 'ArgumentException', 5, $null)) } $groups = $outputData.$groupSelect | Sort-Object -Unique $outputDataGroups = [Collections.Generic.List[Object]]@() foreach ($group in $groups) { $outputDataGroups.Add(($outputData | Where-Object $groupSelect -EQ $group | Select-Object -ExpandProperty $propertySelect)) } if (-not $gLabel -and $groups) { if ($gExpr) { $gLabel = $gExpr } else { $gLabel = $GroupBy } } } # Output Processing if (-not $psISE) { $consoleWidth = $Host.UI.RawUI.WindowSize.Width } else { $consoleWidth = $Host.UI.RawUI.BufferSize.Width } $columnGap = 1 if (-not $outputDataGroups) { $maxLength = ($outputData | Measure-Object Length -Maximum).Maximum if (-not $ColumnCount) { $ColumnCount = [Math]::Max(1, [Math]::Floor($consoleWidth / ($maxLength + $columnGap))) if ($outputData.Count -lt $ColumnCount) { $ColumnCount = $outputData.Count } if ($MaxColumnCount -and $MaxColumnCount -lt $ColumnCount) { $ColumnCount = $MaxColumnCount } } $rowCount = [Math]::Ceiling($outputData.Count / $ColumnCount) if ($MinRowCount -and $MinRowCount -gt $rowCount) { $ColumnCount = [Math]::Max(1, [Math]::Floor($outputData.Count / $MinRowCount)) $rowCount = [Math]::Ceiling($outputData.Count / $ColumnCount) } $columnWidth = [Math]::Floor(($consoleWidth - $ColumnCount * $columnGap) / $ColumnCount) <# Truncate strings longer than column width (applicable only for CustomSize mode, or in AutoSize mode if string lengths greater than or equal to console width are present). #> if ($maxLength -gt $columnWidth) { if ($columnWidth -ge 3) { $outputData = $outputData | ForEach-Object { if ($_.Length -gt $columnWidth) { $_.Remove($columnWidth - 3) + "..." } else { $_ } } } # Write terminating error if column width is too small for truncate ellipsis "..." else { $exception = "ColumnCount value too large for output display." $PSCmdlet.ThrowTerminatingError((New-Object ErrorRecord -Args $exception, 'ColumnCountDisplayLimit', 5, $null)) } } # Create format string for output $alignment = -($columnWidth + $columnGap) $formatString = ( 0..($ColumnCount - 1) | ForEach-Object { "{${_},${alignment}}" } ) -join "" # Output data ordered column by column or row by row if ($PSEdition -eq 'Desktop') { Write-Output "`n" } else { Write-Output "" } if ($OrderBy -eq 'Column') { 0..($rowCount - 1) | ForEach-Object { $row = $_ $lineContent = 0..($ColumnCount - 1) | ForEach-Object { $column = $_ @($outputData)[$row + $column * $rowCount] } Write-Output ($formatString -f $lineContent) } } else { 0..($rowCount - 1) | ForEach-Object { $row = $_ $lineContent = 0..($ColumnCount - 1) | ForEach-Object { $column = $_ @($outputData)[$column + $row * $ColumnCount] } Write-Output ($formatString -f $lineContent) } } if ($PSEdition -eq 'Desktop') { Write-Output "`n" } else { Write-Output "" } } else { if (-not $ColumnCount) { $ColumnCount = ($outputDataGroups | ForEach-Object { $maxLength = ($_ | Measure-Object Length -Maximum).Maximum $colCount = [Math]::Max(1, [Math]::Floor($consoleWidth / ($maxLength + $columnGap))) if ($MaxColumnCount -and $MaxColumnCount -lt $colCount) { $MaxColumnCount } else { $colCount } } | Measure-Object -Minimum).Minimum } $i = 0 foreach ($outputDataGroup in $outputDataGroups) { $rowCount = [Math]::Ceiling($outputDataGroup.Count / $ColumnCount) if ($MinRowCount -and $MinRowCount -gt $rowCount) { $ColumnCount = [Math]::Max(1, [Math]::Floor($outputDataGroup.Count / $MinRowCount)) $rowCount = [Math]::Ceiling($outputDataGroup.Count / $ColumnCount) } $maxLength = ($outputDataGroup | Measure-Object Length -Maximum).Maximum $columnWidth = [Math]::Floor(($consoleWidth - $ColumnCount * $columnGap) / $ColumnCount) <# Truncate strings longer than column width (applicable only for CustomSize mode, or in AutoSize mode if string lengths greater than or equal to console width are present). #> if ($maxLength -gt $columnWidth) { if ($columnWidth -ge 3) { $outputDataGroup = $outputDataGroup | ForEach-Object { if ($_.Length -gt $columnWidth) { $_.Remove($columnWidth - 3) + "..." } else { $_ } } } # Write terminating error if column width is too small for truncate ellipsis "..." else { $exception = "ColumnCount value too large for output display." $PSCmdlet.ThrowTerminatingError((New-Object ErrorRecord -Args $exception, 'ColumnCountDisplayLimit', 5, $null)) } } # Create format string for output $alignment = -($columnWidth + $columnGap) $formatString = ( 0..($ColumnCount - 1) | ForEach-Object { "{${_},${alignment}}" } ) -join "" # Output data ordered column by column or row by row, adding group label and value if ($PSEdition -eq 'Desktop') { Write-Output "" } Write-Output ("`n {0}: {1}`n" -f $gLabel, @($groups)[$i]) if ($PSEdition -eq 'Desktop') { Write-Output "" } if ($OrderBy -eq 'Column') { 0..($rowCount - 1) | ForEach-Object { $row = $_ $lineContent = 0..($ColumnCount - 1) | ForEach-Object { $column = $_ @($outputDataGroup)[$row + $column * $rowCount] } Write-Output ($formatString -f $lineContent) } } else { 0..($rowCount - 1) | ForEach-Object { $row = $_ $lineContent = 0..($ColumnCount - 1) | ForEach-Object { $column = $_ @($outputDataGroup)[$column + $row * $ColumnCount] } Write-Output ($formatString -f $lineContent) } } if (++$i -eq $groups.Count) { if ($PSEdition -eq 'Desktop') { Write-Output "`n" } else { Write-Output "" } } } } } |