Using ValidateSet() with object type - powershell

I'm not sure if this is possible, but I have a parameter that is used in three different parameter sets, and based on which set it is in, the type will be different. While the snip below is syntactically correct, when I attempt to leverage it I'm receiving an error. I do something similar in another function where my validateset is a collection of classes I have defined which may be misleading as those are defined within the powershell module and not "proper" .Net types.
[parameter(Mandatory = $false, ParameterSetName = 'string')]
[parameter(Mandatory = $false, ParameterSetName = 'integer')]
[parameter(Mandatory = $false, ParameterSetName = 'number')]
[ValidateSet([string],[int],[decimal])]
$default,
Resulting error
Cannot validate argument on parameter 'default'. The argument "black" does not belong to the set "string,int,decimal" specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.

Use a ValidateScript attribute instead:
[ValidateScript({$_ -is [string] -or $_ -is [int] -or $_ -is [decimal]})]
[object]$default,

Related

PowerShell Parameter Set Requires Named Parameter

I have the following snippet of a functions parameters and their sets
function Test {
[CmdletBinding(DefaultParameterSetName='StringConsole')]
param (
[Parameter(Mandatory,
ValueFromPipelineByPropertyName,
ParameterSetName = 'ObjectFile')]
[Parameter(Mandatory,
ValueFromPipelineByPropertyName,
ParameterSetName = 'StringFile')]
[Alias("PSPath")]
[ValidateNotNullOrEmpty()]
[string]
$Path,
[Parameter(Mandatory,
ValueFromPipeline,
ParameterSetName='StringFile',
Position = 0)]
[Parameter(Mandatory,
ValueFromPipeline,
ParameterSetName='StringConsole',
Position = 0)]
[ValidateNotNullOrEmpty()]
[string]
$Message,
[Parameter(Mandatory,
ValueFromPipeline,
ParameterSetName='ObjectFile',
Position = 0)]
[Parameter(Mandatory,
ValueFromPipeline,
ParameterSetName='ObjectConsole',
Position = 0)]
[ValidateNotNullOrEmpty()]
[object]
$Object,
[Parameter(ParameterSetName='StringFile')]
[Parameter(ParameterSetName='StringConsole')]
[ValidateSet('Information', 'Verbose', 'Warning', 'Error', 'Object')]
[string]
$Severity = 'Information',
[Parameter(ParameterSetName='StringFile')]
[Parameter(ParameterSetName='StringConsole')]
[switch]
$NoPreamble,
[Parameter(ParameterSetName = 'StringConsole')]
[Parameter(ParameterSetName = 'ObjectConsole')]
[switch]
$Console
)
}
If I call the function using
Test 'Hello, World'
it properly uses the StringConsole default parameter set from CmdletBinding
If I call the function using
Test -Message 'Hello, World' -Path C:\SomeFile.txt
It properly uses the StringFile parameter set
But if I call the function using
Test 'Hello, World' -Path C:\SomeFile.txt
I get this error and the function doesn't execute:
Parameter set cannot be resolved using the specified named parameters
The error specifically states it couldn't resolve the parameter set using the NAMED parameters. If a parameter gets bound by position does it not also satisfy the "named" parameter? Or do you have to specifically bind the parameter using the name?
Is there anyway I could design the parameter sets to make my last example work and not throw an error?
The logic used for your parameter sets looks perfectly fine but the issue is that you have 2 parameters with Position = 0 (-Message and -Object), normally this wouldn't be a problem but one of them is of the type System.Object and since all objects inherit from this class, no matter what you pass as argument in position 0 it will match this parameter. Since the other parameter on Position = 0 is of type System.String then 'Hello, World' (a string but also an object) matches both parameter sets and the binder has no idea which one did you mean to use.
A very easy way of seeing this, without changing your current code and just adding $PSCmdlet.ParameterSetName to the function's body, would be to pass an integer as positional parameter and everything works as expected:
function Test {
[CmdletBinding(DefaultParameterSetName='StringConsole')]
param(
# same param block here
)
'Using: ' + $PSCmdlet.ParameterSetName
}
Test 0 -Path C:\SomeFile.txt # => Using: ObjectFile

Passing Boolean powershell parameters from a non-powershell terminal

Sometimes I invoke powershell scripts from Linux bash/shell scripts like so:
pwsh MyScript.ps1 win-x64 false
And in my MyScript.ps1 file, I set up parameters like so:
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string] $runtime,
[Parameter()]
[bool] $singleFile = $true
)
I get an error for the second parameter:
Cannot process argument transformation on parameter 'singleFile'. Cannot convert value "System.String" to type "System.Boolean". Boolean parameters accept only Boolean values and numbers, such as $True, $False, 1 or 0.
I tried passing '$false' as well as 0 but it treats everything as a string. When invoking powershell scripts from outside of a PWSH terminal, how do I get it to coerce my string-boolean value into an actual Powershell boolean type?
I propose to use [switch]
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string] $runtime,
[Parameter()]
[switch] $singleFile
)
Write-Host $runtime
It works for me with :
pwsh ".\MyScript.ps1" "toto" -singlefile
In fact your code is working with :
pwsh ".\MyScript.ps1" toto -singleFile 1

Powershell passing mandatory dynamic type variable to function

I am implementing a function in Powershell which will perform REST calls. One of the parameters may differ in contents, depending on given scenarios. For instance, the body of the REST call may be a string or a hash table. How do you implement this within the CmdletBinding() declaration?
For instance
Function doRESTcall(){
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[Hashtable]$headers
[Parameter(Mandatory=$true)]
[???????]$body # what type here??
)
.
.
.
}
To declare parameters where any type is allowed you can either not type-constrain the parameter at all or use type constraint [object] (System.Object), by doing so, no type conversion will be needed, since all objects in PowerShell inherit from this type.
It's worth mentioning that unconstrained parameters will allow $null as argument, to avoid this, [ValidateNotNull()] and / or [parameter(Mandatory)] can be used.
function Test-Type {
param(
[parameter(ValueFromPipeline, Mandatory)]
[object]$Value
)
process
{
[pscustomobject]#{
Type = $Value.GetType().FullName
IsObject = $Value -is [object]
}
}
}
PS /> 1, 'foo', (Get-Date) | Test-Type
Type IsObject
---- --------
System.Int32 True
System.String True
System.DateTime True
The correct way to tackel this is to create a ParameterSet:
Function doRESTcall(){
[CmdletBinding()]
param (
[Parameter(Mandatory=$true, ParameterSetName = 'StringBody', Position = 0)]
[Parameter(Mandatory=$true, ParameterSetName = 'HashBody', Position = 0)]
[Hashtable]$headers,
[Parameter(Mandatory=$true, ParameterSetName = 'StringBody', Position = 1)]
[String]$Stringbody,
[Parameter(Mandatory=$true, ParameterSetName = 'HashBody', Position = 1)]
[Hashtable]$Hashbody
)
Write-Host 'Parameter set:' $PSCmdlet.ParameterSetName
Write-Host 'StringBody:' $StringBody
Write-Host 'HashBody:' $HashBody
}
doRESTcall -?
NAME
doRESTcall
SYNTAX
doRESTcall [-headers] <hashtable> [-Hashbody] <hashtable> [<CommonParameters>]
doRESTcall [-headers] <hashtable> [-Stringbody] <string> [<CommonParameters>]
ALIASES
None
REMARKS
None
doRESTcall #{a = 1} 'Test'
Parameter set: StringBody
StringBody: Test
HashBody:
Note: to accept a larger variety of dictionaries (like [Ordered]), I would use a [System.Collections.Specialized.OrderedDictionary] (rather than [Hashtable]) type for the concerned parameters.

Does powershell have a way to handle calling a command with multiple parameter sets?

To be clear, I dont mean my command needs to be accepting multiple parameter sets, it needs to call a function that has multiple parameter sets.
My script
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true, ParameterSetName='NamedCreds')]
[PSCredential] $Credential,
[Parameter(Mandatory=$true, ParameterSetName='NamedCreds')]
[Parameter(Mandatory=$true, ParameterSetName='DefaultCreds')]
[string] $Url,
[Parameter(Mandatory=$true, ParameterSetName='NamedCreds')]
[Parameter(Mandatory=$true, ParameterSetName='DefaultCreds')]
[string] $Path,
[Parameter(Mandatory=$true, ParameterSetName='DefaultCreds')]
[switch] $UseDefaultCredentials
)
#repeating this if block gets tedious
if ($PSCmdlet.ParameterSetName -ieq "NamedCreds")
{
$result = Invoke-RestMethod -Uri $Url -OutFile $Path -Credential $Credential
}
else
{
$result = Invoke-RestMethod -Uri $Url -OutFile $Path -UseDefaultCredentials
}
#do something with result
I'd like to avoid the "if parameter set name, else" conditionals that can end up littering the script. I've read about "splatting" but the examples for it explain it as a way to avoid scrolling to the right for long parameter lists, and don't cover parameter sets or optional parameters. Is there a way to call other commands and properly deal with optional params and param sets?
Splatting is (sort of) the answer to this.
You need your conditionals to determine which parameters to pass. You can use them to build the hashtable and then pass it once.
For what you're trying to do, it might make more sense to rename your -Url parameter to -Uri, and $Path to $OutFile (you can always add an alias if you like the other name better), and then splat $PSBoundParameters directly:
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true, ParameterSetName='NamedCreds')]
[PSCredential] $Credential,
[Parameter(Mandatory=$true, ParameterSetName='NamedCreds')]
[Parameter(Mandatory=$true, ParameterSetName='DefaultCreds')]
[Alias('Url')]
[string] $Uri,
[Parameter(Mandatory=$true, ParameterSetName='NamedCreds')]
[Parameter(Mandatory=$true, ParameterSetName='DefaultCreds')]
[Alias('Path')]
[string] $OutFile,
[Parameter(Mandatory=$true, ParameterSetName='DefaultCreds')]
[switch] $UseDefaultCredentials
)
$result = Invoke-RestMethod #PSBoundParameters
}
Going to address your comment here:
This was an abbreviated example, there are usually more parameters
than that, as well as variables that are derived while the script is
executing that get passed into the command getting called. Is that
going to change this example?
Yeah, unfortunately directly using $PSBoundParameters like that works great, until it doesn't, which is usually quicker than you think (there are issues with optional parameters, certain automatic parameters, etc.).
What it really means is that: when you have conditions, you probably can't avoid conditionals.
When it starts to get complex like that, ignore the parameter sets, and just test every parameter except the ones that are mandatory or have default values (in all sets).
I just noticed you're naming every set on some; you don't need to do that. If the content of the [Parameter()] attribute is the same in every set then you can just use a single [Parameter()] attribute with no set named; I'm going to change your example below to reflect that.
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true, ParameterSetName='NamedCreds')]
[PSCredential] $Credential,
[Parameter(Mandatory=$true)]
[Alias('Url')]
[string] $Uri,
[Parameter(Mandatory=$true)]
[string] $Path,
[Parameter(Mandatory=$true, ParameterSetName='DefaultCreds')]
[switch] $UseDefaultCredentials
)
$params = #{
Uri = $Uri
Path = $Path
}
if ($Credential) {
$params.Credential = $Credential
}
if ($PSBoundParameters.ContainsKey('UseDefaultCredentials')) {
$params.UseDefaultCredentials = $UseDefaultCredentials
}
$result = Invoke-RestMethod #params
}
This is the pattern I typically use. Build a hashtable, then splat it once.
There are some subtleties to note so I will break this down further.
$params = #{
Uri = $Uri
OutFile = $Path
}
Here you create the [hashtable]. At the same time you can populate any members that will always be present. Uri and Path are mandatory paramrters in every parameter set, so essentially the code can't get here unless they have values (or to be more precise; in any condition, they cannot be omitted from the params you pass to Invoke-RestMethod).
If you don't have any of those parameters, then just make it empty $params = #{}.
Note that for $Path we made the key OutFile to match its name in the cmdlet that will be called (so you can have your own param names this way).
if ($Credential) {
$params.Credential = $Credential
}
Pretty easy; test the param for truthiness. If there's something there, you add it to $params.
if ($PSBoundParameters.ContainsKey('UseDefaultCredentials')) {
$params.UseDefaultCredentials = $UseDefaultCredentials
}
Here's one of those special cases. If we use a basic conditional check instead:
if ($UseDefaultCredentials) {
$params.UseDefaultCredentials = $UseDefaultCredentials
}
Then we have a slight problem, because you can call switch parameters with explicit values like this: Invoke-RestMethod -UseDefaultCredentials:$false. If you use the simple conditional, you won't be passing this through, but by checking if it's bound, you will check that the parameter was specified, no matter what its value is.
You don't always have to know; you may not want to support calling the switch parameter explicitly with false, in which case go ahead and do a simple check.
Also note that when you specify a parameter is Mandatory, you are also implicitly not allowing certain falsey values. A Mandatory [String] doesn't allow an empty string or $null. Array parameters don't allow empty arrays. You have to explicitly enable them with other attributes, like [AllowEmptyString()]. If you do this, simple checks don't work for those either because then you aren't passing through the real value; this is another instance where checking $PSBoundParameters is helpful.
The other thing you can do with a switch parameter is just always cast it to [bool] and put it in the hashtable:
$params.UseDefaultCredentials = $UseDefaultCredentials -as [bool]
# or, during initialization
$params = #{
Uri = $Uri
UseDefaultCredentials = $UseDefaultCredentials -as [bool]
}
This can only be done with switches that are not part of a parameter set, because otherwise you will be supplying it to the called command, even when it wasn't on your function, and that may cause invalid parameter sets to be chosen.
Other things you could do:
Use switch ($PSCmdlet.ParameterSetName) instead of lots of ifs, if alternating on the set is more important (like if some of your calculated values rely on the set more than any one specific param).
Start with bound parameters and modify them:
$params = $PSBoundParameters.Clone()
$params.OutFile = $params.Path
$params.Remove('Path')
$params.NewValue = Invoke-MyAlgorithm
There's clearly no one-size-fits-all solution; whichever approach you take depends on exactly what you're doing and what the requirements are.
The one thing in common is: build a hashtable of params, then splat it once*
* Exceptions apply :-p

I don't understand param binding on functions with begin/process/end blocks

function Format-File {
param(
[Parameter(Mandatory = $true, Position = 0)]
[ValidateNotNullOrEmpty()]
[string] $path=$(throw "path is mandatory ($($MyInvocation.MyCommand))"),
[Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[string] $key,
[Parameter(Mandatory = $true, Position = 2, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[string] $value
)
}
I'm calling it like so, assume I've added values to the dictionary (removed for brevity here)
$dict = New-Object 'System.Collections.Generic.Dictionary[string,string]'
$dict.GetEnumerator() | Format-File -Path $updatePath
Here's my conundrum.
The above works perfectly. However, the following does not, note the difference in the key/value parameter
function Format-File {
param(
[Parameter(Mandatory = $true, Position = 0)]
[ValidateNotNullOrEmpty()]
[string] $path=$(throw "path is mandatory ($($MyInvocation.MyCommand))"),
[Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[string] $key=$(throw "key is mandatory ($($MyInvocation.MyCommand))"),
[Parameter(Mandatory = $true, Position = 2, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[string] $value=$(throw "value is mandatory ($($MyInvocation.MyCommand))")
)
}
The above throws an exception. It appears to be getting the default value when the function is first called, but when processing, the key/value parameters are set properly.
It makes a bit of sense as to why the key/value wouldn't be set at the time of the function call, but this also means my mental model is off.
So my question is two-fold.
What is the parameter binding process for functions of this nature, and
How does one verify the input of values that have come in from the pipeline? Manually check in the begin block, or is there another method?
If you have links to describe this all in greater detail, I'm happy to read up on it. It just made me realize my mental model of the process is flawed and I'm hoping to fix that.
What is the parameter binding process for functions of this nature?
In the Begin block, pipeline bound parameters will be $null or use their default value if there is one. This makes some sense, considering that the pipelining of values hasn't started yet.
In the Process block, the parameter will be the current item in the pipeline.
In the End block, the parameter will be the last value from the Process block, unless there was an exception in validating the parameter, in which case it will use the default (or $null).
How does one verify the input of values that have come in from the pipeline?
You can't check in the Begin block.
The best way is to use [Validate attributes, as you have with [ValidateNotNullOrEmpty()].
Your examples with using throw as a default value are useful in some situations but they are a clever workaround. The thing is, you don't need them since you already declared the parameter as Mandatory.
Instead of using a default value, you can use [ValidateScript( { $value -eq 'MyString' } )] for example.
Since the error message from [ValidateScript()] sucks, you can combine the techniques:
function Format-File {
param(
[Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[ValidateScript( {
($_.Length -le 10) -or $(throw "My custom exception message")
} )]
[string] $key
)
}
Using [ValidateScript()] works whether it's a pipeline parameter or not.