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.

Advertisements

8 thoughts on “Splatting and mandatory parameters

  1. Can you elaborate about **Mandatory** part? As I can see all advanced functions behave same way, regardless of being with or without mandatory parameter.

    function f{param([Parameter()]$NonMandatoryParameter)’All OK’}
    $Params=@{NonExistingParameter=12345}
    f @Params # A parameter cannot be found that matches parameter name ‘NonExistingParameter’

    Like

    • I had to recheck this (finding PS 2.0 system was a real PITA :)), but it looks like you’re right – Advanced Functions always behave this way, no matter if Mandatory attribute is specified or not. So Mandatory part applies only to basic functions (without CmdletBinding attribute). Thanks for pointing this out!

      Like

      • AFAIK, you can not have basic function with mandatory parameters. Using [Parameter()] attribute make your function advanced even without [CmdletBinding()] attribute.

        Like

        • Technically, yes: “to be recognized as an advanced function (rather than a simple function), a function must have either the CmdletBinding attribute or the Parameter attribute, or both.”. But Advanced functions without CmdletBinding attribute are practicatly the same as simple functions, i.e.: no Pipeline support, no Automatic variables, no Begin/Process/End blocks etc…

          Like

          • But Advanced functions without CmdletBinding attribute are practicatly the same as simple functions, i.e.: no Pipeline support, no Automatic variables, no Begin/Process/End blocks etc…

            Sorry, I does not understand what do you mean by that. IIRC, basic functions support pipeline, automatic variables and Begin/Process/End blocks since PowerShell v1 while advanced functions first appeared in PowerShell v2.
            And I never encounter any difference in behavior of advanced functions without explicit [CmdletBinding()] attribute.

            Like

            • First let’s establish some common vocabulary, because otherwise it’s becoming confusing. Basically we have:

              1. Simple functions
              2. Advanced functions
              3. Advanced functions with CmdletBinding attribute

              IIRC, basic functions support pipeline, automatic variables and Begin/Process/End blocks since PowerShell v1

              Thanks to your comment I’ve been forced to do some digging around docs and examples and finally found why I’ve believed that Simple functions can’t accept pipeline input. I’ve been using PS since v2, so naturally I’ve tried something like this:

              function SimpleFunctionPipe
              {
              Param ($Pipe)
              Process{ "Pipe: $Pipe" }
              }

              Of course that didn’t work, because for Simple functions to accept pipiline input you have to use $_ automatic variable:

              function SimpleFunctionPipe
              {
              Process{ "Pipe: $_}
              }

              That is not the case with Advanced functions, where you can use [Parameter(ValueFromPipeline = $true)] atttibute on parameter:

              function AdvFunction
              {
              Param( [Parameter( ValueFromPipeline = $true )]$Test )
              Process{ "Test: $Test" }
              }

              I can note that MS docs on this subject are vague and, for example, about_Functions_Advanced_Methods imples that Begin/Process/End blocks are only available to functions using [CmdletBinding()] attribute.

              By Automatic Variables I’ve meant $PSCmdlet variable and I agree that $args and $_ are automatic variables too, so I should’ve been more clear on that subject.

              Like

              • But how “Advanced functions” differ from “Advanced functions with CmdletBinding attribute”? Currently I believe that there is no difference between them. For example FunctionInfo.CmdletBinding return True for all Advanced functions, with or without CmdletBinding attribute.

                Like

                • Yep, I’ve cheked $Myinvocation contents using Compare .NET Objects and besides obvious differences in scriptblock’s AST and such, it seems identical in “Advanced functions” and “Advanced functions with CmdletBinding attribute”. Here is the Gist if you’re interested. So, basically, [CmdletBinding()] can be avoided if one doesn’t need the optional arguments of the CmdletBinding attribute (DefaultParameterSetName, ConfirmImpact, SupportsShouldProcess, etc…).

                  Like

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 )

Twitter picture

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

Facebook photo

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

Google+ photo

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

Connecting to %s