Skip to content

Instantly share code, notes, and snippets.

@JustinGrote
Last active September 6, 2024 04:02
Show Gist options
  • Save JustinGrote/8dcf827517021ac0210b65db7bceecf3 to your computer and use it in GitHub Desktop.
Save JustinGrote/8dcf827517021ac0210b65db7bceecf3 to your computer and use it in GitHub Desktop.
A proxy for Format-Table to apply the resultant view or save it as a format definition

TypeFormat

This module provides an improved Format-Table that lets you persist the resultant Format-Table view either to the current session or to a .ps1xml formatting file.

This module requires PowerShell 7.2+, however the generated XML format files can be used with earlier versions.

Quick Start

PS> Get-Date
Monday, August 19, 2024 9:00:50 PM
PS> Get-Date | Format-TypeTable Year,Day,Month,Hour,Minute,Second
Day Month Hour Minute Second
--- ----- ---- ------ ------
 19     8   20     59     39
PS> Get-Date #Your created type is now persistent for the life of the session
Day Month Hour Minute Second
--- ----- ---- ------ ------
 19     8   20     59     39

How it Works

This module attaches additional metadata to format table objects, and utilizes the same internal PowerShell methods and types to both model the formatting and serialize it to XML. The "live apply" injects the formatting directly into the InitialSessionState Formats collection.

Impetus

This module was inspired by @StartAutomating's EzOut Module, which provided a PowerShell interface to authoring XML formatting files. I wanted to take the concept and expand it to enable you to immediately persist changes, as well as "author" views using command syntax you are already familiar with such as format-table.

#
# Module manifest for module 'TypeFormat'
#
# Generated by: Justin Grote @JustinWGrote
#
# Generated on: 8/18/2024
#
@{
# Script module or binary module file associated with this manifest.
RootModule = 'TypeFormat.psm1'
# Version number of this module.
ModuleVersion = '0.0.0'
# Supported PSEditions
# CompatiblePSEditions = @()
# ID used to uniquely identify this module
GUID = '2ac29ce3-fc3d-4396-9d43-a536b6887899'
# Author of this module
Author = 'Justin Grote @JustinWGrote'
# Company or vendor of this module
CompanyName = 'Unspecified'
# Copyright statement for this module
Copyright = '©2024 Justin Grote'
# Description of the functionality provided by this module
Description = 'Create and Persist PowerShell formatting files using Format-Table syntax'
# Minimum version of the PowerShell engine required by this module
# PowerShellVersion = ''
# Name of the PowerShell host required by this module
# PowerShellHostName = ''
# Minimum version of the PowerShell host required by this module
# PowerShellHostVersion = ''
# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# DotNetFrameworkVersion = ''
# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
# ClrVersion = ''
# Processor architecture (None, X86, Amd64) required by this module
# ProcessorArchitecture = ''
# Modules that must be imported into the global environment prior to importing this module
# RequiredModules = @()
# Assemblies that must be loaded prior to importing this module
# RequiredAssemblies = @()
# Script files (.ps1) that are run in the caller's environment prior to importing this module.
# ScriptsToProcess = @()
# Type files (.ps1xml) to be loaded when importing this module
# TypesToProcess = @()
# Format files (.ps1xml) to be loaded when importing this module
# FormatsToProcess = @()
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
# NestedModules = @()
# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export.
FunctionsToExport = 'Format-TypeTable'
# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = '*'
# Variables to export from this module
VariablesToExport = '*'
# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export.
AliasesToExport = '*'
# DSC resources to export from this module
# DscResourcesToExport = @()
# List of all modules packaged with this module
# ModuleList = @()
# List of all files packaged with this module
# FileList = @()
# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{
PSData = @{
# Tags applied to this module. These help with module discovery in online galleries.
# Tags = @()
# A URL to the license for this module.
# LicenseUri = ''
# A URL to the main website for this project.
# ProjectUri = ''
# A URL to an icon representing this module.
# IconUri = ''
# ReleaseNotes of this module
# ReleaseNotes = ''
# Prerelease string of this module
Prerelease = 'Source'
# Flag to indicate whether the module requires explicit user acceptance for install/update/save
# RequireLicenseAcceptance = $false
# External dependent modules of this module
# ExternalModuleDependencies = @()
} # End of PSData hashtable
} # End of PrivateData hashtable
# HelpInfo URI of this module
# HelpInfoURI = ''
# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
# DefaultCommandPrefix = ''
}
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
#region Exported Functions
function Format-TypeTable {
<#
.SYNOPSIS
Uses Format-Table syntax to persist formatting data to this session, or save it as a formatting file.
.DESCRIPTION
Format-TypeTable is an alternative to hand-editing formatting files, using familiar Format-Table syntax to create custom views for objects.
.EXAMPLE
$obj = [PSCustomObject]@{
Name = 'Test'
Items = 4
NotShown = 'thisproperty'
PSTypeName = 'MyTestType'
}
PS> $obj | Format-TypeTable Name,Length
RESULT:
PS> $obj
Name Items
---- ------
Test 4
This will create a new format view for the type MyTestType that will display the Name and Length properties only. The NotShown property will not be displayed but it is still present.
.EXAMPLE
class Test {
[string]$Name
[int]$Items
[string]$NotShown
}
#>
[CmdletBinding(DefaultParameterSetName = 'Property')]
param(
[Parameter(ParameterSetName = 'Property', Position = 0)]
[System.Object[]]
${Property},
[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},
#If specified, outputs the format data CLIXML instead of saving it to the session state
[string]$Destination
)
begin {
[List[Object]]$InputObjects = @()
#A deduplicated list of the types provided to format-table, for purposes of collecting
[HashSet[String]]$TypeNames = @()
#TODO: Support full groupby syntax
if ($GroupBy -and $GroupBy -isnot [string]) {
Write-Warning 'GroupBy currently can only take a single property, multiple properties or expressions are not currently supported.'
$GroupBy = $null
}
}
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) {
if (
$inputObjectItem.psobject.TypeNames -contains 'System.Management.Automation.PSCustomObject'
) {
if ($inputObjectItem.PSTypeNames.count -le 2) {
throw [NotSupportedException]'A PSCustomObject with more than one PSTypeName was detected. This is not supported and will likely cause formatting issues.'
}
if ($inputObjectItem.PSTypeNames.count -gt 3) {
throw [NotImplementedException]'A PSCustomObject with more than one PSTypeName was detected. This is not supported and will likely cause formatting issues.'
}
[void]$TypeNames.Add($inputObjectItem.PSTypeNames[0])
continue
}
[void]$TypeNames.Add($inputObjectItem.GetType().FullName)
}
#TODO: Gather calculated property information to attach as metadata
#Remove Extension Properties
foreach ($customParam in 'NoPersist', 'ViewName', 'Destination') {
[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.'
}
if ($GroupBy -and $ftResult[1].GetType().Name -ne 'GroupStartData') {
throw [InvalidDataException]'GroupBy was specified but no GroupStartData was found. This is probably a bug.'
}
#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
#This gets lost in translation from Format-Table to FormatStartData
Add-Member -InputObject $ftResult[0] -NotePropertyName 'GroupBy' -NotePropertyValue $GroupBy -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) {
$formatView = $ftResult
| ConvertFrom-Format @convertFromFormatParams
if ($Destination) {
ConvertTo-FormatXml -FormatDefinition $formatView -TypeName $Type -Destination $Destination
} else {
Add-Format -FormatViewDefinition $formatView -Type $Type -NoPersist:$NoPersist
}
}
return $ftResult
}
}
#endregion
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 'CustomTypeTableView
[ValidateNotNullOrEmpty()][string]$Name = 'CustomTypeTableView',
#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'
New-Variable -Name 'GroupData'
New-Variable -Name 'FormatEntryData'
}
process {
#FIXME: Redo this processing loop to capture the first formatdata/groupdata/etc. so less has to be attached via Add-Member
#HACK: This is a PowerShell internal type so we cannot use -is or reference the type directly
$InputObjectType = $InputObject.GetType().Name
#We are capturing the first of each type of header we encounter, for there is metadata we need stored there.
#This is a hacky form a type pattern matching
switch ($InputObjectType) {
'FormatStartData' {
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
break
}
'GroupStartData' {
if ($GroupData) { return }
$GroupData = $InputObject
return
}
'FormatEntryData' {
if ($FormatEntryData) { return }
$FormatEntryData = $InputObject
return
}
}
}
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.' }
[PSObject]$control = switch ($shapeInfo.GetType().Name) {
'TableHeaderInfo' {
$tcProps = @{
CalculatedProperties = $formatStartData.CalculatedProperties #Custom added property
AutoSize = $null -ne $FormatStartData.autoSizeInfo
Wrap = $formatStartData.Wrap #Custom added property
GroupBy = $formatStartData.GroupBy #Custom added property
}
#FIXME: Nulls are emitting here, not sure why
New-TableControl @tcProps $shapeInfo
| Where-Object { $PSItem }
| Select-Object -First 1
}
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, $GroupBy) #Should be TableHeaderInfo
# Helpful class found here: https://github.com/PowerShell/PowerShell/blob/f69a4b542bac0082544f3a38b5e7a54274c26872/src/System.Management.Automation/FormatAndOutput/common/DisplayDatabase/displayDescriptionData_Table.cs#L546
[TableControlBuilder]$tcBuilder = [TableControl]::Create($false, $AutoSize, $TableHeaderInfo.hideHeader)
#FIXME: Continue using the tablecontrolbuilder to build the table control
[TableRowDefinitionBuilder]$row = $tcBuilder.StartRowDefinition($Wrap)
# #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 ($GroupBy) {
if ($GroupBy -is [string]) {
[void]$tcBuilder.GroupByProperty($GroupBy)
} else {
throw [System.NotImplementedException]'TODO:GroupBy with anything except a single string value is not yet implemented'
}
}
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
[void]$tcBuilder.AddHeader($columnInfo.alignment, $columnInfo.Width, $label)
if ($columnInfo.propertyName) {
if ($scriptBlock) {
[void]$row.AddScriptBlockColumn($scriptBlock, $columnInfo.alignment)
} else {
[void]$row.AddPropertyColumn($columnInfo.propertyName, $columnInfo.alignment)
}
}
}
return $row.EndRowDefinition().EndTable()
}
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]$Destination
)
begin {
[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 ($Destination) {
if (-not $Destination.EndsWith('.ps1xml')) {
Write-Warning "$Destination does not have a .ps1xml extension. It cannot be imported via Update-FormatData unless it has a .ps1xml extension."
}
Out-File -InputObject $xmlOutput -FilePath $Destination
return
}
return $xmlOutput
}
}
#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
Describe 'Format-TypeTable' {
BeforeAll {
Import-Module $PSScriptRoot/TypeFormat.psm1 -Force
}
Context 'Metadata' {
It 'Adds Type Metadata' {
$actual = Get-ChildItem -File $PSScriptRoot/TypeFormat.psm1
| Format-TypeTable -NoPersist
$actual[0].TypeNames | Should -Contain 'System.IO.FileInfo'
}
It 'Adds Property Metadata (<Name>)' {
$actual = Get-ChildItem -File $PSScriptRoot/TypeFormat.psm1
| Format-TypeTable -NoPersist -Property $Property, Length
$actual[0].CalculatedProperties.Count | Should -HaveCount 1
$actual[0].CalculatedProperties['NamePlusBang'] | Should -BeOfType [ScriptBlock]
$actual[0].CalculatedProperties['NamePlusBang'].Ast.ToString() | Should -Be { $_.Name + '!' }.Ast.ToString()
} -TestCases @{
Name = 'N/E'
Property = @{
N = 'NamePlusBang'
E = { $_.Name + '!' }
}
},
@{
Name = 'Name/E'
Property = @{
Name = 'NamePlusBang'
E = { $_.Name + '!' }
}
},
@{
Name = 'N/Expression'
Property = @{
N = 'NamePlusBang'
Expression = { $_.Name + '!' }
}
},
@{
Name = 'Name/Expression'
Property = @{
Name = 'NamePlusBang'
Expression = { $_.Name + '!' }
}
}
}
Context 'New Type' {
BeforeAll {
class ___TypeFormatTestType {
[string] $ShownProperty
[string] $HiddenProperty
[string] $CustomViewProperty
}
$SCRIPT:testType = [___TypeFormatTestType]@{
ShownProperty = 'Shown'
HiddenProperty = 'Hidden'
}
}
AfterEach {
RemoveFormatType '___TypeFormatTestType'
}
It 'Specific Properties' {
$testType
| Format-TypeTable ShownProperty
# Check that the default formatting looks as expected.
($testType | Format-Table)[0].shapeInfo.tableColumnInfoList.label | Should -Be 'ShownProperty'
}
It 'Custom View Name' {
$testType
| Format-TypeTable ShownProperty, CustomViewProperty -ViewName PesterCustom
#Check that the custom format was loaded
([Runspace]::DefaultRunspace.InitialSessionState.Formats | Where-Object FormatData -Match 'TypeFormatTestType').FormatData.FormatViewDefinition.Name
# Check that the default formatting looks as expected when used with -View
($testType | Format-Table -View PesterCustom)[0].shapeInfo.tableColumnInfoList.label | Should -Be 'ShownProperty', 'CustomViewProperty'
($testType | Format-TypeTable -View PesterCustom)[0].shapeInfo.tableColumnInfoList.label | Should -Be 'ShownProperty', 'CustomViewProperty'
}
It 'Calculated Property' {
$testType
| Format-TypeTable ShownProperty, @{ N = 'HiddenPlusBang'; E = { $_.HiddenProperty + '!' } }
# Check that the default formatting looks as expected.
($testType | Format-Table)[0].shapeInfo.tableColumnInfoList.label | Should -Be 'ShownProperty', 'HiddenPlusBang'
}
It 'Multiple Calculated Properties' {
$testType
| Format-TypeTable ShownProperty, @{ N = 'HiddenPlusBang'; E = { $_.HiddenProperty + '!' } }, @{ N = 'HiddenPlusBangBang'; E = { $_.HiddenProperty + '!!' } }
# Check that the default formatting looks as expected.
($testType | Format-Table)[0].shapeInfo.tableColumnInfoList.label | Should -Be 'ShownProperty', 'HiddenPlusBang', 'HiddenPlusBangBang'
}
It 'HideTableHeaders' {
$testType
| Format-TypeTable ShownProperty -HideTableHeaders -ViewName 'PesterTableHeaderTest'
# Check that the default formatting looks as expected.
$RunspaceFormats[-1].FormatData.FormatViewDefinition[0].Name | Should -Be 'PesterTableHeaderTest'
$RunspaceFormats[-1].FormatData.FormatViewDefinition[0].Control.HideTableHeaders | Should -BeTrue
}
It 'AutoSize' {
$testType
| Format-TypeTable -AutoSize ShownProperty -ViewName 'PesterTableAutosizeTest'
# Check that the default formatting looks as expected.
$RunspaceFormats[-1].FormatData.FormatViewDefinition[0].Name | Should -Be 'PesterTableAutosizeTest'
$RunspaceFormats[-1].FormatData.FormatViewDefinition[0].Control.AutoSize | Should -BeTrue
}
It 'Wrap' {
$testType
| Format-TypeTable -Wrap ShownProperty -ViewName 'PesterTableWrapTest'
# Check that the default formatting looks as expected.
$RunspaceFormats[-1].FormatData.FormatViewDefinition[0].Name | Should -Be 'PesterTableWrapTest'
$RunspaceFormats[-1].FormatData.FormatViewDefinition[0].Control.Rows.Wrap | Should -BeTrue
}
It 'GroupBy' {
$testType
| Format-TypeTable -GroupBy ShownProperty -ViewName 'PesterGroupByTest'
# Check that the default formatting looks as expected.
$RunspaceFormats[-1].FormatData.FormatViewDefinition[0].Name | Should -Be 'PesterGroupByTest'
$RunspaceFormats[-1].FormatData.FormatViewDefinition[0].Control.GroupBy.Expression.Value
| Should -Be 'ShownProperty'
}
It 'NoPersist' {
$testType
| Format-TypeTable ShownProperty -NoPersist -ViewName 'PesterNoPersistTest'
# Check that the default formatting looks as expected.
$RunspaceFormats[-1].FormatData | Should -BeNullOrEmpty
}
It 'Errors on Raw PSCustomObject' {
{
[pscustomobject]@{ ShownProperty = 'Shown' }
| Format-TypeTable ShownProperty
}
| Should -Throw -ExceptionType ([NotSupportedException])
}
It 'Errors on Multiple PSCustomObject Type Names' {
{
[pscustomobject]@{ ShownProperty = 'Shown'; PSTypeName = 'One' }
| ForEach-Object { $_.PSTypeNames.Insert(0, 'Two'); $_ }
| Format-TypeTable ShownProperty
}
| Should -Throw -ExceptionType ([NotImplementedException])
}
}
Context 'Existing Type' {
AfterEach {
#Cleanup
if ('System.IO.FileInfo' -eq $RunspaceFormats[0].FormatData) {
$RunspaceFormats.RemoveItem(0)
Update-FormatData
}
}
It 'Prepends on Existing Type' {
(Get-Item $PSScriptRoot/TypeFormat.tests.ps1)
| Format-TypeTable Name, Length
$RunspaceFormats[0].FormatData.TypeName | Should -Be 'System.IO.FileInfo'
$RunspaceFormats[0].FormatData.FormatViewDefinition[0].Name | Should -Be 'CustomTypeTableView'
}
}
}
#Utility Functions
BeforeAll {
$SCRIPT:RunspaceFormats = [Runspace]::DefaultRunspace.InitialSessionState.Formats
function RemoveFormatType ($Type) {
#Cleanup the format data
$formats = [Runspace]::DefaultRunspace.InitialSessionState.Formats
$i = -1
$formats
| Where-Object {
$i++
$_.FormatData -Match $Type
}
| Select-Object -First 1
| Out-Null
$formats.RemoveItem($i)
Update-FormatData
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment