Wednesday, March 12, 2014

PowerShell passing empty parameters and $false

In my last post I mentioned dealing with $true and $false as parameters.

And, a reader quickly picked up on making the parameter mandatory to solve many issues that you could run into with boolean and how PowerShell deals with it as a parameter.  His example was:

    param
    (
        [parameter(Mandatory = $true)]
        [System.Boolean]
        $VirtualMachineMigration
    )

What this does is it forces the use of the function to always provide either $true of $false.  And thus force the later behavior.  However, what if you have a valid third state for your parameter – empty.

This problem was articulated by Jeff Snover back in 2006: “In PowerShell, Strings can be evaluated as Booleans.  If a string is ZERO length – it is false, otherwise it is TRUE.”

This means that in my case – not passing a string actually sets that parameter to $false within the function / script.  In my previous post I used the natural language way of testing for a string being empty before attempting to process it by using ($VirtualMachineMigration) – does this parameter contain anything or (!$VirtualMachineMigration) – does this parameter contain nothing.

Well, I had the first one close to right; ($VirtualMachineMigration) is more properly stated ‘does the ‘string’ parameter contain a value’ and (!$VirtualMachineMigration) is more properly stated ‘is the value equal to $false’, since the handling behavior of setting an empty boolean equal to false does not allow the question ‘is the string empty’.

So. if you always type your boolean parameter as a boolean, it will always be $true or $false.  And if it is allowed to be empty (not made mandatory) then it defaults to $false.  Therefore it can never be empty.  It always has a count or length property.

I tried to work around this by keeping the type set to [System.Boolean] and using the default value declaration - ‘if nothing is passed default the value to x’

I first tried $null, since I was already in that way of thinking. [System.Boolean]$VirtualMachineMigration = $null  And that fails with the error: Cannot convert value "" to type "System.Boolean". Boolean parameters accept only Boolean values and numbers, such as $True, $False, 1 or 0.

Okay, so lets be sneaky and try ‘2’.  That processes without error.  But what is the result? It is $true!

So, how do you get around this if you need the possibility of your boolean parameter to be empty.

Guess what, don’t type it as a boolean, deal with it as a string.  This way you can test for $true or $false (or 1 or 0) and set a default value that is none of those to evaluate against.

If I do that, I get my additional option of leaving the parameter empty, I lose the option of forcing the caller of the script to either $true or $false, and I have to change the way I test the value.

function EnableVMMigration
{
    [CmdletBinding()]
    param
    (
        $VirtualMachineMigration = 2
    )

    if ($VirtualMachineMigration -eq $true) {
        "making a positive choice"
    }
    elseif ($VirtualMachineMigration -eq $false) {
        "Making a negative choice"
    }
    "Out of my IF"
}

If I stick with ($VirtualMachineMigration) as my test, and I use a numeric value as my string that is equal or greater than 1 – the $true condition is evaluated and the loop is entered.  So, leaving it that way forces me to use an alpha string as my default value.

But I want to use ‘any’ value as my default other than ‘0’ or ‘1’ so I changed my evaluation to literally test for $true or $false as the value: ($VirtualMachineMigration -eq $true) which satisfies that need.

In the end I have my three conditions of $true, $false, and empty, my script properly processes the $true and $false, but I did lose the forcing of the value type.  I will just have to document that and accept it – or add a value test using ValidateSet.

    param
    (
        [ValidateSet("True","False","", 0, 1)]
        [System.String]
        $VirtualMachineMigration = 2
    )

This allows me to enforce the use of ‘true’ or ‘false’ or empty or 0 or 1.

This prevents the consumer from errant input

PS C:\> EnableVMMigration George
EnableVMMigration : Cannot validate argument on parameter 'VirtualMachineMigration'. The argument "George" does not belong to the set "True,False,,0,1" specified by the ValidateSet attribute. Supply an argument that is in the
set and then try the command again.

and supports all of my requirements. If the caller uses $true it validates to ‘1’, $false to ‘0’

I still need to have a default value to handle the case of the empty parameter (don’t want PowerShell setting $false by default after all).

And my complete solution is:

function EnableVMMigration
{
    [CmdletBinding()]
    param
    (
        [ValidateSet("True","False","", 0, 1)]
        [System.String]
        $VirtualMachineMigration = 2
    )

    if ($VirtualMachineMigration -eq $true) {
        "making a positive choice"
    }
    elseif ($VirtualMachineMigration -eq $false) {
        "Making a negative choice"
    }
    "Out of my IF"
}

I hope this is useful!

1 comment:

Anonymous said...

[Param()]
[PSObject]$StringOrBoolean

switch($StringOrBoolean.GetType().Name)
{
"Boolean" {
# parameter was set
}
"String" {
if ($StringOrBoolean.Length -eq 0)
{
# parameter was not set
}
}
default {
# something else was done
}
}