Recently a reader, Matt Penny, shared a tip in a comment on one of my articles. He had a short and simple PowerShell function that he used to insert ToDo commands into his Pester test scripts. Although you could easily use it for other PowerShell work. Of course, I am always on the look out for inspiration so I took Matt's idea and overworked it.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Matt's original function was very simple, which is always a good thing.
function todo { param ([string]$TodoText) write-host -foregroundcolor DarkYellow ” [o] Todo: $TodoText” }
Then in a script or function all he has to do is add lines that invoke this function. There is an assumption that this ToDo function is already loaded, perhaps as part of a profile script. Here's a demo function with several ToDo commands inserted. These indicate additional sections that need to be coded.
Function Demo { [cmdletbinding()] Param( [Parameter(ValueFromPipeline)] [int]$X = 1 ) Begin { Write-Verbose "[BEGIN ] Starting: $($MyInvocation.Mycommand)" ToDo "verbose PSBoundparameter data goes here" } #begin Process { Write-Verbose "[PROCESS] Processing $X" ToDo "calculate square root of X" ToDo "calculate X^X" #write a custom object to the pipeline [pscustomobject]@{ Value = $X Square = $X*$X SquareRoot = 0 Power = 0 } ToDo "add option to save output" } #process End { ToDo "Display runtime" ToDo "Clean up environment" Write-Verbose "[END ] Ending: $($MyInvocation.Mycommand)" } #end }
When I run the command, I see the ToDo statements.
I like that. But I was curious to see what else I could do with it. Of course even if you have no need for my version of the function, I hope you'll learn something new.
I decided I wanted to have each ToDo numbered. I thought it also might be nice to color code ToDo entries to help me prioritize which to work on first. Finally, the current implementation doesn't work very well when running something in a pipelined expression. My Demo function takes pipeline input so if I test it with say 3 numbers I'll get 3 copies of the ToDo items, when all I really need is one.
Keeping a count is relatively simple. I can initialize a variable and then use the ++ operator to increment it. The challenge though is scope. When I call my version of the ToDo function, I get a new scope which disappears when the function exits. And I don't want to make my main script more difficult to write. So I will create a variable in the global scope and increment it. Note that best practice is to avoid referencing out of scope items, but there are always exceptions. This is one of them. By using the $global: prefix I'm telling PowerShell I know what I'm doing.
The other challenge was keeping track of what messages have already been displayed so I cut down on duplicates during a pipelined operation. Again, I resorted to a global variable to keep track of message history. If the message has already been displayed don't do anything, otherwise show it and add it to the history variable.
if (-Not ($global:ToDoHistory -contains $ToDo)) { $global:ToDoCounter++ #format the counter with 2 leading zeros. $msg = "[$($global:ToDoCounter.ToString("00#"))] TO-DO: $ToDo" Write-Host $msg -ForegroundColor $ForegroundColor $global:TodoHistory+=$ToDo }
To demonstrate here is a modified version of my demo function that invokes my version of the ToDo function. My version takes a parameter for the message and an optional parameter for the message color. The default is Cyan.
Function Demo { [cmdletbinding()] Param( [Parameter(ValueFromPipeline)] [int]$X = 1 ) Begin { Write-Verbose "[BEGIN ] Starting: $($MyInvocation.Mycommand)" ToDo "verbose PSBoundparameter data goes here" yellow } #begin Process { Write-Verbose "[PROCESS] Processing $X" ToDo "calculate square root of X" red ToDo "calculate X^X" #write a custom object to the pipeline [pscustomobject]@{ Value = $X Square = $X*$X SquareRoot = 0 Power = 0 } ToDo "add option to save output" green } #process End { ToDo "Display runtime" ToDo "Clean up environment" red <# force ToDo cleanup This is optional. If you don't, you'll manually need to clear $ToDoHistory and $ToDoCounter from the global scope before running another script that uses ToDo. #> Remove-ToDoVariable Write-Verbose "[END ] Ending: $($MyInvocation.Mycommand)" } #end }
As I complete ToDos I can delete the line from the function. The only thing extra I added was an additional function, Remove-ToDoVariable, in the End block to clean up the global variables. Otherwise, the next time I ran the function, the global variables would be re-used which wouldn't be helpful. You wouldn't have to take this step, you could always manually reset the variables before testing again. But now I get (perhaps) a more useful ToDo message.
And a pipelined test doesn't repeat anything.
I have put the entire set of functions on GitHub as a gist.
As I mentioned, you may not have much of a need for the function itself, but hopefully you picked up a tip or two. Always interested in hearing what you think.
By the way, you could combine this with my ToDo ISE snippet https://gist.github.com/jdhitsolutions/75a7216ab86110f7a2c1507d3b29499c
If anyone is interested a small modification to add the script name and line number.
if($null -ne $MyInvocation.ScriptName) {
$msg = “[$($global:ToDoCounter.ToString(“00#”)) $($MyInvocation.ScriptName):$($MyInvocation.ScriptLineNumber)] TO-DO: $ToDo”
} else {
$msg = “[$($global:ToDoCounter.ToString(“00#”))] TO-DO: $ToDo”
}
That’s a neat idea — I actually extended pester to use the tag system to mark tests as @wip or @todo … And print information, but not success/failure for them.I have to say, reading that, I was struck that it would be a good bullet point in a talk about how to know when you should write a module: when you need a persistent variable outside your script scope… You might need a module.I also thought of a way to have the number reset automatically: since you want it to reset when you run the script again, you basically want to reset it at the end of your pipeline– but not the pipeline it’s in, the whole outer pipeline that was kicked off from the prompt. For that, all you need is $MyInvocation.HistoryId … if you store that (in module or global scope) when you’re called, and compare against it each time, if it changes, reset your count…