Splatting and mandatory parameters

Splat them. For the PowerShell knows those that are His own.

Splatting is very useful PowerShell technique that allows to greatly simplify calls to a functions or cmdlets. Basically, instead of passing arguments one by one, you create a hashtable where keys named as arguments and their values are passed to the function.

I use splatting extensively. Usually I have one big hashtable, where all the settings for the current script are stored. The hashtable itself is read from INI file, which are in my opinion provide optimum balance between hardcoding settings into the script itself and storing them in XML files. So, when I’ve to call a function in my script, I just splat this hashtable on it and PowerShell binds values from the hashtable to the function parameters. Well, at least this should be so in a perfect world.

The problem comes when you decide that you’re grown up enough to write advanced functions and declare parameters using PowerShell’s Param() statement and parameter arguments. Not having to manually validate if a parameter supplied or not removes a great burden from your shoulders. Combined with Parameter Validation attributes it allows to write a more streamlined and easier to read code. Of course it also makes parameter definition section somewhat bloated, but it totally worth it.

One of the first things to try is a Mandatory argument to make a parameter required. Here is example: I’d like to splat a hashtable $UserInfo, which contains details about some user, on the function Test-Password to validate that user’s password is strong enough, before proceeding further. The parameters Password and UserName in this function are declared as mandatory:

$UserInfo = @{
    Computer = 'Wks_1'
    UserName = 'John'
    Password = '!Passw0rd'
    Comment = 'Marketing'
}

function Test-Password
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $true)]
        [string]$Password,

        [Parameter(Mandatory = $true)]
        [string]$UserName
    )

    Process
    {
        if($Password.Length -ge 8 -and $Password -ne $UserName)
        {
            return $true
        }
        else
        {
            return $false
        }
    }
}

Test-Password @UserInfo

But this is what happens when you actually run this code: if any of parameters in your function are declared as Mandatory and you splat hashtable with extra values, PowerShell will reject your futile attempt.

Why? For the heck of it, that’s why.

Test-Password : A parameter cannot be found that matches parameter name 'UserName'.
At C:\Scripts\Test-Splat.ps1:35 char:15
+ Test-Password @UserInfo
+ ~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Test-Password], ParameterBindingException
+ FullyQualifiedErrorId : NamedParameterNotFound,Test-Password

I’ve no reasonable explanation for this behavior. Sure it would be nice to know, that I’ve passed more parameters the function, that it can handle. Because maybe I was wrong about it, so it’s OK to tell me (Write-Warning would do the job). I’m sure, that behind the scenes, there was some reason to make splatting behave this way, but it seems to me that is not valid anymore. I hope, that someone would be able to enlighten me. This excerpt from the about_Functions_CmdletBindingAttribute help looks related, but i doesn’t mention Mandatory argument and doesn’t really explain why this happens:

In functions that have the CmdletBinding attribute, unknown parameters and positional arguments that have no matching positional parameters cause parameter binding to fail.

OK, it’s not a perfect world, alas. I’ve to cope with this and there are several options:

  • Give up the Mandatory attribute.

    Not a real option. Sure I could go back to the wild days of the simple functions and validate attributes manually, but this is boring, error prone and defies progress. Not the things I’d like to do.

  • Pass all attributes as named arguments

    I did this once, and it made my eyes hurt and fingers ache. Even with tab-completion.

    Test-Password -Password $UserInfo.Password -User $UserInfo.UserName
    
  • Write proxy function to strip hashtable of the all the parameters that target function can’t handle.

    Davin K. Tanabe wrote a good one, be sure to check it out. It’s the only sane way to use this kind of one-size-fits-all splatting with built-in cmdlets and 3rd-party functions.

  • Convert hashtable to PSObject and employ ValueFromPipelineByPropertyName

    We can not solve our problems with the same level of thinking that created them.
    – Albert Einstein

    All it takes, is to add ValueFromPipelineByPropertyName to the parameter arguments, convert hashtable to PSObject and pass it to the function via pipeline. Actually, there is no splatting involved at all, but hey, at least you can keep the hashtable!

    $UserInfo = @{
        Computer = 'Wks_1'
        UserName = 'John'
        Password = '!Passw0rd'
        Comment = 'Marketing'
    }
    
    function Test-Password
    {
        [CmdletBinding()]
        Param
        (
            [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
            [string]$Password,
    
            [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
            [string]$UserName
        )
    
        Process
        {
            if($Password.Length -ge 8 -and $Password -ne $UserName)
            {
                return $true
            }
            else
            {
                return $false
            }
        }
    }
    
    # PowerShell 2.0 way:
    New-Object PSObject -Property $UserInfo | Test-Password 
    
    # Powershell 3.0 way:
    [PSCustomObject]$UserInfo | Test-Password
    
  • Harness the magic of ValueFromRemainingArguments

    ValueFromRemaingArguments is not an argument that you encounter often. Here is what PowerShell help has to say about it:

    ValueFromRemainingArguments Argument

    The ValueFromRemainingArguments argument indicates that the parameter accepts all of the parameters values in the command that are not assigned to other parameters of the function.
    The following example declares a ComputerName parameter that is mandatory and accepts all the remaining parameter values that were submitted to the function.

    Param
    (
        [parameter(Mandatory=$true, ValueFromRemainingArguments=$true)]
        [String[]]
        $ComputerName
    )
    

    Sweep it under the carpet and turn a blind eye.

    In short, once declared, it makes your parameter to catch all what’s left after all other parameters were bound. So here is idea: why don’t we let PowerShell bind everything it can from the hashtable and then dump unused values to the dummy parameter $Splat.

    $UserInfo = @{
        Computer = 'Wks_1'
        UserName = 'John'
        Password = '!Passw0rd'
        Comment = 'Marketing'
    }
    
    function Test-Password
    {
        [CmdletBinding()]
        Param
        (
            [Parameter(Mandatory = $true)]
            [string]$Password,
    
            [Parameter(Mandatory = $true)]
            [string]$UserName,
    
            [Parameter(ValueFromRemainingArguments = $true)]
            $Splat
        )
    
        Process
        {
            if($Password.Length -ge 8 -and $Password -ne $UserName)
            {
                return $true
            }
            else
            {
                return $false
            }
        }
    }
    
    Test-Password @UserInfo
    

    This times code runs fine and that’s what we’ve got in the our $Splat variable:

    [DBG]: PS C:\Users\beatcracker>> $Splat
    -Comment:
    Marketing
    -Computer:
    Wks_1
    
    [DBG]: PS C:\Users\beatcracker>> $Splat.GetType()
    IsPublic IsSerial Name      BaseType
    -------- -------- ----      --------
    True     True     ArrayList System.Object

    All our extra arguments are landed safely here, no harm done. Though, I’ll be honest, this is a kludge, but it’s the only way I know to make the PowerShell splatting work as it should.

Actually one can combine the last two approaches: declare all the parameters as ValueFromPipelineByPropertyName and add a dummy parameter with a ValueFromRemainingArguments argument. This is what I’ve ended up with:

$UserInfo = @{
    Computer = 'Wks_1'
    UserName = 'John'
    Password = '!Passw0rd'
    Comment = 'Marketing'
}

function Test-Password
{
    [CmdletBinding()]
    Param
    (
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]$Password,

        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]$UserName,

        [Parameter(ValueFromRemainingArguments = $true)]
        $Splat
    )

    Process
    {
        if($Password.Length -ge 8 -and $Password -ne $UserName)
        {
            return $true
        }
        else
        {
            return $false
        }
    }
}

# Splatting:
Test-Password @UserInfo

# PowerShell 2.0 way:
New-Object PSObject -Property $UserInfo | Test-Password 

# Powershell 3.0 way:
[PSCustomObject]$UserInfo | Test-Password

As you can see, now I’m able to do splatting and piping, while keeping my parameters as mandatory as my heart desires.