TB

MoppleIT Tech Blog

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

Clear, Consistent Console Output with PSStyle: Color-Safe Status Lines for Faster Reviews

You can speed up code reviews, reduce mistakes, and make incident response calmer by standardizing your console output. With PowerShell 7+ you get $PSStyle, a first-class way to use ANSI styles safely, consistently, and cross‑platform. In this guide youll build a small status printer that highlights the signal, falls back to plain text when color isnt available, and keeps messages short and scannable.

Why consistency and color-safety matter

  • Faster scanning: Standard tags like [OK], [WARN], and [ERR] let reviewers jump to what matters.
  • Portable logs: Not every environment supports ANSI. Color-safe output avoids escape garbage in CI logs and transcripts.
  • Actionable messages: Short, consistent phrasing reduces cognitive load and supports automated parsing.
  • Lower friction in automation: Uniform output makes it easier to grep, gate, and analyze logs.

PSStyle quick primer

PowerShell 7 introduced $PSStyle for ANSI styling. Key pieces youll use:

  • $PSStyle.Foreground.(Color): e.g., Green, Yellow, Red, Blue, etc.
  • $PSStyle.Background.(Color), $PSStyle.Bold, $PSStyle.Underline, and $PSStyle.Reset for resetting styles.
  • $PSStyle.OutputRendering: Controls whether styles are rendered. Typical values include Ansi, PlainText, and Host. If its PlainText, you should avoid emitting ANSI codes.

By respecting $PSStyle.OutputRendering, you can produce attractive color in terminals and clean, readable plain text in CI systems or when output is redirected.

Reference implementation: color-safe status printer

The function below prints a status tag with color when safe, and falls back to plain text otherwise. It returns a string to the pipeline so you can pipe to Tee-Object, redirect to files, or use it inside scripts without mixing host-only output.

# Color-safe status printer (PowerShell 7+; falls back when color is not available)
$useColor = ($PSStyle -and $PSStyle.OutputRendering -ne 'PlainText')

function Write-Status {
  param(
    [ValidateSet('OK','WARN','ERR')]
    [string]$Level,
    [Parameter(Mandatory)]
    [string]$Message
  )
  if ($useColor) {
    switch ($Level) {
      'OK'   { $tag = "$( $PSStyle.Foreground.Green )[OK]$( $PSStyle.Reset )" }
      'WARN' { $tag = "$( $PSStyle.Foreground.Yellow )[WARN]$( $PSStyle.Reset )" }
      'ERR'  { $tag = "$( $PSStyle.Foreground.Red )[ERR]$( $PSStyle.Reset )" }
    }
  } else {
    switch ($Level) {
      'OK'   { $tag = '[OK]' }
      'WARN' { $tag = '[WARN]' }
      'ERR'  { $tag = '[ERR]' }
    }
  }
  "{0} {1}" -f $tag, $Message
}

Write-Status -Level OK   -Message 'Checks passed'
Write-Status -Level WARN -Message 'Retrying download'
Write-Status -Level ERR  -Message 'Config missing'

Guidelines:

  • Keep labels short: [OK], [WARN], [ERR] are easy to scan and type.
  • Keep messages actionable: Prefer 22Retrying download22 over 22Something failed22.
  • Return strings, dont write to host: This keeps output composable and pipeline-friendly.

Pipeline-friendly and host-friendly

Because Write-Status returns strings, you can easily send output to both the screen and a log file:

Write-Status OK 'Checks passed' | Tee-Object -FilePath build.log

If you want a host-only variant for interactive sessions, wrap it:

function Show-Status {
  param(
    [ValidateSet('OK','WARN','ERR')]$Level,
    [Parameter(Mandatory)][string]$Message
  )
  Write-Host (Write-Status $Level $Message)
}

Alignment, timestamps, and context

Regular structure helps scanning. Add timestamps and component names, and keep tags a fixed width so messages line up:

$useColor = ($PSStyle -and $PSStyle.OutputRendering -ne 'PlainText')

function New-StatusLine {
  param(
    [ValidateSet('OK','WARN','ERR')][string]$Level,
    [Parameter(Mandatory)][string]$Message,
    [string]$Component = 'app',
    [switch]$Stamp
  )
  $time = if ($Stamp) { (Get-Date).ToString('HH:mm:ss') } else { '' }
  if ($useColor) {
    $tag = switch ($Level) {
      'OK'   { "$( $PSStyle.Foreground.Green )[OK ]$( $PSStyle.Reset )" }
      'WARN' { "$( $PSStyle.Foreground.Yellow )[WARN]$( $PSStyle.Reset )" }
      'ERR'  { "$( $PSStyle.Foreground.Red )[ERR ]$( $PSStyle.Reset )" }
    }
    $comp = "$($PSStyle.Foreground.Blue)$Component$($PSStyle.Reset)"
  } else {
    $tag = switch ($Level) {
      'OK'   { '[OK ]' }
      'WARN' { '[WARN]' }
      'ERR'  { '[ERR ]' }
    }
    $comp = $Component
  }
  # Format: [HH:mm:ss] [TAG] component - message
  if ($Stamp) { "[$time] $tag $comp - $Message" } else { "$tag $comp - $Message" }
}

New-StatusLine OK   'Checks passed' -Component 'build' -Stamp
New-StatusLine WARN 'Retrying download' -Component 'fetch'
New-StatusLine ERR  'Config missing: db.conn' -Component 'init' -Stamp

Notes:

  • Fixed-width tags: Padding [OK ] and [ERR ] to five characters aligns messages.
  • Component names: Show the subsystem responsible (build, api, db) to speed triage.
  • Short, precise messages: Include the actionable detail, e.g. the missing key.

Honor NO_COLOR and CI defaults

Many teams use the NO_COLOR convention to disable ANSI in tools. Likewise, CI systems often prefer plain text. You can set a default at script start and still let users override:

# Opt into plain text if NO_COLOR is set, or in CI by default
if ($env:NO_COLOR -or $env:CI) { $PSStyle.OutputRendering = 'PlainText' }

# Allow force-enable with an environment override
if ($env:FORCE_COLOR) { $PSStyle.OutputRendering = 'Ansi' }

Because your functions already check $PSStyle.OutputRendering, everything downstream 22just works22.

Real-world usage patterns

Local scripts

# build.ps1
New-StatusLine OK 'Node modules restored' -Component 'deps' -Stamp
New-StatusLine WARN 'Retrying test run (1/3)' -Component 'test'
if (-not (Test-Path .\appsettings.json)) {
  New-StatusLine ERR 'Config missing: appsettings.json' -Component 'init' -Stamp | Tee-Object -File build.log
  exit 1
}

CI/CD pipelines

Prefer plain text to keep logs readable and searchable:

# GitHub Actions example (pwsh)
- name: Build
  shell: pwsh
  run: |
    $PSStyle.OutputRendering = 'PlainText'
    . .\scripts\status.ps1
    New-StatusLine OK 'Restore complete' -Component 'deps' -Stamp | Tee-Object build.log -Append
    New-StatusLine WARN 'Slow network; extending timeout' -Component 'net'

Structured logs (JSON) alongside human-readable output

You can emit human-readable lines for the console and JSON for machines, without duplicating message text:

$line = New-StatusLine OK 'Payment service healthy' -Component 'health' -Stamp
$line | Tee-Object -FilePath console.log -Append
[pscustomobject]@{
  ts        = (Get-Date).ToString('o')
  level     = 'OK'
  component = 'health'
  message   = 'Payment service healthy'
} | ConvertTo-Json -Compress | Add-Content -Path events.jsonl

Security, performance, and maintainability tips

  • Never log secrets: Redact tokens, keys, and passwords. Centralize logging in a helper that redacts known patterns.
  • Bounded length: Keep messages under ~120 characters so they fit common terminals and wrap less in CI UIs.
  • Use ValidateSet for levels: This prevents typos and enforces consistency.
  • Prefer returning strings: Avoid Write-Host in core functions to keep output composable and testable.
  • Feature flags via env: Support NO_COLOR, FORCE_COLOR, or a custom LOG_LEVEL to drop [OK] noise in noisy environments.
  • Standardize component names: Keep them short (312 chars), consistent, and documented in your repo27s CONTRIBUTING guide.

Quick tests with Pester

Lock in consistency with lightweight tests:

Describe 'New-StatusLine' {
  It 'emits WARN tag in plain text' {
    $PSStyle.OutputRendering = 'PlainText'
    $out = New-StatusLine WARN 'Retrying' -Component 'net'
    $out | Should -Match '^\[WARN\]\s+net\s+-\s+Retrying$'
  }
  It 'emits ERR in red when ANSI is on' {
    $PSStyle.OutputRendering = 'Ansi'
    $out = New-StatusLine ERR 'Config missing' -Component 'init'
    $out | Should -Match "\x1b\["  # contains ANSI
  }
}

Putting it all together

  1. Adopt short, standard tags: [OK], [WARN], [ERR].
  2. Respect $PSStyle.OutputRendering and NO_COLOR to keep logs clean.
  3. Add timestamps and component labels for fast triage.
  4. Return strings to keep output pipeline-friendly and testable.
  5. Back your conventions with a tiny Pester suite.

With a small helper and a few guardrails, your scripts will produce clear, consistent, and color-safe output26mdash;accelerating reviews and reducing mistakes.

Further reading: PowerShell Advanced CookBook 26ndash; Take your console UX further in PowerShell 26rarr; https://www.amazon.com/PowerShell-Advanced-Cookbook-scripting-advanced-ebook/dp/B0D5CPP2CQ/

← All Posts Home →