More ANSI Alternatives
I've been demonstrating how to use ANSI to display messages in the console while your PowerShell code is executing. What I've shown you should be considered a proof of concept more than anything. Expect a lot of trial and error to get the desired output. My ideas will work best if your command doesn't write anything to the pipeline, or if it sends output after all of your ANSI-based messaging is complete. But that probably is wishful thinking on my part.
Before we get to today's content, I want to point out a subtle distinction in clearing the console. In the previous article, I mentioned you could use this ANSI sequence to clear the display.
"e[1J"
You may wonder what the difference is between this and using Clear-Host
. It depends on where the cursor is when you clear the screen. If you are at the bottom of the console, using Clear-Host
will scroll the console up, leaving the cursor at the top. Using the ANSI sequence will clear the screen and leave the cursor at the bottom. You can try these two commands to see the difference.
Get-Process ; Start-Sleep -milliseconds 500; Clear-Host; Write-Host "I am here"
Get-Process ; Start-Sleep -milliseconds 500; "`e[1J"; Write-Host "I am here"
Notice where the message is displayed? This is a minor point, but it might be important in your script. This should make it clear you need to test extensively when using ANSI sequences in your code. You might also want to add restrictions or tests to ensure your code is running in a supported environment.
I also showed you in the last article a function that displayed status messages using ANSI escape sequences at the bottom of the screen. I was using Write-Host
to display a string using a private function.
Function Status {
Param([string]$Message)
#$Row is found in the parent scope
Write-Host "`e[$Row;1H`e[2K`e[93;1;3mSTATUS: $Message`e[0m"
Start-Sleep -Milliseconds 250
}
Private functions do not need to follow the Verb-Noun naming convention.
The downside to this approach is that I am not using the Verbose
stream. This means I don't get the VERBOSE
indicator. And if I am redirecting streams, the Status function can't be redirected as Verbose
output.
However, with a little tweaking, I can still use the Verbose
stream. I will define a proxy function in my script for Write-Verbose
.
Function Write-Verbose {
Param([string]$Message)
If ($VerbosePreference -eq 'SilentlyContinue') { Return }
#$Row is found in the parent scope
#invoke the native Write-Verbose cmdlet to write the message
Write-Host "`e[$Row;1H`e[2K`e[1A" ; Microsoft.PowerShell.Utility\Write-Verbose "`e[93;1;3m $Message`e[0m"
#artificially pause for demo purposes
Start-Sleep -Milliseconds 250
}
My function still uses Write-Verbose
but it invokes the proxy function which in turn calls the native Write-Verbose
cmdlet using the command's fully qualified name to display the ANSI-formatted string.
Microsoft.PowerShell.Utility\Write-Verbose "`e[93;1;3m $Message`e[0m"
The result is that I get the verbose output where I want it.

Here's the complete script file that generated the output in Figure 1.
#requires -version 7.4
#requires -module Microsoft.PowerShell.PSResourceGet
#requires -module Microsoft.PowerShell.Utility
#This version uses ANSI-displayed Verbose messages
#The function has artificial pauses using Start-Sleep to simulate processing long
#running code and to allow Verbose messages to be displayed
Function Get-ModuleInfo {
[cmdletbinding()]
[OutputType('psModuleInfo')]
Param(
[Parameter(
Position = 0,
ValueFromPipeline,
ValueFromPipelineByPropertyName,
HelpMessage = 'Specify the module name to get information'
)]
[ValidateNotNullOrEmpty()]
[SupportsWildcards()]
[string]$Name = '*'
)
Begin {
#define a private proxy function to write the status messages
Function Write-Verbose {
Param([string]$Message)
If ($VerbosePreference -eq 'SilentlyContinue') { Return }
#$Row is found in the parent scope
#invoke the native Write-Verbose cmdlet to write the message
Write-Host "`e[$Row;1H`e[2K`e[1A" && Microsoft.PowerShell.Utility\Write-Verbose "`e[93;1;3m $Message`e[0m"
#artificially pause for demo purposes
Start-Sleep -Milliseconds 250
}
Clear-Host
#get current cursor position
$pos = $host.UI.RawUI.CursorPosition
$Row = $host.UI.RawUI.WindowSize.Height - 1
Write-Verbose "[$((Get-Date).TimeOfDay) BEGIN ] Starting $($MyInvocation.MyCommand)"
Write-Verbose "[$((Get-Date).TimeOfDay) BEGIN ] Running under PowerShell version $($PSVersionTable.PSVersion)"
} #begin
Process {
$Modules = Get-Module -Name $Name
foreach ($Module in $Modules) {
#using variables for the status messages in the Process block in case
#a message has to be re-written after scrolling up
$msg = "[$((Get-Date).TimeOfDay) PROCESS] Processing $($Module.Name) version $($Module.Version)"
Write-Verbose $msg
$msg = "[$((Get-Date).TimeOfDay) PROCESS] Getting $($Module.Name) module size"
Write-Verbose $msg
$measure = Get-ChildItem $Module.ModuleBase -Recurse -File | Measure-Object -Property Length -Sum
$msg = "[$((Get-Date).TimeOfDay) PROCESS] Getting PSGallery information for $($Module.Name)"
Write-Verbose $msg
Try {
$online = Find-PSResource -Name $Module.Name -Repository PSGallery -ErrorAction Stop
$LastUpdate = $online.PublishedDate
$PSGalleryVersion = $online.Version
}
Catch {
$LastUpdate = $Null
$PSGalleryVersion = $Null
}
if ($Module.ModuleBase -match $env:Username) {
$ModuleScope = 'User'
}
else {
$ModuleScope = 'AllUsers'
}
#Wipe the last status message so it doesn't get scrolled yup
If ($VerbosePreference -eq 'Continue') {
Write-Host "`e[$Row;1H`e[2K"
}
#set the cursor position to write the output
if ($verbosePreference -eq 'Continue') {
$host.UI.RawUI.CursorPosition = $pos
}
[PSCustomObject]@{
PSTypeName = 'psModuleInfo'
Name = $Module.Name
Author = $Module.Author
Version = $Module.Version
PSGalleryVersion = $PSGalleryVersion
PSGalleryUpdate = $LastUpdate
Installed = (Get-Item $Module.ModuleBase).CreationTime
Scope = $moduleScope
PSEdition = $Module.CompatiblePSEditions
Commands = $Module.ExportedCommands.Count
Files = $measure.Count
Size = $measure.sum
Tags = $module.Tags
Path = $Module.ModuleBase
ComputerName = [System.Environment]::MachineName
}
#save cursor position after writing the output
If ($VerbosePreference -eq 'Continue') {
$pos = $host.UI.RawUI.CursorPosition
#Start-Sleep -Milliseconds 500
}
} #foreach
} #process
End {
Write-Verbose "[$((Get-Date).TimeOfDay) END ] Ending $($MyInvocation.MyCommand)"
#clear the last Status message
if ($VerbosePreference -eq 'Continue') {
Write-Host "`e[$Row;1H`e[2K"
}
} #end
} #close Get-ModuleInfo
Summary
My code samples should be treated as proof of concept. They are intended to demonstrate concepts and techniques. You will need to test and adapt them to your environment and requirements. I am not implying that you should use these ideas in your code. The standard Verbose stream is perfectly fine for most situations. But if you need to get creative or have a specific use case, the ideas from the last two articles might be of help. If you write something using these ideas, I hope you'll share a link to your code.
I have one more ANSI-related topic I want to cover which I'll do in the next article. In the meantime, grab the samples and try this out. I'll look for your questions in the comments.