How to define a function which requires one of two parameters? - powershell

For the following function, it must have a parameter of Group of type int or a parameter of Items of type int array. How to define the function? It must have one of these parameters but cannot have both.
Start-Execute -Group 1
Start-Execute -Items 100,200,300

You want to use Parameter Sets, which are a feature of Advanced Functions.
function Start-Execute {
[CmdletBinding()]
param(
[Parameter(
ParameterSetName='ByGroup',
Mandatory=$true
)]
[int]
$Group ,
[Parameter(
ParameterSetName='ByItems',
Mandatory=$true
)]
[int[]]
$Items
)
# function code
}
Inside the function, you can determine which parameter set was specified by testing the value of $PSCmdlet.ParameterSetName to see which parameter set it matches.
To see that your parameter sets were created correctly, run the following after the function definition has been executed:
Get-Help Start-Execute
That will show you separate invocations for each parameter set.
That being said, we can't tell what your function does. If $Items is just meant to be an array of multiple $Groups (that is, a single item is the same as a group), then your function should accept a single int array [int[]] and then just always process it with foreach, because that will work correctly even with a single value supplied.
Adding a parameter to multiple parameter sets.
You asked about adding a parameter called -Debug. I just want to point out that -Debug is a Common Parameter, so you probably shouldn't use that name. I'll show an example using a parameter named -Test:
function Start-Execute {
[CmdletBinding()]
param(
[Parameter(
ParameterSetName='ByGroup',
Mandatory=$true
)]
[int]
$Group ,
[Parameter(
ParameterSetName='ByItems',
Mandatory=$true
)]
[int[]]
$Items ,
[Switch]
$Test
)
)
# function code
}
This is one way to do it: don't provide any parameter set names. It will be available in all sets.
Another way is to provide a separate [Parameter()] attribute for each parameter set:
[Parameter(
ParameterSetName='ByItems',
Mandatory=$true
)]
[Parameter(
ParameterSetName='ByGroup',
Mandatory=$false
)]
[Switch]
$Test
This is useful when you want to use different settings for different sets, such as to make this parameter mandatory in one parameter set but optional in another, or to make the parameter available to multiple but not all sets.

Related

Using ValidateSet() and ValidatePattern() to allow new values?

I would like to write a script with a parameter that has an existing set of values, but also allows the user to enter an unknown value. I am hoping that it will allow tab-completion from the known set, but not reject a value not yet in the known set.
In this case, there is a list of known servers. A new server might be added, so I want to allow the new server name to be entered. However, the ValidateSet() will reject anything it does not know.
This code does not work.
[cmdletbinding()]
Param (
[Parameter(Mandatory=$true)]
[Validatepattern('.*')]
[ValidateSet('server1', 'server2', 'bazooka')]
[string]$dbhost
)
Write-Host $dbhost
Running this for a known host works well. Automatic tab-completion works with the known list of hosts. But, a new hostname will be rejected.
>.\qd.ps1 -dbname server2
server2
>.\qd.ps1 -dbname spock
C:\src\t\qd.ps1 : Cannot validate argument on parameter 'dbname'. The argument "spock" does not belong to the set "server1,server2,bazooka" specified by the
ValidateSet attribute. Supply an argument that is in the set and then try the command again.
You can use a ArgumentCompleter script block for this purpose. See https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters
Example:
function Test-ArgumentCompleter {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[ArgumentCompleter({
$possibleValues = #('server1', 'server2', 'bazooka')
return $possibleValues | ForEach-Object { $_ }
})]
[String] $DbHost
)
}

Powershell splatting operator only for accepted parameters?

Is it possible, using PowerShell, to use splatting from hashtable when the hashtable contains more entries that the function accepts ?
My use case is to have config objects I pass from one function to another. However, all functions does not require same parameters.
Ex:
function Process-Something{
param(
[Parameter()]
[string]$Owner
)
}
function Process-SomethingElse{
param(
[Parameter()]
[string]$Owner,
[Parameter()]
[int]$x,
[Parameter()]
[int]$y
)
}
$config = #{
"Owner" = "Bart Simpson"
"X" = 10
"Y" = 20
}
Process-Something #config
Process-SomethingElse #config
It fails with these error:
Process-Something : Cannot find a matching parameter « Y ».
The idea is to avoid specifying individual properties for each functions.
As #Ansgar is stating in the comments, the whole idea of having defined your parameters, is to get validation. When you are splatting parameters to your function, you are forcing them to the function. So if a given property of your hashtable doesn't exist as a parameter, you will get an error - just like it is intended.
What you can do, is going into a PSCustomObject and utilize the pipe. If you set all you parameters to accept value from the pipeline, using property name (ValueFromPipelineByPropertyName = $true), then you can actually get the desired behavior.
First I'm redefining your different functions, to have the ValueFromPipelineByPropertyName = $true parameter attribute configured.
function Process-Something{
param(
[Parameter(ValueFromPipelineByPropertyName = $true)]
[string]$Owner
)
$PSBoundParameters
}
function Process-SomethingElse{
param(
[Parameter(ValueFromPipelineByPropertyName = $true)]
[string]$Owner,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[int]$x,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[int]$y
)
$PSBoundParameters
}
With that in place, I'm able to create a hashtable like your example, convert it to a PSCustomObject, and now I can pipe that new object to the different methods and have them pick up only the properties that they need.
I included the PSBoundParameters to showcase that they get what they expect.
Testing is done like this:
$config = #{
"Owner" = "Bart Simpson"
"X" = 10
"Y" = 20
}
$psConfig = [PSCustomObject]$config
$psConfig | Process-Something
$psConfig | Process-SomethingElse

Parameter binding by name through pipeline when some values are empty

In my module, I have two functions a 'Get-Data' and 'Add-Data' I'm having trouble passing information between them through the pipeline.
The following simplified code shows my issue.
function Get-Data{
param ($path)
$out = Import-Csv $path
Write-Output $out
}
The data is the following, notice there are 'gaps' in the data and not every object has all three properties, in fact they cant. You can only have Name and Color or Name and Fruit. (In the real code there are many more properties)
Name,Color,Fruit
Jim,red,
Kate,,Apple
Bob,green,
Abby,,Banana
In the Add-Data function I want to use parameter sets as there are lots of parameters, but only 4 parameter sets possible (two in the simplified code, 'A' and 'B'). This function is exported from the module and I don't want the user to be able to input invalid parameter combinations.
Here is the add-data function:
function Add-Data {
[CmdletBinding()]
Param (
[Parameter(
Position = 1,
ValuefromPipelineByPropertyName = $true,
Mandatory = $true
)]
[System.String]$Name,
[Parameter(
ParameterSetName = 'A',
Position = 2,
ValuefromPipelineByPropertyName = $true,
Mandatory = $true
)]
[System.String]$Color,
[Parameter(
ParameterSetName = 'B',
Position = 3,
ValuefromPipelineByPropertyName = $true,
Mandatory = $true
)]
[System.String]$Fruit
)
BEGIN{}
PROCESS{
Write-Output "$Name $Color $Fruit"
}
END{}
}
When I pipe one to the other like below:
Get-Data -Path c:\some.csv | Add-Data
I get the error:
Cannot bind argument to parameter 'Fruit' because it is an empty string
I know that I am passing an empty string to the fruit parameter and it's what is causing me the issue, but it's obviously not the behaviour I want.
I'd like the fact that the property is empty to help resolve the parameter set so it only needs Name and Color.
I can't use [AllowNull()] or [AllowEmptyString()] and remove the parameter sets as I expect users to use 'add-data' from the command line and I want
help add-data
to show them the correct params to enter.
Thanks in advance for any help.

Passing function arguments by position: Do explicit values get used before pipeline input?

I'm experimenting with creating a function that can take multiple arguments by position, including pipeline input.
Here's a simple test function:
function TestMultiplePositionalParameters
{
param
(
[Parameter(
Position=0,
Mandatory=$true)
]
[String]$FirstParam,
[Parameter(
Position=1,
Mandatory=$true,
ValueFromPipeline=$true)
]
[String]$SecondParam
)
begin
{
Write-Host '================================='
}
process
{
Write-Host '$FirstParam:' $FirstParam
Write-Host '$SecondParam:' $SecondParam
Write-Host ''
}
}
When I call it, it works fine:
"Input1","Input2" | TestMultiplePositionalParameters 'ExplicitArgument'
Which results in:
=================================
$FirstParam: ExplicitArgument
$SecondParam: Input1
$FirstParam: ExplicitArgument
$SecondParam: Input2
However, if I change the parameter which takes the value from the pipeline:
function TestMultiplePositionalParameters
{
param
(
[Parameter(
Position=0,
Mandatory=$true,
ValueFromPipeline=$true)
]
[String]$FirstParam,
[Parameter(
Position=1,
Mandatory=$true)
]
[String]$SecondParam
)
# etc...
}
And again call it:
"Input1","Input2" | TestMultiplePositionalParameters 'ExplicitArgument'
This time I get an error:
TestMultiplePositionalParameters : The input object cannot be bound to any parameters
for the command either because the command does not take pipeline input or the input
and its properties do not match any of the parameters that take pipeline input.
At C:\...\DEMO_Function_Pipeline_MultiPositionalParams.ps1:77 char:21
+ "Input1","Input2" | TestMultiplePositionalParameters 'ExplicitArgument'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (Input1:String)
[TestMultiplePositionalParameters ], ParameterBindingException
+ FullyQualifiedErrorId : InputObjectNotBound,TestMultiplePositionalParameters
My question is: Why does the second version of the function pass the explicit argument 'ExplicitArgument' to the first parameter? I would have thought that given only the first parameter can accept pipeline input the function should have passed the pipeline input to the first parameter and the explicit argument to the second parameter.
Why does the second version of the function pass the explicit argument
'ExplicitArgument' to the first parameter?
Because $FirstParam is marked as being at position '0' and the first parameter passed to the cmdlet is 'ExplicitArgument'. Essentially, the binding of 'ExplicitArgument' to $FirstParam has already happened by the time PowerShell is looking for a parameter that will accept pipeline input.
To get your expected behaviour, you would have to execute the following:
"Input1","Input2" | TestMultiplePositionalParameters -SecondParam 'ExplicitArgument'
The reason for this is that you can have more than one parameter that accepts its value from the pipeline. If you had the following, what would you expect to happen in your second example?
param
(
[Parameter(
Position=0,
Mandatory=$true,
ValueFromPipeline=$true)
]
[String]$FirstParam,
[Parameter(
Position=1,
Mandatory=$true,
ValueFromPipeline=$true)
]
[String]$SecondParam
)
There has to be a precedence and it is that named and positional arguments have higher priority than the pipeline arguments.

Is there a better way to declare multiple parameter sets?

I'm writing a cmdlet (in PowerShell) that is responsible for writing a record into a database. With the conditional command line, it seems like I have to define four different parameter sets.
Is there a more succient way of doing this?
DETAILS
The parameters of the cmdlet are:
ComputerName (the SQL server to connect to)
Path (the location of the data)
Xml (the raw data itself)
UserName
Password
UseIntegratedSecurity (instead of username/password, use current credentials)
Path and Xml are mutually exclusive, and UserName/Password and UseIntegratedSecurity are mutually exclusive.
To get this wired up correctly, it seems like I have to define four different parameter sets, e.g.:
function Install-WidgetData
{
[CmdletBinding()]
PARAM
(
[Parameter(ParameterSetName="Xml_AutoConnect", Mandatory=$True)]
[Parameter(ParameterSetName="Xml_ManualConnect", Mandatory=$True)]
[Parameter(ParameterSetName="Path_AutoConnect", Mandatory=$True, )]
[Parameter(ParameterSetName="Path_ManualConnect", Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[string[]] $ComputerName,
[Parameter(ParameterSetName="Path_AutoConnect", Mandatory=$True)]
[Parameter(ParameterSetName="Path_ManualConnect", Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[string] $Path,
[Parameter(ParameterSetName="Xml_AutoConnect", Mandatory=$True)]
[Parameter(ParameterSetName="Xml_ManualConnect", Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[string[]] $Xml,
[Parameter(ParameterSetName="Xml_AutoConnect")]
[Parameter(ParameterSetName="Path_AutoConnect")]
[switch] $UseIntegratedSecurity,
[Parameter(ParameterSetName="Xml_ManualConnect")]
[Parameter(ParameterSetName="Path_ManualConnect")]
[ValidateNotNullOrEmpty()]
[string] $UserName,
[Parameter(ParameterSetName="Xml_ManualConnect")]
[Parameter(ParameterSetName="Path_ManualConnect")]
[ValidateNotNullOrEmpty()]
[string] $Password,
)
If you want a fast sanity check on your parameter sets, you can use Show-Command
This will display a form with multiple tabs, one for each parameter set. For example:
Show-Command Get-ChildItem
Will show this:
Or; If you want a Command Line alternative, you can use Get-Command -Syntax
Get-Command Get-ChildItem -Syntax
Will show you this:
Get-ChildItem [[-Path] ] [[-Filter] ] [-Include ] [-Exclude ] [-Recurse] [-Depth ] [-Force] [-Name] [-UseTransaction] [-Attributes ] [-Directory] [-File] [-Hidden] [-ReadOnly] [-System] []
Get-ChildItem [[-Filter] ] -LiteralPath [-Include ] [-Exclude ] [-Recurse] [-Depth ] [-Force] [-Name] [-UseTransaction] [-Attributes ] [-Directory] [-File] [-Hidden] [-ReadOnly] [-System] []
Sadly, that is the only way to do it, according to about_Functions_Advanced_Parameters
Here is an excerpt:
You can specify only one ParameterSetName value in each argument and only
one ParameterSetName argument in each Parameter attribute. To indicate that
a parameter appears in more than one parameter set, add additional Parameter
attributes.
The following example explicitly adds the Summary parameter to the Computer
and User parameter sets. The Summary parameter is mandatory in one parameter
set and optional in the other.
Param
(
[parameter(Mandatory=$true,
ParameterSetName="Computer")]
[String[]]
$ComputerName,
[parameter(Mandatory=$true,
ParameterSetName="User")]
[String[]]
$UserName
[parameter(Mandatory=$false, ParameterSetName="Computer")]
[parameter(Mandatory=$true, ParameterSetName="User")]
[Switch]
$Summary
)
For more information about parameter sets, see Cmdlet Parameter Sets
in the MSDN library.
There is a better way, but it's a design solution rather than a technical one.
The problem is actually that your function is doing too many things. One might say it's violating the single responsibility principle. Each task it performs has two separate parameter sets. The tasks and their parameter sets are:
Build a connection string
Manual (user name and password)
Auto (OS account authentication)
Sending a query to the database
XML data
Path to XML file containing data
Since each task has its own different parameters sets, your function ends up needing the Cartesian product of them (Manual & XML, Auto & XML, Manual & path, Auto & path).
Any time you find yourself in one of these "Cartesian product" parameter situations, it's almost always a sign that you can move one piece of functionality into a separate function and make the new function's result a parameter to the original. In this case, you can split it up into New-ConnectionString and Install-WidgetData, and Install-WidgetData can accept a full connection string as a parameter. This removes the logic of building the connection string from Install-WidgetData, condensing several parameters into one and halving the number of parameter sets needed.
function New-ConnectionString(
[Parameter(Mandatory=$True, Position=0)] # Makes it mandatory for all parameter sets
[ValidateNotNullOrEmpty()]
[string[]]$ComputerName,
[Parameter(ParameterSetName="AutoConnect", Mandatory=$True)]
[switch]$UseIntegratedSecurity,
[Parameter(ParameterSetName="ManualConnect", Mandatory=$True, Position=1)]
[ValidateNotNullOrEmpty()]
[string]$UserName,
[Parameter(ParameterSetName="ManualConnect", Mandatory=$True, Position=2)]
[ValidateNotNullOrEmpty()]
[string]$Password
) {
# ... Build connection string up
return $connString
}
function Install-WidgetData(
[Parameter(Mandatory=$True, Position=0)]
[ValidateNotNullOrEmpty()]
[string]$ConnectionString,
[Parameter(ParameterSetName="Path", Mandatory=$True, Position=1)]
[ValidateNotNullOrEmpty()]
[string]$Path,
[Parameter(ParameterSetName="Xml", Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[string[]]$Xml
) {
# Do installation
}
You can see that this did what you want by invoking help on the commands:
PS C:\> help New-ConnectionString
NAME
New-ConnectionString
SYNTAX
New-ConnectionString [-ComputerName] <string[]> -UseIntegratedSecurity [<CommonParameters>]
New-ConnectionString [-ComputerName] <string[]> [-UserName] <string> [-Password] <string> [<CommonParameters>]
...
PS C:\> help Install-WidgetData
NAME
Install-WidgetData
SYNTAX
Install-WidgetData [-ConnectionString] <string> [-Path] <string> [<CommonParameters>]
Install-WidgetData [-ConnectionString] <string> -Xml <string[]> [<CommonParameters>]
...
Then you call them something like this:
Install-WidgetData (New-ConnectionString 'myserver.example.com' -UseIntegratedSecurity) `
-Path '.\my-widget-data.xml'
You can store the result of New-ConnectionString in a variable if you want, of course. You also get some additional features from doing this refactor:
New-ConnectionString's return value can be reused for any number of functions that require a connection string.
Callers can obtain connection strings from other sources if they prefer
Callers can forego your New-ConnectionString in favor of doing it themselves if they need to use features you didn't provide access to
Well, this is the most succinct way. More succinct then the horrors of switch/case or if/then traps to account for all possible parameter sets!
However, your other option is to write different commandlest for mutually exclusive parameter sets, for example
Install-WidgetDataFromPath
Install-WidgetDataFromXml
Both can call Install-WidgetData script commandlet which you can keep hidden inside the module or using scope modifier to hide it from global scope if you are using only a script file. The internal commandlet can implement shared code for both (or more) user-facing wrappers. Judging from your code I don't think you need to be explained how to implement this.