|
using namespace System.Management.Automation |
|
using namespace System.Management.Automation.Runspaces |
|
using namespace System.Collections |
|
using namespace System.Collections.Generic |
|
using namespace System.Text |
|
using namespace System.Xml |
|
using namespace System.IO |
|
|
|
$ErrorActionPreference = 'Stop' |
|
$SCRIPT:RunspaceFormats = [Runspace]::DefaultRunspace.InitialSessionState.Formats |
|
|
|
function ConvertFrom-Format { |
|
<# |
|
.SYNOPSIS |
|
Converts Format output to a FormatViewDefinition that can be persisted to the session state or saved as an XML file with ConvertTo-FormatXml |
|
#> |
|
param( |
|
#The name of the format view definition assigned to the format data. Default is 'CustomEasyTableView |
|
[ValidateNotNullOrEmpty()][string]$Name = 'CustomEasyTableView', |
|
#The format data to convert. It's best to pipe to this command from Format-Table, etc. for best results |
|
[Parameter(ValueFromPipeline)][object]$InputObject |
|
) |
|
|
|
begin { |
|
$ErrorActionPreference = 'Stop' |
|
New-Variable -Name 'FormatStartData' |
|
} |
|
|
|
process { |
|
#HACK: This is an internal type so we cannot use -is or reference the type directly |
|
if (($InputObject.GetType().Name) -ne 'FormatStartData') { |
|
return |
|
} |
|
|
|
if ($FormatStartData) { |
|
Write-Warning 'Multiple formats detected, please pass only one format type to this function. It will only process the first format detected' |
|
return |
|
} |
|
$formatStartData = $InputObject |
|
} |
|
end { |
|
if (-not $formatStartData) { |
|
Write-Error 'You must provide formatting info to this command. Try | Format-Table | ConvertFrom-Format' |
|
} |
|
|
|
$shapeInfo = $formatStartData.shapeInfo |
|
if ($shapeInfo.HeaderInfo.count -gt 1) { throw 'Multiple Header Infos detected. This is a bug and should never happen.' } |
|
|
|
[PSControl]$control = switch ($shapeInfo.GetType().Name) { |
|
'TableHeaderInfo' { |
|
$tcProps = @{ |
|
CalculatedProperties = $formatStartData.CalculatedProperties |
|
AutoSize = $null -ne $FormatStartData.autoSizeInfo |
|
Wrap = $formatStartData.Wrap |
|
} |
|
New-TableControl @tcProps $shapeInfo |
|
} |
|
default { throw [NotImplementedException]'This type of format is not supported yet' } |
|
} |
|
|
|
return [FormatViewDefinition]::new($Name, $control) |
|
} |
|
} |
|
|
|
filter Add-Format { |
|
<# |
|
.SYNOPSIS |
|
Adds a new format view definition to the current session state |
|
#> |
|
[CmdletBinding()] |
|
param( |
|
#Supply the type to associate to the format definition |
|
[Parameter(Mandatory)][string]$Type, |
|
[Parameter(Mandatory, ValueFromPipeline)] |
|
[Management.Automation.FormatViewDefinition]$FormatViewDefinition, |
|
|
|
#Pass through the new Extended Type Definition |
|
[switch]$PassThru, |
|
|
|
#Don't save the format data to the session state as the default view for the object |
|
[switch]$NoPersist, |
|
|
|
#Update the definitions but do not process them. You should rarely need to do this, maybe if adding a lot of type definitions at runtime amd then update them separately |
|
[switch]$NoUpdate |
|
) |
|
|
|
$ErrorActionPreference = 'Stop' |
|
|
|
$formatName = $FormatViewDefinition.Name |
|
|
|
#TODO: More Advanced Conflict/Update Checking |
|
$existingTypeData = Get-FormatData -TypeName $Type |
|
| Where-Object TypeNames -Contains $Type |
|
|
|
$existingFormatData = $existingTypeData |
|
| ForEach-Object FormatViewDefinition |
|
| Where-Object Name -EQ $formatName |
|
| Where-Object Control -Is [TableControl] |
|
|
|
$formatData = [ExtendedTypeDefinition]::new( |
|
$Type, |
|
[List[FormatViewDefinition]]@($FormatViewDefinition) |
|
) |
|
|
|
if (-not $NoPersist) { |
|
if ($existingFormatData) { |
|
#Update the formatting information for the type |
|
throw [NotImplementedException]"#TODO: An existing table view $formatName for $Type was found. Updating existing format data is not yet implemented." |
|
# Write-Verbose "Found existing table format data $Name for type $Type. Updating..." |
|
# $existingFormatData.Control = $FormatViewDefinition.Control |
|
} |
|
|
|
Write-Verbose "Adding new format definition $formatName for type $Type." |
|
|
|
if ($existingTypeData) { |
|
#HACK: We need to "prepend" our new type data to have it take precedence and be the default view, but unfortunately |
|
#the collection has no prepend method, so we need to create a new list, prepend, flush the formats, and add the |
|
#new list with the format prepended |
|
Write-Verbose "Existing type data found for $Type. Prepending new format data." |
|
[List[SessionStateFormatEntry]]$newFormats = $SCRIPT:RunspaceFormats |
|
$newFormats.insert(0, $formatData) |
|
$SCRIPT:RunspaceFormats.Clear() |
|
$SCRIPT:RunspaceFormats.Add($newFormats) |
|
} else { |
|
#If it is a net-new type format, then prepending isn't needed |
|
$SCRIPT:RunspaceFormats.Add($formatData) |
|
|
|
#TODO: -Append parameter to add to an existing type definition |
|
} |
|
} |
|
|
|
if (-not $NoUpdate) { |
|
Update-FormatData |
|
} |
|
|
|
if ($PassThru) { |
|
return $formatData |
|
} |
|
|
|
} |
|
function New-TableControl { |
|
[OutputType([TableControl])] |
|
param($TableHeaderInfo, $CalculatedProperties, [bool]$AutoSize, [bool]$Wrap) #Should be TableHeaderInfo |
|
|
|
[TableControl]$table = [TableControl]::new() |
|
[TableControlRow]$rowFormat = [TableControlRow]::new() |
|
$rowFormat.Wrap = $Wrap |
|
|
|
#HACK: There is an additional objectCount property that is apparently not used in the translation |
|
$table.AutoSize = $AutoSize |
|
$table.Rows = $rowFormat |
|
$table.HideTableHeaders = $TableHeaderInfo.hideHeader |
|
#FIXME: This is set in format data but doesn't take effect in the view. Need to find out why. |
|
if ($table.HideTableHeaders) { |
|
Write-Warning 'BUG: HideTableHeaders will only work if you save this format definition to XML. More Info: https://github.com/PowerShell/PowerShell/issues/23841' |
|
} |
|
|
|
if ($TableHeaderInfo.tableColumnInfoList.count -le 0) { throw 'No table column info found' } |
|
foreach ($columnInfo in $TableHeaderInfo.tableColumnInfoList) { |
|
#This is a little confusing, but the row columns and the headers should almost always align. |
|
#The display label is defined on the header and the actual property it references is defined in the column. |
|
#If the property and label are the same, the label can be omitted |
|
#So for each property in the HeaderInfo, we create a new TableControlColumnHeader and TableControlColumn |
|
|
|
#If the columnInfo has a calculated property with a scriptblock, the propertyname needs to be set to the header label |
|
#to display correctly. Otherwise, label can be omitted unless it is different from the propertyname. |
|
|
|
[string]$scriptBlock = $null -ne $CalculatedProperties ? |
|
$CalculatedProperties[$columnInfo.propertyName] : |
|
$null |
|
|
|
[string]$label = $scriptBlock ? $columnInfo.propertyName : $columnInfo.Label |
|
|
|
$columnHeader = [TableControlColumnHeader]::new($label, $columnInfo.Width, $columnInfo.Alignment) |
|
|
|
$table.Headers.Add($columnHeader) |
|
if ($columnInfo.propertyName) { |
|
$displayEntry = $scriptBlock ? |
|
[DisplayEntry]::new($scriptBlock.Trim(), 'ScriptBlock') : |
|
[DisplayEntry]::new($columnInfo.propertyName, 'Property') |
|
|
|
$column = [TableControlColumn]::new($columnInfo.Alignment, $displayEntry) |
|
$rowFormat.Columns.Add($column) |
|
} |
|
} |
|
|
|
return $table |
|
} |
|
|
|
function ConvertTo-FormatXml { |
|
<# |
|
.SYNOPSIS |
|
Converts ExtendedTypeDefinition objects to XML format data |
|
#> |
|
[CmdletBinding(DefaultParameterSetName = 'FormatView')] |
|
param( |
|
[Parameter(ParameterSetName = 'TypeDefinition', Mandatory, ValueFromPipeline)] |
|
[Management.Automation.ExtendedTypeDefinition[]]$TypeDefinition, |
|
|
|
[Parameter(ParameterSetName = 'FormatView', Mandatory, ValueFromPipeline)] |
|
[Management.Automation.FormatViewDefinition]$FormatDefinition, |
|
|
|
[Parameter(ParameterSetName = 'FormatView', Mandatory, ValueFromPipeline)] |
|
[String]$TypeName, |
|
|
|
[switch]$Compress, |
|
[string]$OutFile |
|
) |
|
|
|
begin { |
|
Write-Host -ForegroundColor Magenta "ParameterSetName: $($PSCmdlet.ParameterSetName)" |
|
[List[ExtendedTypeDefinition]]$TypeDefinitions = @() |
|
} |
|
|
|
process { |
|
if ($FormatDefinition) { |
|
[List[FormatViewDefinition]]$FormatDefinitionList = @($FormatDefinition) |
|
$TypeDefinitions.Add(([ExtendedTypeDefinition]::new($TypeName, $FormatDefinitionList))) |
|
} |
|
|
|
foreach ($tdItem in $TypeDefinition) { |
|
$TypeDefinitions.Add($tdItem) |
|
} |
|
} |
|
|
|
end { |
|
#HACK: Fast way to get SMA rather than enumerating. There's no guarantee FormatXML will remain here but it is reasonably safe. |
|
$smaAssembly = [psobject].assembly |
|
|
|
$writeToXml = $smaAssembly. |
|
GetType('Microsoft.PowerShell.Commands.FormatXmlWriter'). |
|
GetMethod('WriteToXml', @('Static', 'NonPublic')) |
|
|
|
$xmlSettings = [XmlWriterSettings]::new() |
|
|
|
if (-not $Compress) { |
|
$xmlSettings.Indent = $true |
|
$xmlSettings.NewLineOnAttributes = $true |
|
} |
|
|
|
$sb = [StringBuilder]::new() |
|
$xmlWriter = [XmlWriter]::Create($sb, $xmlSettings) |
|
$writeToXmlParams = @( |
|
$xmlWriter, |
|
$TypeDefinitions, |
|
$true #Export Script blocks |
|
) |
|
$writeToXml.Invoke($null, $writeToXmlParams) |
|
$xmlWriter.Flush() |
|
$xmlOutput = $sb.ToString() |
|
|
|
if ($OutFile) { |
|
Out-File -InputObject $xmlOutput -FilePath $OutFile |
|
return |
|
} |
|
|
|
return $xmlOutput |
|
} |
|
} |
|
|
|
function Format-EasyTable { |
|
[CmdletBinding(DefaultParameterSetName = 'Property')] |
|
param( |
|
[Parameter(ParameterSetName = 'Property', Position = 0)] |
|
[System.Object[]] |
|
${Property}, |
|
|
|
#TODO: Implement these properties |
|
# [System.Object] |
|
# ${GroupBy}, |
|
|
|
[Parameter(ParameterSetName = 'View')] |
|
[string] |
|
${View}, |
|
|
|
# [switch] |
|
# ${ShowError}, |
|
|
|
# [switch] |
|
# ${DisplayError}, |
|
|
|
# [switch] |
|
# ${Force}, |
|
|
|
# [ValidateSet('CoreOnly', 'EnumOnly', 'Both')] |
|
# [string] |
|
# ${Expand}, |
|
|
|
[switch] |
|
${AutoSize}, |
|
|
|
# [switch] |
|
# ${RepeatHeader}, |
|
|
|
[switch] |
|
${HideTableHeaders}, |
|
|
|
[switch] |
|
${Wrap}, |
|
|
|
#region Custom Properties |
|
|
|
#If specified, will not save the format data to the session state as the default view for the object |
|
[Parameter(ParameterSetName = 'Property')] |
|
[switch]$NoPersist, |
|
|
|
#If specified, will save the view with a custom name that can be referenced later with Format-Table -View |
|
[Parameter(ParameterSetName = 'Property')] |
|
[string]$ViewName, |
|
|
|
#endregion Custom Properties |
|
|
|
#InputObject goes as the last property as this is usually provided via the pipeline and we don't want it to be positional. |
|
[Parameter(Mandatory, ValueFromPipeline = $true)] |
|
[psobject] |
|
${InputObject} |
|
) |
|
|
|
begin { |
|
[List[Object]]$InputObjects = @() |
|
|
|
#A deduplicated list of the types provided to format-table, for purposes of collecting |
|
[HashSet[String]]$TypeNames = @() |
|
} |
|
|
|
process { |
|
#Collect the input objects on the pipeline, if any |
|
$InputObjects.Add($PSItem) |
|
} |
|
|
|
end { |
|
#Replace the boundparameters for splatting to Format-Table |
|
if ($InputObjects.count -gt 0) { |
|
$PSBoundParameters.InputObject = $InputObjects |
|
} |
|
|
|
#Collect the types of the input objects for attaching to the formatStartData. |
|
foreach ($inputObjectItem in $PSBoundParameters.InputObject) { |
|
[void]$TypeNames.Add($inputObjectItem.GetType()) |
|
} |
|
|
|
#TODO: Gather calculated property information to attach as metadata |
|
|
|
#Remove Extension Properties |
|
foreach ($customParam in 'NoPersist', 'ViewName') { |
|
[void]$PSBoundParameters.Remove($customParam) |
|
} |
|
|
|
$ftResult = Microsoft.PowerShell.Utility\Format-Table @PSBoundParameters -ErrorAction Stop |
|
|
|
#If -View was used, we don't save the output and return immediately. |
|
if ($View) { |
|
return $ftResult |
|
} |
|
|
|
if ($ftResult.count -eq 0) { |
|
Write-Warning 'No formatting output found. Did you pass any objects?' |
|
return |
|
} |
|
|
|
if ($ftResult[0].GetType().Name -ne 'FormatStartData' ) { |
|
throw [InvalidDataException]'Unexpected output type from Format-Table.' |
|
} |
|
|
|
#Add the types as an ETS property to FormatStartData |
|
Add-Member -InputObject $ftResult[0] -NotePropertyName 'TypeNames' -NotePropertyValue $TypeNames -Force |
|
|
|
Add-Member -InputObject $ftResult[0] -NotePropertyName 'Wrap' -NotePropertyValue $Wrap -Force |
|
|
|
$calcPropertyMetadata = Get-CalculatedPropertiesMetadata $PSBoundParameters.Property |
|
if ($calcPropertyMetadata.count -gt 0) { |
|
Add-Member -InputObject $ftResult[0] -NotePropertyName 'CalculatedProperties' -NotePropertyValue $calcPropertyMetadata -Force |
|
} |
|
|
|
$convertFromFormatParams = @{} |
|
if ($ViewName) { |
|
$convertFromFormatParams.Name = $ViewName |
|
} |
|
|
|
foreach ($Type in $ftResult[0].TypeNames) { |
|
$ftResult |
|
| ConvertFrom-Format @convertFromFormatParams |
|
| Add-Format -Type $Type -NoPersist:$NoPersist |
|
} |
|
|
|
return $ftResult |
|
} |
|
} |
|
|
|
|
|
#region Private |
|
function Get-CalculatedPropertiesMetadata ([object]$Properties) { |
|
|
|
#TODO: In order to do this we have to update the typedata. |
|
|
|
$calcProps = @{} |
|
foreach ($Property in $Properties) { |
|
|
|
|
|
if ($Property -is [scriptblock]) { |
|
throw [NotSupportedException]'Raw Scriptblocks in -Properties are not supported, use hashtables instead' |
|
} |
|
if ($Property -is [string]) { |
|
#This has already been handled by Format-Table, there's nothing to include |
|
continue |
|
} |
|
|
|
#Only hashtables from this point on |
|
if ($Property -isnot [hashtable]) { |
|
Write-Warning "$($Property.GetType()) is not currently implemented for parsing in -Property and will be ignored" |
|
continue |
|
} |
|
|
|
if ($Property.ContainsKey('Name') -and $Property.ContainsKey('N')) { |
|
throw [InvalidDataException]'Cannot specify both Name and N for a calculated Property' |
|
} |
|
|
|
if ($Property.ContainsKey('Expression') -and $Property.ContainsKey('E')) { |
|
throw [InvalidDataException]'Cannot specify both Expression and E for a calculated Property' |
|
} |
|
|
|
[string]$Name = $null |
|
|
|
foreach ($key in $Property.keys) { |
|
#TODO: Add FormatString, Width, Alignment if these properties not present in FT |
|
switch -Wildcard ($key) { |
|
'Name' { |
|
$Name = $Property[$key] |
|
break |
|
} |
|
'N' { |
|
$Name = $Property[$key] |
|
break |
|
} |
|
'default' { |
|
Write-Warning "$key is not a currently implemented key for a calculated property in -Properties and will be ignored" |
|
continue |
|
} |
|
} |
|
} |
|
|
|
if (-not $Name) { |
|
Write-Warning 'The Name key was not included in the calculated property hashtable. That property will be ignored.' |
|
continue |
|
} |
|
|
|
if (-not ($Property.Expression -or $Property.E)) { |
|
Write-Warning "Calculated Property $Name does not have an Expression key. This property will be ignored." |
|
continue |
|
} |
|
$calcProps[$Name] = $Property.Expression ?? $Property.E |
|
} |
|
return $calcProps |
|
} |
|
|
|
#endregion Private |