Eric's Technical Outlet

Learning the hard way so you don't have to

Magic in PowerShell Scripts Lets Evil Win

When my daughter was younger, my family would gather around the television and watch the series Once Upon a Time. Of its dominant recurring themes, one oft-repeated line in particular stands out: “Magic always has a price.” In the show, when the characters used non-trivial magic, some sort of dire consequence was sure to follow (plot allowing, of course). Their takeaway: never use magic without a willingness to pay the price. That lesson works for PowerShell scripters, too.

What PowerShell “Magic”?

“Magic”, in programming and scripting, describes uses that cannot be explained solely through a language’s grammar, syntax, or keywords.

As an example, consider the following:

$pi = 3.14

If you know about PowerShell, then you know that the above assigns the value 3.14 to a variable named “pi”. A casual reader might assume that the variable name of “pi” means mathematical pi, especially since it uses a reasonable value approximation.

The above has two problems: if you recognize “pi”, that comes from your mathematical background, not your PowerShell knowledge. To someone without that background, it’s just a couple of letters and a number. Second, PowerShell will not enforce external semantics such as these. “pi” in this case might mean something else entirely and only use that particular value coincidentally. Therefore, we consider “pi” in the above to be a “magic number”.

We cannot universally avoid magic in our programs and scripts, especially numbers. Typically, we address that with comments:

# set value of pi to use in mathematical operations
$pi = 3.14

That solves both problems. Even if someone reading your script does not know what “pi” means in math, you have pointed them in the correct direction. For readers that do understand “pi”, you have disambiguated your usage.

An Example of Evil Magic in PowerShell

The “pi” example demonstrates a relatively benign form of magic. We can reasonably assume that if someone reading your script does not understand “pi”, the following script that uses it will also confuse them, no matter how well-formed. So, the “price” of magic in this case is a bit more confusion for a limited set of readers.

Consider the following:

# configure the environment
$ErrorActionPreference = 'Stop'
$DefaultErrorMessage = 'An unknown error occurred'
$DefaultSuccessMessage = 'Action successful'
$ErrorCount = 0
$ScriptBeginStamp = [datetime]::Now

How you interpret the above depends entirely on your experience. Not your level of experience, just your experience. Do you see 3 string variables, an integer variable, and a DateTime variable? That is what the PowerShell language specification indicates that you should see. No one has any valid reason to criticize you if that’s what you see. Unfortunately, that’s not what you’re looking at.

The author of the above script has set the script global variable “ErrorActionPreference”. How do I know that “ErrorActionPreference” is a script global variable? Because I know. I’ve seen it before. Is it part of PowerShell’s grammar, syntax, or keywords? No, it is not. Can someone have a long, productive history of using PowerShell without ever encountering this variable? Yes. Does it even use the common prefix of $PS in other built-in variables that might serve as a tip? No. This script exhibits both problems set forth in the initial paragraph.

Where’s the Evil?

I think it’s clear that this script is at least questionable. But is it truly evil? If left alone, or only modified by people experienced with it, maybe not. But, if I want code that I control from cradle to grave, then I write it in C++ and distribute an .EXE. When I distribute PowerShell, I expect people to tinker with it. Therefore, the world should expect me to write good script. I mean, if your scripts don’t make the world a better place, even minimally, just what sort of scripts do you write?

This script:

$ErrorActionPreference = 'Stop'
$VirusesAverage = $VirusesFoundOnThisDrive / $VirusesTotal
Delete-AllFiles -Target $ThisDrive
Write-Host 'This drive is clear'

Behaves dramatically different from this script:

$VirusesAverage = $VirusesFoundOnThisDrive / $VirusesTotal
Delete-AllFiles -Target $ThisDrive
Write-Host 'This drive is clear'

Yes, we’re just going to ignore that these are stupid scripts to begin with. They serve to illustrate the point. The first one will stop if no viruses were found because division by zero will trigger ErrorActionPreferences’s overridden Stop behavior. On most systems, the second will just plow right through and run that “Delete-AllFiles” line anyway. If that were a real cmdlet, you have a disaster.

Is it reasonable to assume that someone would edit the former into the latter? Well, for this script, I would hope someone would just permanently delete the whole thing. But in a more reasonable script, yes, someone might delete the $ErrorActionPreference line. Why?

If you do not realize that $ErrorActionPreference does something important, setting it with a string appears to do nothing.

A perfectly reasonable person could read that script and believe that you created a variable named “ErrorActionPreference”, set it, and then never used it again. Deleting the line is the rational, responsible action to take. In the case of the contrived script, the negative ramifications of removing that line will only appear when it can cause the worst possible outcome. Therefore, evil.

Purging Evil Magic from Your Scripts

So, what to do? Start simple. Most gurus already tell you to avoid aliases and positional parameters in scripts. Extend that to any case in which a perfectly rational person, following only PowerShell’s rules for grammar, syntax, and keywords, would not have any reason to believe that a script line has any special use.

In the case of the “ErrorActionPreference” variable, our options are somewhat limited. I believe that this particular item was a design misstep. It could have been triggered by a Set- cmdlet or stuffed into a fixed group of PowerShell environment variables ($PSEnv.things). Like other globals, it could have been a $PS prefix. But, it does us little good to wish it were different now.

You might use a comment to indicate that it is an environment variable.

$ErrorActionPreference = 'Stop' # force script to stop on errors unless overridden with try{} or -ErrorAction

I do things a bit differently. One of the “magic” parts of this variable is that it processes string input into a named enumerator. If you give it an invalid value, it will throw an error. Since it uses a complex type, we can use that as a context clue for readers.

First, get the type.

$ErrorActionPreference.GetType().FullName

That returns “System.Management.Automation.ActionPreference”. Now we know the type name of the enumerator. With that, or any other enumerator type, you can enclose the complete name in square brackets ( [ ] ), append two colons, and then use the TAB key to flip through all possible enumerator values:

[System.Management.Automation.ActionPreference]::

I know that I want to set it to “Stop”, so in script:

$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop

This does not completely address the problem, of course. It still appears that I set a variable and never used it again. However, this one does not blend in as easily. It should immediately stand out from normal variable creation, even to novice users. I have changed the “price” of magic from “the dangers of using a seemingly innocuous variable creation to mask a potentially dangerous setting” to “the dangers of someone tinkering with an obviously different variable without researching it”.

It Doesn’t Hurt

I understand a reluctance against extending the length of your script lines. But, two things:

  • Tab completion works for navigating namespaces. Try it out by typing “[Sys” and pressing TAB (maybe a few times to get back to System), then “.” and TAB, and “Ma” and TAB, etc. As long as you can kind of remember where you’re going, you can get there quickly. If you forget completely, use the trick above for getting an enumerator value.
  • Copy/paste is your friend. Your normal scripting environment probably lets you set up templates or snippets.

The extended line is a much smaller price to pay than the potential fallout of a confused script maintainer.

Carry On, Interactive Users

This sort of behavior really only applies to script files. When you’re working interactively, take all the shortcuts that you [safely] can.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: