ValidateScript for Beginners

There’s been a lot of chatter about in Scripting Games 2013 blog posts about the ValidateScript attribute. The chatter is, appropriately, confined to the advanced events – this sort of thing is not expected in a one-liner. But I thought I’d take a minute and demystify it – and discuss an issue that it raises about when input should be rejected.

Let’s start with a quick description of ValidateScript and its siblings. For help, see about_functions_advanced_parameters.

What is ValidateScript?

ValidateScript and its siblings are parameter validation attributes. These attributes are statements that are added to the parameter definition. They tell Windows PowerShell to examine the parameter values that are used when the function is called and determine whether the parameter values meet some specified conditions. In particular, ValidateScript lets you write a script block to test the conditions that the values must satisfy. Windows PowerShell runs the validation script on the parameter values and, if the script returns $False, it throws a terminating error.

Before we get to the details, let’s talk about why you’d want to use something like this. The answer is simplicity. “What!!?!,” you say, incredulously? The syntax of this thing looks like a sampler of Windows PowerShell enclosures. There’s a square bracket “[“ or two “]”, a pair of parentheses “( )” and even some curly braces “{  }”. So it doesn’t look simple.

But once you get over the syntax, you realize that putting the parameter value validation into the parameter definition means that you don’t need to test the parameter value in your script. Instead, the Windows PowerShell engine tests the parameter value and you can use the script to do scripty things.

Using ValidateScript

Here’s what I mean. Here’s a silly function that will serve as our example.

function Get-EventDate
{
    Param($EventDate)
    if ($EventDate -is [DateTime] -and $EventDate -gt (Get-Date))
        {"The event is happening on $EventDate."}
    else
        {Write-Error "Event date must be a DateTime object `
         that represents a date in the future."}
}

The Get-EventDate function has a $EventDate parameter. If the value of the $EventDate parameter is a DateTime object and it’s later than now, the function writes a nice sentence with the date to the console or host program. But, if the value of $EventDate is not a DateTime object, or it’s not a future date, the function generates an error. (To be complete, this info would be in the Help for the function.)

But much of this little function is wrapped around validating the value of the $EventDate parameter. So let’s see if we can get Windows PowerShell to validate it for us.

In this version, we add a parameter value type enclosed in square brackets ([DateTime]) on the line before the parameter name ($EventDate).

But that’s enough to allow us to delete the “if $EventDate –is [DateTime]” from the If statement and from the error message.

function Get-EventDate
{
    Param(
    [DateTime]
    $EventDate
    )
    if ($EventDate -gt (Get-Date))
      {"The event is happening on $EventDate."}
    else
      { Write-Error "Event date must represents a future date."}
}

Let’s make sure it works. I’ll send it a process object instead of a date. And, sure enough, Windows PowerShell generates an error explaining that it can’t convert (“process argument transformation” – oy!) a process object to a DateTime object.

PS C:\> Get-EventDate -EventDate (Get-Process PowerShell)

Get-EventDate : Cannot process argument transformation on parameter 
'EventDate'. Cannot convert the "System.Diagnostics.Process (powershell)" 
value of type "System.Diagnostics.Process" to type "System.DateTime".
At line:15 char:26
+ Get-EventDate -EventDate (Get-Process PowerShell)
+                          ~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : InvalidData: (:) [Get -EventDate], 
ParameterBindingArgumentTransformationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,Get-EventDate

Now, let’s get Windows PowerShell to test the other date condition for us. Here’s where ValidateScript comes in.

The syntax is a bit wonky. ValidateScript is enclosed in square brackets: [ValidateScript]. Its parameter is enclosed in parentheses: [ValidateScript( )] and the parameter value is a script block, complete with curly braces: [ValidateScript({ Your-script-goes-here })]. I can never remember this, so I use an ISE snippet or copy it from about_functions_advanced_parameters.

But aside from the syntax, ValidateScript is easy to use. I just moved the (-gt (Get-Date)) from the script into the ValidateScript script block. Now, I can eliminate the error message, too.

In the script block, “$_” represents the parameter value. If a parameter takes a collection (more than one) of objects, “$_” represents each value in the collection, which is tested one at a time – no need for a Foreach-Object command.

function Get-EventDate
{
    Param(
    [ValidateScript({$_ -gt (Get-Date)})]
    [DateTime]
    $EventDate
    )

    "The event is happening on $EventDate."
}

When a parameter value fails a test, ValidateScript generates a terminating error. If the parameter value takes a collection, like a list of dates, and any one of the dates fails the test, ValidateScript throws an error that stops the script, even if all other dates pass the test.

Let’s test by sending it a date in the past. (Today’s date would generate the same error.) The error message explains (in more words that I could use) that the date failed the validation test.

It’s not a great error message, but it’s the best we could do, because Windows PowerShell just executes the validation script in the script block. It can’t guess your intent.

PS C:\> Get-EventDate -EventDate (Get-Date -Month 9 -Day 21 -Year 2007)

Get-EventDate : Cannot validate argument on parameter 'EventDate'. 
The "$_ -gt (Get-Date)" validation script for the argument with value 
"9/21/2007 5:36:36 PM" did not return true. Determine why the validation 
script failed and then try the command again.
At line:1 char:26
+ Get-EventDate -EventDate (Get-Date -Month 9 -Day 21 -Year 2007)
+                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo          : InvalidData: (:) [Get-EventDate], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Get-EventDate

And, just for kicks, let’s pass it a date in the future. This one works.

PS C:\ > Get-EventDate -EventDate (Get-Date -Month 9 -Day 21 -Year 2013)
The event is happening on 09/21/2013 18:00:50.

 

ValidateScript in Advanced Functions

This is the sort of clever thing that the advanced folks are doing. For example, here’s the parameter section from Toni’s totally terrific Archival Atrocity solution.

[CmdletBinding()]
param(
[ValidateScript({ Test-Path $_ -PathType Container })]
[string]$LogPath="C:\Application\Log",
[Parameter(Mandatory=$true)]
[ValidateScript({ Test-Path "$LogPath\*" -Include $_ -PathType Container })]
[string[]]$ApplicationLogFolder,
[Parameter(Mandatory=$true)]
[ValidateScript({ Test-Path $_ -PathType Container })]
[string]$DestinationPath,
[int]$Period=90
)

We’re not even in the function statements yet, but we already know for sure that the values of the $LogPath, $ApplicationLog, and DestinationPath parameters are folders (not files), and the full path to these folders already exists in the file system. Not bad! Excellent, really. Clever enough to win second prize in the Nobel Prizes of PowerShell scripting. (Congratulations, Toni!)

In fact, almost all of the advanced scripts used validation parameters. Take a peek. Keep a copy of about_functions_advanced_parameters nearby.

Should we use ValidateScript?

This is very clever scripting, but is it a good idea? It’s easier for the author and easier to maintain, because the conditions are in a predictable place.

But, is this the right thing to do for users? I don’t know the answer, but I think that we, as a community, need to consider the question.

Jeffrey Snover, the Windows PowerShell grand architect, wisely proclaims that Windows PowerShell differs from other languages in that scripts should “just work.” Windows PowerShell scripts should make the user successful.

The language goes to all ends in its pursuit of this principle. When you send the wrong type of parameter value to a cmdlet, Windows PowerShell tries to convert the value to the right type. It returns an error only when its attempts to convert fail.

In Windows PowerShell 3.0, if you send it a collection of object and ask for a property that the collection doesn’t have, Windows PowerShell checks to see if the objects in the collection have that property and, if they do, it returns the property value. (Try: (Get-Process).Name ).

If you ask Windows PowerShell 3.0 how many items are in an empty object, it tells you 0, even though empty objects don’t have a Count or Length property.

PS C:\> $zoo = $null
PS C:\> $zoo.Count
0

Many scripts, including those we’ve seen in these esteemed Games, have elaborate try-catch syntax to capture errors and create a pleasant user experience.

So, given that background, should we encourage scripting techniques that throw errors to users, instead of making them successful? And, in particular, errors that cannot provide very helpful error messages?

Personally, I prefer scripts that optimize the user experience, instead of the authoring experience. In the Get-EventDate example, where I planned to write an error anyway, ValidateScript is probably a cleaner alternative. But in the Archival Atrocity script, it would have been a much better user experience to create a directory if it didn’t already exist.

On the other hand, if I were writing a script only for myself, I would keep it strict. I would prefer the error message to the risk that I just created a directory structure for a typo.

What do you think? Should we create community guidance for using validation attributes?

About the Author

June Blender

June Blender was a senior programming writer on the Windows PowerShell team at Microsoft from Windows PowerShell 1.0 – 3.0. You see her work every time you type Get-Help for the core modules. She’s now working on the Windows Azure Active Directory SDK team, and she remains an avid Windows PowerShell user and a passionate user advocate. She’s a guest blogger for the Scripting Guys and she tweets Windows PowerShell tips on Twitter at @juneb_get_help.

A 16-year veteran of Microsoft, June lives in magnificent Escalante, Utah, where she works remotely when she’s not out hiking, canyoneering, taking Coursera classes, or convincing lost tourists to try Windows PowerShell. She believes that outstanding documentation is a collaborative effort, and she welcomes your comments and contributions to Windows PowerShell and Windows Azure Help.

11 Comments

  1. It might not have to be an either/or choice.

    You could attempt to create the directory in the Validate script with the -Confirm switch so you get some notification you're about to create a new directory.

    Use Try/Catch to catch errors and check the type. If it's because the directory already exists, return $True so the validation test passes. If it's any other error (access denied, etc) return $False so the validation test will fail.

    • So i gave mjolinor's suggestion a quick try with

      [ValidateScript({if (Test-Path $_ -PathType Container){return $true}else{new-item $_ -ItemType Container -confirm}})]

      And it works. My concern is that while it works it could get quite complex and you need to keep an eye on your logic to make sure you return the correct true or false to allow the validate to pass.

      Also i'm sure someone else with more experience than me will be able to do some proper performance testing to see if this method is slower.

      • complex, like this, but I wasn't able to get the syntax perfect so I didn't submit. Probably not the smartest thing to try since it was my first time with validation.

        Param(
        [parameter(Manditory=$false)]
        [ValidateScript({If($_ -notmatch 'htm|html'){throw $Filename=Read-Host 'Please enter a proper filename and path ending in either htm or html'} else {$true}})]
        [Alias('FilePath','f')]
        [string]$FilePath="$env:USERPROFILE\Desktop\Users.html",

        [parameter(Manditory=$false)]
        [ValidateScript({If($_ -notmatch '\d'){throw $Selection=Read-Host 'Please enter a proper selection count, either as a number or a percentage'} else {$true}})]
        [Alias('Selection','s')]
        [string]$Selection="20"
        )

  2. so yeah the messages are definately not useful. However you can go against the pattern, and with a little more work inside your validate script throw a more meaningful exception yourself (i'm not sure what effects it has on parameterbinding order etc)

    function test-greater100 {
    [CmdletBinding()]
    param(
    [ValidateScript({ if (-not($_ -gt 100)) {throw "A must be greater than 100"} else {$true}})]
    [int] $a

    )
    $a
    }

    -----
    test-greater100 110 #works fine
    32 minutes ago · Like

    Karl Prosser test-greater100 110
    produces
    test-greater100 80
    test-greater100 : Cannot validate argument on parameter 'a'. A must be greater than 100
    At line:11 char:17
    + test-greater100 80
    + ~~
    + CategoryInfo : InvalidData: (:) [test-greater100], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,test-greater100
    31 minutes ago · Like

    • That is excellent! I've completely avoided using ValidateScript because the error messages returned to the caller were so unfriendly. I may change my mind on that, with this option for producing better feedback.

  3. so i took this further and thought, we'll everything i do is in a module, so can i make a helper function to more declaratively do this.

    function unless{
    [cmdletbinding()]
    param ($expressionResultOrScriptBlock , $failmessage)
    process {
    $_ = (Get-Variable -Scope 1 -Name _).Value
    $result = $expressionResultOrScriptBlock;
    if ($expressionResultOrScriptBlock -is [scriptblock]) { $result = & $expressionResultOrScriptBlock }
    if ($result) { $true } else {throw "[ $failmessage ]"}
    }
    }

    function test-greater100 {
    [CmdletBinding()]
    param(
    #both these work
    #[ValidateScript({unless {$_ -gt 100 } -fail "needs to be greater than 100"})]
    [ValidateScript({unless ($_ -gt 100 ) -fail "needs to be greater than 100"})]
    [int] $a

    )
    $a
    }

  4. Not only for validation attributes, but I think a general guidance on error handling would be great. Not so much to say “This is how to do it” but more along the lines of “Consider these things when handling errors.”

    There are just so many things to think about when handling errors in PowerShell. There are terminating errors, non-terminating errors, & $scriptblock, (functionCall), (advancedFunctionCall), error codes from external programs, preference variables, shouldprocess, shouldcontinue, null, empty collections, empty strings, trap, try/catch, throw, write-error, write-warning, validation attributes, errors thrown in the middle of the pipeline, what to expect from a line like $a = if throws an error, etc.

    Then when you try to mix those things together, there’s just a lot of subtlety that are easy to miss.

  5. Great article!
    I studied through about_functions_advanced_parameters just before the games began and have been pretty happy with the ideas it gave me.

    It seems to me that validating parameters is so awesome because it fills a very special need.

    As we've all discovered there's lot's of "semi-appropriate" ways to use Powershell that work until you learn the right way. In some of those cases a good prediction was made during development "The user should be doing THIS... but they might do THAT instead. "

    But accurately making that sort of prediction means that we're counting on the user to give us sensible inputs. Parameter Validation is a great way of protecting our code from input that is entirely wrong instead of almost right.

    But I think it's smart to preach caution anywhere that we have the power to be too rigid.

    • I was just reviewing some entries, and it happened to strike me that Adv 4 is a great place to LEARN [validate..] but perhaps a bad place to implement it.

      If i gave this to a button monkey I wouldn't want it to fail because he put "filename" rather than "filename.html".
      I'd want it to test his input, append .html if it's not present, and then notify him "Hey you asked me to output to C:\windows\explorer.exe which isn't an html file, so your output was saved as C:\windows\explorer.exe.html enjoy!"

      However I'd be perfectly happy for it to fail completely for an invalid path:
      "Validate script did not return $true {Test-path "The intranet" }"

  6. Thanks to everyone for their thoughtful replies. I think there's probably a proper use of this technique, but we need to be sensitive to our users and maximize their success rate. I agree that this decision, like many of these decisions, depend on the audience.