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 EinsteinAll 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.
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’
LikeLike
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!
LikeLike
AFAIK, you can not have basic function with mandatory parameters. Using
[Parameter()]
attribute make your function advanced even without[CmdletBinding()]
attribute.LikeLike
Technically, yes: “to be recognized as an advanced function (rather than a simple function), a function must have either the
CmdletBinding
attribute or theParameter
attribute, or both.”. But Advanced functions withoutCmdletBinding
attribute are practicatly the same as simple functions, i.e.: no Pipeline support, no Automatic variables, no Begin/Process/End blocks etc…LikeLike
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.LikeLike
First let’s establish some common vocabulary, because otherwise it’s becoming confusing. Basically we have:
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.LikeLike
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
returnTrue
for all Advanced functions, with or withoutCmdletBinding
attribute.LikeLike
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 theCmdletBinding
attribute (DefaultParameterSetName
,ConfirmImpact
,SupportsShouldProcess
, etc…).LikeLike