PowerShell – Being Informative the Write Way

I recently gave a lunch and learn for using methods and techniques beyond the basics for a group of my colleagues.  During this seminar, I covered Write-Host, and why it shouldn’t be used in a script.  Beyond Don Jones saying that if you use Write-Host, God kills a puppy, there are very good reasons for using alternate methods.  I read through Mr. Snover’s blog post on the matter to gain more clarification before covering the topic myself.

Mr. Snover covers two scenarios in which a person may wish to use Write-Host.  They are:

  • For the purpose of conveying comforting information to the user.
  • For the purpose of conveying results.

I’m not one for teaching by PowerPoint, so I needed to find a good way to relay this information by example.  So I decided to show the differences between Write-Verbose, Write-Output, and Write-Host.  So let’s start by taking a look at the offending Write-Host, and why it shouldn’t be used in a script.

Write-Host "This is a text message"

Write-Host in itself seems harmless enough.  But whenever it is added to a script, it will always be executed regardless if the user cares about seeing your messages or not.  There is no switch to make these messages go away, so you’re always stuck with them.  Furthermore, if you take a closer look…

Write-Host "This is a text message" | Get-Member

You’ll get this return:

WriteWay2

Write-Host doesn’t even output a usable object.  This means that not only can you not get rid of the annoying messages, but you can’t even use it further in the pipe to add to a PSCustomObject output for reporting or any other purpose.

When we write a script, especially complex ones, we often feel we have an obligation to provide comforting information to the user (ie – “This script is going to do X”).  This is very helpful the first time you’re executing a script, but not something that they’ll care about often after that – unless something breaks of course.

Write-Verbose "This is a text message"

When you execute this as is, you get a blank return.  However, if you do this:

Write-Verbose "This is a text message" -Verbose

You get this:

WriteWay1

So now you have a message that you can relay to the user, and give them the choice of looking at it, or not, so long as you have the cmdletbinding function enabled.  There are a lot of reasons that you should probably do this regardless.

The caveat with Write-Verbose, of course, is that it’s not really an object.  So if you wanted to send the message to a file or Out-Gridview (one of my personal favorites), you’ll need to find a different approach.  That’s where Write-Output comes into play.

WriteWay3

Write-Output is for those times when you need to display information to the user, but you might want this information to be used further in the pipe as well.  So let’s see how this works:

Write-Output "This is text"

WriteWay4

Well that looks easy enough.  Just like a Write-Host.  But let’s do this:

Write-Output "This is text" | Get-Member

WriteWay5

Well now that’s different!

Where I find that Write-Output can be really useful is if I’m running a script where the potential output may be null.  In those instances, you don’t necessarily want to see nothing return.  A real world example of this is a script that I use to see what updates are pending on an SCCM client.  Using the CCM_SoftwareUpdate class in the ccm\ClientSDK namespace, I can query a machine and find out.

$ComputerName = "server01"
 FOREACH ($Computer in $ComputerName){
 get-wmiobject -ComputerName $Computer -Namespace root\ccm\ClientSDK -Class CCM_SoftwareUpdate | Select-Object * |
 ForEach-Object {
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"0","None");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"1","Available");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"2","Submitted");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"3","Detecting");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"4","PreDownload");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"5","Downloading");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"6","WaitInstall");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"7","Installing");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"8","PendingReboot");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"9","PendingReboot");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"10","PendingReboot");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"11","Verifying");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"12","InstallComplete");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"13","Error");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"14","WaitServiceWindow");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"15","WaitUserLogon");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"16","WaitUserLogoff");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"17","WaitJobUserLogon");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"18","WaitUserReconnect");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"19","PendingUserLogoff");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"20","PendingUpdate");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"21","WaitingRetry");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"22","WaitPresModeOff");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"23","WaitForOrchestration");
 return $PSItem;}

}#End FOREACH

When I run  this against a machine that has updates outstanding, I get a return like so:
WriteWay6

But when I run this against a fully compliant machine, I just get an empty return.  So instead, we modify our code with some logic to Write-Output in event of no updates found, and wrap that in a label that will be recognized :

$ComputerName = "SERVER01"
 FOREACH ($Computer in $ComputerName){
 $Update = 
 get-wmiobject -ComputerName $Computer -Namespace root\ccm\ClientSDK -Class CCM_SoftwareUpdate | Select-Object * |
 ForEach-Object {
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"0","None");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"1","Available");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"2","Submitted");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"3","Detecting");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"4","PreDownload");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"5","Downloading");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"6","WaitInstall");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"7","Installing");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"8","PendingReboot");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"9","PendingReboot");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"10","PendingReboot");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"11","Verifying");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"12","InstallComplete");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"13","Error");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"14","WaitServiceWindow");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"15","WaitUserLogon");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"16","WaitUserLogoff");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"17","WaitJobUserLogon");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"18","WaitUserReconnect");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"19","PendingUserLogoff");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"20","PendingUpdate");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"21","WaitingRetry");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"22","WaitPresModeOff");
 $PSItem.EvaluationState = [regex]::Replace($PSItem.EvaluationState,"23","WaitForOrchestration");
 return $PSItem;} | Select-Object -First 1
If ($Update.count -eq 0){$Update = Write-Output "$Computer does not have any updates available."
 [PSCustomObject]@{
 "Name" = $Update}
 }#EndPSCustomObject
Else {$Update}
}

Now when we run our script:

WriteWay7b

And that looks a lot better.  Better yet, we can array our machines, and send it to Out-Gridview and we have a nice little report with a full accounting of the list.

Write-Host allows you to do fun and cool things like change the color of the text and stuff.  But when we’re building tools, we’re looking to build something useful that requires as little intervention by the user as possible.  Leave the coloring and formatting to Word and Excel when you need it.