Friendly Output: Colors and Progress
Write-Progress shows a progress bar while a long job runs, so nobody wonders whether it's stuck, and $PSStyle colors make the important lines stand out on the screen, as long as the colors stay in what you show and never go into the objects a tool hands back.
Journeyman · PowerShell · Updated
After this lesson you can
- Show a progress bar for a long job with Write-Progress, and close it with -Completed
- Color words for the screen with $PSStyle and Write-Host, and keep colors out of the objects a tool hands back
Key takeaways
Write-Progress -Activity -Status -PercentCompleteshows a bar while a job runs, and-Completedcloses it.- Progress bars and
Write-Hostlines are for the screen: the pipeline and>never get them. $PSStyle.Foreground.Greenstarts a color and$PSStyle.Resetends it.- A color written into a property travels with it: the name no longer matches, and the codes end up in a spreadsheet.
- Objects carry plain values; a Show- command at the end of the line adds the color.
You start an import of three hundred old recipe cards, and the cursor sits there blinking. Is it working? Is it stuck? Should you press Ctrl+C? A minute later it finishes, and prints three hundred lines in one plain color, so the quick weeknight dinners you were looking for are lost in the list. Apps you use every day answer both problems: a progress bar while they work, and color for what matters. Your tool can have both, as long as the color stays on the screen.
This lesson adds two commands to RecipeBox.ps1 from Where a Tool Keeps Its Data. Make a folder of pretend old cards first, each with a name, minutes and a category on three lines:
$cards = "$HOME\terminal-practice\recipe-box\old-cards"
New-Item -ItemType Directory -Force -Path $cards | Out-Null
Set-Content "$cards\card-01.txt" -Value 'Black Bean Tacos', 25, 'Dinner'
Set-Content "$cards\card-02.txt" -Value 'Banana Bread', 70, 'Baking'
Set-Content "$cards\card-03.txt" -Value 'Tomato Soup', 35, 'Lunch'
Set-Content "$cards\card-04.txt" -Value 'Pancakes', 20, 'Breakfast'
Set-Content "$cards\card-05.txt" -Value 'Fruit Salad', 15, 'Snack'
Is it stuck? `Write-Progress`
A progress bar is the "now boarding, 3 of 5" board at the gate: it says what's happening and how far along it is. Write-Progress draws one. -Activity names the job, -Status says what it's on now, and -PercentComplete is how far along it is, from 0 to 100. Add this function to RecipeBox.ps1:
function Import-RecipeCard {
param([string]$Path)
$cards = @(Get-ChildItem $Path -Filter *.txt)
$done = 0
foreach ($card in $cards) {
$percent = $done / $cards.Count * 100
Write-Progress -Activity 'Importing recipe cards' -Status $card.Name -PercentComplete $percent
$lines = Get-Content $card.FullName
Add-Recipe -Name $lines[0] -Minutes $lines[1] -Category $lines[2]
Start-Sleep -Milliseconds 500 # stands in for a slow job
$done++
}
Write-Progress -Activity 'Importing recipe cards' -Completed
}
Five cards take no time at all; three hundred cards, or a folder on a slow network drive, do. Start-Sleep pauses half a second per card so you can watch the bar. The last line, with -Completed, closes the bar; without it, the bar can stay on the screen until the whole script ends.
Load the file again and import the cards. Each recipe the import adds is a result, so keep them in a variable:
. .\RecipeBox.ps1
$imported = Import-RecipeCard .\old-cards
$imported.Count
5
While it runs, a bar fills across the window with Importing recipe cards and the card it's on:
The bar is for the screen only, like Write-Host in Where Output Goes: Write-Output, Write-Host and Friends: the pipeline and > never get it, so $imported holds the five recipes and nothing else. In Windows PowerShell 5.1 the bar is a larger block across the top of the window; it works the same way.
Colors for the screen: `$PSStyle`
$PSStyle, from A Prompt in Your Colors, names the codes that switch a terminal's color: $PSStyle.Foreground.Green starts green, and $PSStyle.Reset (read "reset") goes back to normal. Add a command that shows the box for people, with every recipe of 20 minutes or less in green:
function Show-Recipe {
foreach ($recipe in Get-Recipe) {
$line = "$($recipe.Name), $($recipe.Minutes) min, $($recipe.Category)"
if ($recipe.Minutes -le 20) {
$line = "$($PSStyle.Foreground.Green)$line$($PSStyle.Reset)"
}
Write-Host $line
}
}
. .\RecipeBox.ps1
Show-Recipe
Lemon Garlic Pasta, 30 min, Dinner
Overnight Oats, 10 min, Breakfast
Black Bean Tacos, 25 min, Dinner
Banana Bread, 70 min, Baking
Tomato Soup, 35 min, Lunch
Pancakes, 20 min, Breakfast
Fruit Salad, 15 min, Snack
On your screen, the oats, the pancakes and the fruit salad are green. Forget $PSStyle.Reset, and every line after the first quick one turns green too. Windows PowerShell 5.1 has no $PSStyle; there, write Write-Host $line -ForegroundColor Green.
Never in the objects
It's tempting to color the name inside Get-Recipe itself, so the green shows wherever the recipes go. Here's what that does to one recipe:
$oats = [PSCustomObject]@{ Name = "$($PSStyle.Foreground.Green)Overnight Oats$($PSStyle.Reset)"; Minutes = 10; Category = 'Breakfast' }
$oats.Name.Length
@($oats | Where-Object Name -eq 'Overnight Oats').Count
23
0
The color codes are hidden characters, ANSI escape codes, and they're part of the name now. Overnight Oats is 14 characters, but this name is 23, so a search for it finds nothing. Export it, and the CSV file gets the codes too, so a spreadsheet shows [32m and [0m around the name.
So keep the two jobs apart. Get-Recipe hands back plain objects that sort, filter and save cleanly. Show-Recipe is the end of the line: it writes to the screen with Write-Host and hands nothing on, so $shown = Show-Recipe prints the list and leaves $shown empty. Color what people read; never color what commands read.
Check yourself
2 questions to try on the lesson page.
So what
A bar that moves and a color that points the way make a tool pleasant to use; keeping them on the screen, out of the objects, keeps it useful to every command that comes after.