Tuesday, September 17, 2013

A better "For" loop without using "For" in PowerShell

I hate the ugly For command in PowerShell.  It is ugly, inelegant and non-intuitive.  And it's ugly.

But this is PowerShell, and there is always another way to do everything.

Back when I was writing in BASIC, sometime in the Pleostine Era, I think, if you needed to loop ten times, with variable i incrementing for each loop, it looked like this:

For i = 1 to 10

Next I

(I left out the line numbers.  Are you old enough to remember line numbers?)

The PowerShell architects decided to make the same functionality look like this:

For ( $i = 1; $i -lt 11; $i++ )
{ }


Blech.  That may be intuitive to someone with a background in whatever predecessor language they borrow that from.  (I certainly hope they didn't come up with that completely on their own.)  It is also very flexible and allows you to do many fancy things.

But I don't need to do anything fancy; I just need to loop 10 times, and I need it to be readily apparent that that's what I'm doing, and I don't want it to be so damn ugly.

Instead of using For, what if we use ForEach?

(ForEach is an alias for ForEach-Object.  I mostly follow the best practice of not using aliases in scripts to help assure portability, but ForEach is one of my exceptions, especially in this context, as it improves the readability of the script, which is half the point of this tip.)

ForEach allows you to loop through a block of code, once for each object in a collection or array.  So we could use it to loop through an array containing the integers 1 through 10.

We don't want to have to build an array object and populate it with a bunch of integers.  That's too much work and even clunkier than the For command.  Especially if the number of loops we need is variable.

PowerShell shortcuts to the rescue!

From a PowerShell prompt, if I type:

1..4

I get:

1
2
3
4


Two integers separated by two periods is PowerShell shorthand for an array of integers from the first number to the second number, inclusive.

It even works with variables.

$LastOne = 5
1..$LastOne


Yields

1
2
3
4
5


It even works with nested commands.  For example:

1..( ( Get-ChildItem D:\TestFolder ).Count + 2 )

So if we combine that with the ForEach command, we get an elegant statement that is even more intuitive than the old BASIC For command, and far, far better than the ugly PowerShell For command.

ForEach ( $i in 1..10 )
{ }

Return error codes or result codes from PowerShell scripts

You write a script and create a scheduled task to run it.  It runs fine every night for weeks.  Then you notice that the work has been piling up.  You check Task Scheduler, and it reports the task completed successfully.  But it also reports that the script finished in seconds when it should have taken several minutes.  What happened?

Well first of all, your script is broken.  But I don't care.  You can figure that out by yourself.  It's your imaginary script, after all.

More interesting is what happened with Task Scheduler.

Task Scheduler was told to launch PowerShell with a parameter to run the script.  It did that successfully.  It then waits for PowerShell to finish and go away, and log the completion time.  It did that successfully as well.  Task Scheduler doesn't know or care what did or did not happen during the execution of the script.  As far as it is concerned, this was yet another task perfectly executed.

But we want to know the script failed, and we want a hint as to why.

You will see this same behavior and face this same challenge when you launch PowerShell scripts from System Center Orchestrator, or from a command line, or from within another PowerShell script, or from anything else that lets you do such things.

What we want to do, of course, is pass a result code back to the calling application, in our case, Task Scheduler, so that it can respond when appropriate, or at least log the result code.

There are two steps to accomplish this: passing a result code out of the script, and passing the result code on to the calling application.

The trick to the second part, is instead of telling PowerShell to run your script, you tell it to run a two-line code snippet, which in turn runs your script, and then returns the result code.

Currently your task launches PowerShell with parameter:

-File D:\Scripts\MyFavoriteScript.ps1

(You can and probably do leave off the "-File" parameter name.)

Instead, use this:

-Command { . D:\Scripts\MyFavoriteScript.ps1; Exit $LastResultCode }

Dot referencing the script executes it.

$LastResultCode is a built-in variable where PowerShell stores the result code from the last time something within our script or code snippet ran a child instance or PowerShell and returned a result code.

The Exit function exits the code snippet and passes out the contents of $LastResultCode as the result code for the snippet, which in turn will be used as the result code for the task.

Similarly, we use Exit to exit the script and pass a result code out to the snippet.

You can test some condition at the end of or anywhere within your script, and if you don't find what you like, exit the script and pass the result code of your choice:

If ( -not Test-Path D:\NewFile.pdf ) { Exit 42 }

Or you can use built in variables to test the success of a command and if exit it fails.

New-Item D:\BadPath\LogFiles -ItemType directory

If ( -not $? ) { Exit 84 }


To signal successful completion of the script, Exit with code 0, or use Exit without an explicit code, or just let your script finish normally without an Exit function.

So to sum up, use Exit in your script to pass out your desired result code:

Exit 42

and use a code snippet to launch your script and pass on the result code:

PowerShell.exe -Command { . D:\Script1.ps1; Exit $LastResultCode }

Thanks to Ben Schneider at General Mills for his great talk about automation with System Center Orchestrator at the Minneapolis System Center Users Group, which inspired this article.