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
Related
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
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.
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,
I was trying to figured out how can I throw exception or set it to default value, once I didn't put any value on the parameter?
Function ConnectionStrings {
param(
[Parameter(
Mandatory = $false)]
[AllowNull()]
[AllowEmptyString()]
[string] $region
)
try{
if (!$regionHash.ContainsKey($region)){
$regionHash["US"]
} elseif (!$region) {
$regionHash["US"]
#$slave = $regionHash["US"].slave
#$master = $regionHash["US"].master
#$track = $regionHash["US"].tracking
} else {
$regionHash[$region]
#$slave = $regionHash[$region].slave
#$master = $regionHash[$region].master
#$track = $regionHash[$region].tracking
}
} catch {
Write-Warning -Message "OOPS!"
}
}
Once I run the command: ConnectionStrings -region It should throw an exception or set to a default value instead.
Need your guidance, I'm just new on powershell.
Thanks.
edit: Without setting the value of the parameters ConnectionStrings -region
You cannot use both Mandatory and a default value for the same parameter. Why not? If a Param has the Mandatory` attribute, the cmdlet will prompt the user for a value, if no value is provided.
If you want to use a default value, provide one like this:
function Verb-Noun
{
Param
(
# Param1 help description
[ValidateNotNull()]
[ValidateNotNullOrEmpty()]
$Param1 = 'default'
)
"value of `$param1` is $Param1"
}
If executed like so
>Verb-Noun -Param1 a
value of $param1 is a
But if the user does not provide a value for the parameter, the default value will be used.
Verb-Noun
value of $param1 is default
Now, if we add the Mandatory attribute like so...
Param
(
# Param1 help description
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
[ValidateNotNullOrEmpty()]
$Param1 = 'default'
)
#rest of code...
When the user fails to provide a value for Param1, the user is prompted to provide a value. There is no opportunity for the default to be used.
Verb-Noun
cmdlet Verb-Noun at command pipeline position 1
Supply values for the following parameters:
Param1:
#function pauses and won't run until a value is provided
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.