Why Doesn't My ValidateScript() work correctly?

I've received a few comments from folks after my observations on the Scripting Games Event 1. In those observations, I noted how much I loved:

[ValidateScript({Test-Path $_})][string]$path

As a way of testing to make sure your -Path parameter got a valid value, I love this. I'd never thought of it, and I plan to use it in classes. I may write a book about it someday, or maybe even an ode. Seriously good logic. But... I also bemoaned some scripts that provided an additional Test-Path, in the script's main body of code. Why have a redundant check?

So, first, thanks for the e-mails you all sent. Second... please understand that I can't respond to you all. I've got this full-time job thing, and I've got to do it or the grocery store will stop taking our checks. You're welcome to drop comments here, and I really appreciate when you say stuff like, "can you explain ___ in a future post?" because it gives me ideas to write about. I just can't get into private e-mail based education for a dozen folks. Teaching is kinda what I do for my job, so most of my time has to go to that.

But - there's a great teaching point here. Let's take this example:

valid-default-path

This works as you would hopefully expect. When given a valid path, it's fine. When allowed to use a valid default, it's fine. When given an invalid path, it barfs in the ValidateScript. Now look at the next example - which more closely approximates what people have been seeing in their Scripting Games scripts:

invalid-default-path

In the Games, you were given a default path that wasn't valid on your computer. So folks allowed their script to run with that default, and got errors, and were annoyed that ValidateScript() didn't catch the problem.

It never will.

When you run a command, PowerShell goes through a process called parameter binding, wherein it attaches values to parameters and runs any declarative validation - like ValidateScript(). That validation will always catch invalid incoming data that's been manually specified or sent in via the pipeline (for parameters that accept pipeline input). Because my -Path parameter wasn't declared as mandatory, the validation routine will let me run the script and not specify -path.

Then the shell actually runs my code - and that's when it assigns the default value to $path if one wasn't specified on -path. Validation is over by this point, so an invalid default value will sneak by. The assumption by the shell is that you're providing the default value, so you're smart enough to provide a valid one. If you don't, it's your problem.

So do you just add a second, in-code check for the parameter? I'd still say no. I really dislike redundancy. If you know, because of your situation, that you can't rely on ValidateScript(), then don't use it at all - one check should suffice, and if it needs to be in-code instead of declarative, that's fine. What'd be nice is if there was a declarative way of specifying a default, like [Default('whatever')] that ran before the validation checks, but such a thing doesn't exist. Frankly, you could probably argue that if you can't guarantee the validity of a default, then you shouldn't provide one - and I'd probably buy into that argument, and subscribe to your newsletter.

In this case, the problem is entirely artificial. The default path value given to you in the Games scenario is valid in the context of the Games; it's just when you test it on your system, outside that context, that a problem crops up.

Hopefully this helps explain how the ValidateXXX() attributes work, and how they interact with other features, like a default value.

Now explain why this will never assign C:\ as a default value:

[Parameter(Mandatory=$True)][string]$path = 'c:\'

About the Author

Don Jones

Don Jones is a Windows PowerShell MVP, author of several Windows PowerShell books (and other IT books), Co-founder and President/CEO of PowerShell.org, PowerShell columnist for Microsoft TechNet Magazine, PowerShell educator, and designer/author of several Windows PowerShell courses (including Microsoft’s). Power to the shell!

5 Comments

  1. Good stuff Don. Thanks.

    [Parameter(Mandatory=$True)][string]$path = ‘c:\’

    This will never assign C:\ as a default value because the Mandatory switch has been flipped. PowerShell will prompt for a value for $path every time the script is run overriding the defined default.

  2. Don, regarding your teaching point about how to leverage ValidateScript, that's a great best practice to follow. However, there is a better way to do it than the way you suggested here. Consider the following script:

    function Test-ValidateScriptAttribute {
    [CmdletBinding()]
    param(
    [ValidateScript({
    Test-Path -LiteralPath $_
    })]
    [string]
    $Path
    )
    "'${Path}' is a valid literal path that exists on the local system."
    }

    Test-ValidateScriptAttribute 'C:\I do not exist'

    If you run that, you'll get a big ugly red error that (a) shows some of the internals of the function by exposing the script inside the ValidateScript attribute script block and (b) can be very difficult to decipher due to the amount of information it returns. What part of the ValidateScript attribute script block was invalid? It's not easy to figure out what to do when you invoke that command with an invalid path.

    Now consider this script:

    function Test-ValidateScriptAttribute {
    [CmdletBinding()]
    param(
    [ValidateScript({
    if (-not (Test-Path -LiteralPath $_)) {
    throw "Path '${_}' does not exist. Please provide the path to a file or folder on your local computer and try again."
    }
    $true
    })]
    [string]
    $Path
    )
    "'${Path}' is a valid literal path that exists on the local system."
    }

    Test-ValidateScriptAttribute 'C:\I do not exist'

    It serves the same purpose, however it has a huge benefit over the previous version. By using throw inside of a ValidateScript script block, you're controlling the error message that goes back to the end user. This message can therefore be used to tell the user what went wrong, and to tell them what they should consider doing to correct the problem. Here is the output of that second version when run on my local system:

    Test-ValidateScriptAttribute : Cannot validate argument on parameter 'Path'. Path 'C:\I do not exist' does not exist. Please provide the path to a file or folder on your local computer and try again.
    At line:1 char:30
    + Test-ValidateScriptAttribute 'C:\I do not exist'
    + ~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidData: (:) [Test-ValidateScriptAttribute], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Test-ValidateScriptAttribute

    This identifies that there was a problem with a parameter, it highlights which parameter both by name and with the red squiggly underline, and it tells me what I should do to fix it. Much nicer.

    As for the other problem, well, realistically you should never provide a default value for a command parameter that is invalid. 🙂

    Kirk out.

  3. Ooh! Ooh! Me! Me!

    Because it's a mandatory parameter, so it has to be supplied to the function. That means that the assignment of the default value is never run because the parameter binding occurs before the assignment, and parameter binding won't let the function run without supplying that mandatory parameter.