Pee-Object

When you don't want to cut off your PowerShell pipeline midstream just to print to console

Recently, while performing several different tasks using PowerShell, I found I had the desire to print intermediate results to the console.

This is, of course, easy to do.

My problem was that there isn’t an immediately obvious built-in way to do it that fits my style.

Let’s take the following example:

Get-PSDrive -PSProvider FileSystem | %{Get-ChildItem "$_`:`\" -Filter "*.txt" -Recurse -File -ErrorAction SilentlyContinue} | Measure-Object | select Count

This command will tell me the sum total of the .txt files on my local filesystem. It does so by first finding all drives. Then for each it finds the text files recursively and passes them all to Measure-Object to get the count.

What if you wanted to know the results of the Get-PSDrive call as you process each iteration of its results on the pipeline? One reason may be to double check the results are as you expect. While this example was contrived, I contrived it to match a need I had in a different script. I wanted a quick indication of my progress along the way. If you have several sizeable drives, it would be nice to know how far along you are instead of waiting for all the results at the end.

The very first way one might think to print the results of the first command would be like this:

$drives = Get-PSDrive -PSProvider FileSystem
Write-Host $drives
$drives | %{Get-ChildItem "$_`:`\" -Filter "*.txt" -Recurse -File -ErrorAction SilentlyContinue} | Measure-Object | select Count

That’s fine if it works for you. It doesn’t for me for a couple reasons.

First, that solution prints all of the results of Get-PSDrive immediately, which could be good for debugging purposes, but not for progress purposes.

Second, one of the reasons I turn to PowerShell is doing quick-hit one-liners. Even if it takes me a few tries to get my task done, the workflow for iterating through a PowerShell one-liner is great. Hit the Up key, edit, and run it again. Up/edit/run isn’t an option if I have to break the work into multiple lines. I have the option of putting it into an external file, requiring me to switch back and forth between two windows to edit the code and run it. Or I can split it up into temporarily saved variables (making me decide and remember variable names) to try to just up/edit/run the current chunk of the problem.

You can fix the order of operations to get progress, and resolve my multi-line gripe by packing it together this way:

Get-PSDrive -PSProvider FileSystem | %{Write-Host $_; %{Get-ChildItem "$_`:`\" -Filter "*.txt" -Recurse -File -ErrorAction SilentlyContinue} | Measure-Object | select Count}

The workflow is a bit improved to edit that line, but now I’m ready to share my strongest objection.

As soon as I begin to introduce for loops and semi-colons into code, then I’ve lost what feels like nice PowerShell to me. That is slipping into procedural programming. I love PowerShell’s pipeline and I want to use it. I want to think and type left-to-right, one command at a time, with one piece of data flowing out of the previous command into the next. When I open up that ForEach loop before the Write-Host and terminate it at the end of the line, I’ve effectively killed off the pipeline. I open up another on the inside, but it’s a different one. If I wanted to get rid of the Write-Host, then I’d have to remove code before the Get-ChildItem as well as cleaning up the closing curly brace at the end of the line instead of being able just delete a contiguous set of characters. This does not flow like I want my PowerShell to.

Each unique concern should live between its own set of pipe characters. I want something like this:

First-Command | [Some command here that prints to console and sends the input object down the pipeline.] | Second-Command

If you search the internet for a solution, you might stumble on Tee-Object which sounds promising at first, as it is often described as a command that lets you send an input object to two places. However, it is limited in where you can send it. One is down the pipeline as we want. The other is to a file or a variable. Console is not an option. Unless, of course, Tee-Object appears as the last command on your pipeline, in which the implicit Out-String at the end will end up making the output object print.

Creating a custom bit of inline code to accomplish this isn’t hard, using the previously maligned ForEach-Object and semicolon duo:

First-Command | %{Write-Host $_; $_} | Second-Command

For each result of First-Command, this will first print the object and then send it down the pipeline for further processing by Second-Command. Of course, there are some ways you could make the output look nicer, the but succinctness of this method is highly valuable unless you really need change the formatting. You may want to look into Write-Host ($_ | Out-String) if you want the results to look exactly the same as they would if you let PowerShell do its default formatting for the object as it would at the end of a pipeline.

Our example would now look like this:

Get-PSDrive -PSProvider FileSystem | %{Write-Host $_; $_} | %{Get-ChildItem "$_`:`\" -Filter "*.txt" -Recurse -File -ErrorAction SilentlyContinue} | Measure-Object | select Count

Not too bad, though it looks very… punctuation-y. I’m sure that’s a word.

If we stick this in a function, then we don’t have to really think about how to do this next time. The most perfect and logical name would be Pee-Object. It’s like Tee-Object, but for Printing objects.

Get-PSDrive -PSProvider FileSystem | Pee-Object | %{Get-ChildItem "$_`:`\" -Filter "*.txt" -Recurse -File -ErrorAction SilentlyContinue} | Measure-Object | select Count

Just as its inspiration Tee-Object has the alias of tee, Pee-Object can be aliased to pee.

Get-PSDrive -PSProvider FileSystem | pee | %{Get-ChildItem "$_`:`\" -Filter "*.txt" -Recurse -File -ErrorAction SilentlyContinue} | Measure-Object | select Count

Get Pee-Object on GitHub.

I would like to note that while this article stresses my like for one-liners in PowerShell, that if you are going to be writing re-usable code, then you would want to place it in an external file anyways. In that case maintainability should probably be a higher priority than terseness. You most likely shouldn’t be using Write-Host in a re-usable module anyways.