TB

MoppleIT Tech Blog

Welcome to my personal blog where I share thoughts, ideas, and experiences.

Cleaner PowerShell Output with Default Display Sets: Safer Logs, Faster Reviews

Console output is your product when you demo, debug, and review automation. If every object dumps dozens of columns by default, you get noisy tables, slow scrollback, and accidental leaks of things you never meant to show. The fix is simple and scalable: attach a custom type name to your objects and set a default display property set so PowerShell prints only the signal you intend.

Why Default Display Sets Matter

PowerShell7s formatting system decides what to print when you emit objects without explicit formatting cmdlets. By defining a DefaultDisplayPropertySet for your object type, you control the default columns in table views. That gives you:

  • Cleaner tables: consistent, compact columns that fit the screen.
  • Faster reviews: pull requests and CI logs show only what matters.
  • Safer logs: sensitive fields remain in the object but stay off the screen.
  • Better UX: teams see stable, predictable output across scripts and sessions.

Here7s a minimal example that demonstrates the technique.

$items = 1..3 | ForEach-Object {
  [pscustomobject]@{
    PSTypeName = 'Demo.Item'
    Id    = $_
    Name  = ("Item{0}" -f $_)
    Secret = ('token-{0}' -f (Get-Random -Minimum 1000 -Maximum 9999))
  }
}

Update-TypeData -TypeName 'Demo.Item' -DefaultDisplayPropertySet 'Id','Name' -Force

# Default view shows only Id and Name
$items
# Full data is still available when needed
$items | Select-Object * | Format-Table -AutoSize

By default, you see just Id and Name in a tidy table. When you explicitly select or format all fields, you can still access Secret for debugging or downstream processing.

How It Works

1) Attach a custom type name

PowerShell7s formatting engine looks up display rules by type name. For PSCustomObject, add a PSTypeName in the hashtable literal or via Add-Member:

# Inlined via hashtable key
[pscustomobject]@{
  PSTypeName = 'Demo.Item'
  Id = 42
  Name = 'Item42'
  Secret = 'token-1234'
}

# Or after the fact
$obj = [pscustomobject]@{ Id = 42; Name = 'Item42'; Secret = 'token-1234' }
$obj.PSObject.TypeNames.Insert(0, 'Demo.Item')

Use a clear namespace (e.g., Contoso.Inventory.Item) to avoid collisions.

2) Define the default display property set

Update-TypeData registers type metadata for your session (or module) without XML files:

Update-TypeData -TypeName 'Demo.Item' `
  -DefaultDisplayPropertySet 'Id','Name' `
  -Force

From now on, whenever a Demo.Item hits the pipeline without explicit Format-*, PowerShell prints only Id and Name.

3) Keep sensitive fields off screen (but available)

Hiding a property from the default display doesn7t remove it. This is ideal for secrets, tokens, or oversized blobs. Still, remember: if you pipe to Export-Csv or ConvertTo-Json, those fields will be exported unless you exclude them.

Practical Patterns and Tips

Pattern: Redact instead of reveal

You may want to show that a secret exists without dumping its value. Add a computed, masked property and include it in the default set:

Update-TypeData -TypeName 'Demo.Item' -MemberType ScriptProperty `
  -MemberName SecretMasked `
  -Value { if ($this.Secret) { ($this.Secret.Substring(0,[math]::Min(3,$this.Secret.Length))) + '***' } } -Force

Update-TypeData -TypeName 'Demo.Item' `
  -DefaultDisplayPropertySet 'Id','Name','SecretMasked' -Force

Now reviewers see a hint (e.g., tok***) without the full token.

Pattern: Default key property set

If your objects have a natural key, add it to the default key property set for better formatting and grouping behaviors:

Update-TypeData -TypeName 'Demo.Item' -DefaultKeyPropertySet 'Id' -Force

Pattern: Per-module persistence

Update-TypeData changes are session-scoped unless you ship them with your module. Persist the rules in a .types.ps1xml file and declare it in your module manifest.

  1. Create MyModule.Types.ps1xml:
<Types>
  <Type>
    <Name>Demo.Item</Name>
    <Members>
      <ScriptProperty>
        <Name>SecretMasked</Name>
        <GetScriptBlock>
          if ($this.Secret) { ($this.Secret.Substring(0,[math]::Min(3,$this.Secret.Length))) + '***' }
        </GetScriptBlock>
      </ScriptProperty>
    </Members>
    <DefaultDisplayPropertySet>
      <ReferenceName>Id</ReferenceName>
      <ReferenceName>Name</ReferenceName>
      <ReferenceName>SecretMasked</ReferenceName>
    </DefaultDisplayPropertySet>
    <DefaultKeyPropertySet>
      <ReferenceName>Id</ReferenceName>
    </DefaultKeyPropertySet>
  </Type>
</Types>
  1. In your .psd1 manifest, reference it:
@{
  RootModule = 'MyModule.psm1'
  ModuleVersion = '1.0.0'
  GUID = '00000000-0000-0000-0000-000000000000'
  TypesToProcess = @('MyModule.Types.ps1xml')
}

Now the moment your module loads, your objects display consistently on every machine and in CI.

Pattern: Keep output stable across commands

  • Don7t rely on property discovery. Define the default set and document it.
  • Favor short, fixed-width columns for console views. Defer details to explicit Select-Object * or separate commands (e.g., Get-ItemDetails).
  • When logging, explicitly select properties you want to persist to disk, excluding secrets.
# Safe logging
$items |
  Select-Object Id, Name, @{n='LoggedAt';e={[datetime]::UtcNow}} |
  Export-Csv .\items-log.csv -NoTypeInformation

End-to-End Example

Let7s build a small end-to-end script that produces clean default output, supports a detailed view, and logs a safe subset.

# Domain object creation
function New-DemoItem {
  param(
    [Parameter(Mandatory)] [int]$Id,
    [Parameter(Mandatory)] [string]$Name
  )
  [pscustomobject]@{
    PSTypeName = 'Demo.Item'
    Id     = $Id
    Name   = $Name
    Secret = 'token-' + (Get-Random -Minimum 100000 -Maximum 999999)
    Tags   = @('alpha','beta')
    Owner  = $env:USERNAME
  }
}

# Type rules (session-scoped; move to .types.ps1xml for modules)
Update-TypeData -TypeName 'Demo.Item' -MemberType ScriptProperty -MemberName SecretMasked -Value {
  if ($this.Secret) { ($this.Secret.Substring(0,3)) + '***' }
} -Force
Update-TypeData -TypeName 'Demo.Item' -DefaultDisplayPropertySet 'Id','Name','SecretMasked' -Force
Update-TypeData -TypeName 'Demo.Item' -DefaultKeyPropertySet 'Id' -Force

# Produce some items
$items = 1..5 | ForEach-Object { New-DemoItem -Id $_ -Name ("Item{0}" -f $_) }

# Clean default view (Id, Name, SecretMasked)
$items

# Detailed view on demand
$items | Select-Object * | Format-List

# Safe log: never include Secret
$items |
  Select-Object Id, Name, Owner, Tags, @{n='LoggedAtUtc';e={[DateTime]::UtcNow}} |
  Export-Csv .\demo-items.csv -NoTypeInformation

# Use in CI: emit compact console output first, then attach detailed artifact file
$items | Out-File .\demo-items-detailed.txt

Result: your console shows a stable, compact summary for quick review. When you need deep inspection, detailed artifacts or explicit selection reveal everything without changing the default UX.

Common Pitfalls and How to Avoid Them

1) Expecting masking to secure secrets

Default display is not a security boundary. If you serialize objects, hidden properties will be included unless you filter them out. Use secret vaults, secure strings where appropriate, and redact at log time.

2) Losing the custom type after selection

Select-Object can transform objects into Selected.System.Management.Automation.PSObject, which may drop your custom type name. If you want to preserve formatting after selection, re-apply the type:

$selected = $items | Select-Object Id, Name
$selected | ForEach-Object { $_.PSObject.TypeNames.Insert(0,'Demo.Item'); $_ }
$selected # still uses Demo.Item display rules

3) Overriding default formatting inadvertently

Any explicit Format-Table or Format-List with property arguments overrides the default set. Let the engine use defaults unless you need a custom view for a specific command.

4) Colliding type names

Pick globally unique type names (e.g., use your module prefix or company domain) so that two modules don7t fight over the same name.

Operational Benefits in DevOps Workflows

  • Pull requests: reviewers see crisp, uniform tables instead of screen-wide dumps.
  • CI/CD logs: compact console output shortens logs and reduces truncation in hosted runners.
  • Incident response: default summaries speed triage; detailed exports keep deep data off the console and in artifacts.
  • Observability: when piping to structured sinks, explicitly select a safe schema to prevent PII/token leaks.

Quick Checklist

  • Define a unique PSTypeName for every domain object.
  • Use Update-TypeData to set -DefaultDisplayPropertySet and optionally -DefaultKeyPropertySet.
  • Redact sensitive fields or omit them from the default set.
  • Persist rules in .types.ps1xml and reference them in your module manifest.
  • For logs and exports, explicitly select fields and exclude secrets.

Format output with intent and your scripts become easier to read, safer to share, and quicker to review. For more advanced patterns and deep dives into the formatting and type system, read the PowerShell Advanced Cookbook: PowerShell Advanced Cookbook.

← All Posts Home →