Mutually exclusive switch parameters - powershell

The function has one required parameter, -Path, and two other mutually exclusive switches. This is not the real function, but a MRE (Minimal Reproducable Example). The default operation is to copy the file to a known location and then remove it.
Do-TheFile [-Path] <String[]> [[-Copy] | [-Remove]]
-Path = filename is mandatory
-CopyOnly = only copy the file, cannot be used with -Remove
-RemoveOnly = only remove the file, cannot be used with -Copy
This is the current code.
param (
[Parameter(Mandatory=$true, Position=0)]
[string[]]$Path
,[Parameter(Mandatory=$false, ParameterSetName='CopyOnly')]
[switch]$CopyOnly
,[Parameter(Mandatory=$false ,ParameterSetName='RemoveOnly')]
[switch]$RemoveOnly
)
The console allows me to specify both -CopyOnly and -RemoveOnly. My expectation was that the console would not permit me to enter both -CopyOnly and -RemoveOnly because they are in different ParameterSets. How can I specify these ParameterSets so that -Copy and -Remove are mutually exclusive?
PS C:\src\t> Do-TheFile -Path t.txt -CopyOnly -RemoveOnly
Do-TheFile: Parameter set cannot be resolved using the specified named parameters. One or more parameters issued cannot be used together or an insufficient number of parameters were provided.

Agree with the others here.
Your code works, as written when using IntelliSense, but PowerShell will not stop you from typing in other valid switches/variables/property names (in either the consolehost, ISE, VSCode, Visual Studio, etc...), that does not mean it would work just because you typed both.
Why make two switches, when you only want to use one option at a time, no matter what.
Just use a simple validation set.
Function Test-MyFunctionTest
{
[cmdletbinding()]
param
(
[Parameter(Mandatory = $true, Position = 0)]
[string[]]$Path,
[Parameter(Mandatory)][ValidateSet('CopyOnly', 'RemoveOnly')]
[string]$FileAction
)
}
# Results
<#
Test-MyFunctionTest -Path $PWD -FileAction CopyOnly
Test-MyFunctionTest -Path $PWD -FileAction RemoveOnly
#>
Otherwise, as you have discovered, you have to code this up yourself. For example:
Function Test-MyFunctionTestAgain
{
[cmdletbinding()]
param
(
[Parameter(Mandatory=$true, Position=0)]
[string[]]$Path,
[switch]$RemoveOnly
)
If($RemoveOnly.IsPresent)
{'Do the remove action'}
Else {'Do the copy action'}
}
Test-MyFunctionTestAgain -Path $PWD
# Results
<#
Do the copy action
#>
Test-MyFunctionTestAgain -Path $PWD -RemoveOnly
# Results
<#
Do the remove action
#>
Update
As for this...
"I agree that this could work. Although, the default operation (using
no switches) is to both Copy and Remove."
... then this...
Function Test-MyFunctionTestMore
{
[cmdletbinding()]
param
(
[Parameter(Mandatory = $true, Position = 0)]
[string[]]$Path,
[Parameter(Mandatory = $false)][ValidateSet('CopyAndRemove', 'CopyOnly', 'RemoveOnly')]
[string]$FileAction = 'CopyAndRemove'
)
Switch ($FileAction)
{
CopyAndRemove {'Do the copy and remove action'}
CopyOnly {'Do the copy only action'}
RemoveOnly {'Do the remove only action'}
}
}
Test-MyFunctionTestMore -Path $PWD
# Results
<#
Do the copy and remove action
#>
Test-MyFunctionTestMore -Path $PWD -FileAction CopyOnly
# Results
<#
Do the copy only action
#>
Test-MyFunctionTestMore -Path $PWD -FileAction RemoveOnly
# Results
<#
Do the remove only action
#>
Or this way, if you are really yearning just to have a switch ;-} ...
Function Test-MyFunctionTestSwitch
{
[cmdletbinding()]
param
(
[Parameter(Mandatory=$true, Position=0)]
[string[]]$Path,
[Parameter(Mandatory = $false)][ValidateSet('CopyAndRemove', 'CopyOnly', 'RemoveOnly')]
[string]$FileAction = 'CopyAndRemove',
[switch]$RemoveOnly
)
If($RemoveOnly.IsPresent)
{
$FileAction = 'RemoveOnly'
'Do the remove only action'
}
ElseIf ($FileAction -eq 'CopyOnly')
{'Do the copy only action'}
Else{'Do the copy and remove action'}
}
Test-MyFunctionTestSwitch -Path $PWD
# Results
<#
Do the copy and remove action
#>
Test-MyFunctionTestSwitch -Path $PWD -FileAction CopyOnly
# Results
<#
Do the copy only action
#>
Test-MyFunctionTestSwitch -Path $PWD -RemoveOnly
# Results
<#
Do the remove only action
#>
Lastly as a point of note:
Trying to emulate some other tools actions, or expecting PowerShell to natively emulate some other tools actions, params, etc., really should not be an expectation.
If you believe PowerShell should have a specific feature, then the option is to submit it to the PowerShell team, to have it upvoted by others for work/inclusion or since PowerShell is open-sourced, you can tool it up and submit it for review/approval of commit.

In contemporary versions of PowerShell, ParameterSets are mutually exclusive.
function greet {
Param(
[String]
$Name = "World",
[Parameter(ParameterSetName="intro")]
[Switch]
$Hello,
[Parameter(ParameterSetName="outro")]
[Switch]
$Farewell
)
if ($Hello) {
echo "Hello, $Name!"
} elseif ($Farewell) {
echo "Farewell, $Name!"
} else {
echo "What's up, $Name"
}
}
This results in the split-groupings that you often see in MS cmdlets:
> greet -?
NAME
greet
SYNTAX
greet [-Name <string>] [-Hello] [<CommonParameters>]
greet [-Name <string>] [-Farewell] [<CommonParameters>]
Doing this requires that the user or the script identify which ParameterSet should be used.
greet -Name "Bob"
> greet -Name "Bob"
greet: Parameter set cannot be resolved using the specified named parameters. One or more parameters issued cannot be used together or an insufficient number of parameters were provided.
This is trying to tell the user they weren't specific enough. See DefaultParameterSetName for how to set it from the script:
There is a limit of 32 parameter sets. When multiple parameter sets are defined, the DefaultParameterSetName keyword of the CmdletBinding attribute specifies the default parameter set. PowerShell uses the default parameter set when it can't determine the parameter set to use based on the information provided to the command.

#Postanote's answer is great and I will prefer it.
However, as #kfsone underlined, DefaultParameterSetName can achieve this with your two switches if you add a ParameterSetName for $Path only and set it as default :
[CmdletBinding(DefaultParameterSetName='CopyAndRemove')]
param (
[Parameter(Mandatory=$true, Position=0)]
[Parameter(ParameterSetName='CopyAndRemove')]
[Parameter(ParameterSetName='CopyOnly')]
[Parameter(ParameterSetName='RemoveOnly')]
[string[]]$Path,
[Parameter(Mandatory=$false, ParameterSetName='CopyOnly')]
[switch]$CopyOnly,
[Parameter(Mandatory=$false ,ParameterSetName='RemoveOnly')]
[switch]$RemoveOnly
)
$Path
$PSCmdlet.ParameterSetName

Related

Is it possible to implement IValidateSetValuesGenerator for script parameter validation?

In the example in the powershell documentation you need to implement a class, like:
Class SoundNames : System.Management.Automation.IValidateSetValuesGenerator { ... }
But, how can you do this in a script based cmdlet? (See "Parameters in scripts" in about_Scripts).
In a script cmdlet the first line must be param( ... ).
Unless "SoundNames" is already defined, then it fails with Unable to find type [SoundNames]
If this type is already defined in the powershell session, this works ... but I'd like a stand-alone script that doesn't require the user do something first to define this type (e.g. dot-sourcing some other file).
I want to define a parameter that accepts only *.txt filenames (for existing files in a specific directory).
You can't do this in a (stand-alone) script, for the reason you state yourself:
The param block (possibly preceded by a [CmdletBinding()] attribute, must be at the very start of the file (leaving using statements aside).
This precludes defining a custom class that implements the IValidateSetValuesGenerator interface for use in your param block.
To work around this limitation, use a [ValidateScript()] attribute:
param(
[ValidateScript({
if (-not (Get-Item -ErrorAction Ignore "$_.txt")) {
throw "$_ is not the (base) name of a *.txt file in the current dir."
}
return $true
})]
[string] $FileName
)
Note that this doesn't give you tab-completion the way that IValidateSetValuesGenerator-based validation would automatically give you.
To also provide tab-completion, additionally use an [ArgumentCompleter()] attribute:
param(
[ValidateScript({
if (-not (Get-Item -ErrorAction Ignore "$_.txt")) {
throw "$_ is not the (base) name of a *.txt file in the current dir."
}
return $true
})]
[ArgumentCompleter({
param($cmd, $param, $wordToComplete)
(Get-Item "$wordToComplete*.txt").BaseName
})]
[string] $FileName
)

PowerShell -Confirm only once per function call

I'm obviously trying to do something wrong, so hoping someone can help since it seems easy and it is probably an easy mistake. Basically I am writing a little script to create new folders and I want to include -Confirm, but I want it to basically ask ONCE if the user wants to continue, instead of asking for each nested folder that is created.
Here's what I have so far
function Create-NewFolderStructure{
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory)]
[string]$NewFolderName,
[Parameter()]
$LiteralPath = (Get-Location),
[Parameter()]
$inputFile = "zChildDirectoryNames.txt"
)
process {
$here = $LiteralPath
$directories = Get-Content -LiteralPath "$($here)\$($inputFile)"
If (-not (Test-Path -Path "$($here)\$($NewFolderName)")){
$directories | ForEach-Object {New-Item -ItemType "directory" -Path "$($here)\$($NewFolderName)" -Name $_}
}
Else {
Write-Host "A $($NewFolderName) folder already exists"
}
}
end {
}
}
The issue is that if I use -Confirm when calling my function, or directly at New-Item - I end up getting the confirmation prompt for every single 'child folder' that is being created. Even with the first prompt, saying "Yes to all" doesn't suppress future prompts
In order to consume the user's answer to the confirmation prompt, you have to actually ask them!
You can do so by calling $PSCmdlet.ShouldContinue():
function Create-NewFolderStructure {
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory, ValueFromPipeline)]
[string]$NewFolderName,
[Parameter()]
$LiteralPath = (Get-Location),
[Parameter()]
$inputFile = "zChildDirectoryNames.txt",
[switch]$Force
)
process {
if(-not $Force){
$yesToAll = $false
$noToAll = $false
if($PSCmdlet.ShouldContinue("Do you want to go ahead and create new directory '${NewFolderName}'?", "Danger!", [ref]$yesToAll, [ref]$noToAll)){
if($yesToAll){
$Force = $true
}
}
else{
return
}
}
# Replace with actual directory creation logic here,
# pass `-Force` or `-Confirm:$false` to New-Item to suppress its own confirmation prompt
Write-Host "Creating new folder ${NewFolderName}" -ForegroundColor Red
}
}
Now, if the user presses [A] ("Yes To All"), we'll remember it by setting $Force to true, which will skip any subsequent calls to ShouldContinue():
PS ~> "folder 1","folder 2"|Create-NewFolderStructure
Danger!
Do you want to go ahead and create new directory 'folder 1'?
[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): A
Creating new folder folder 1
Creating new folder folder 2
PS ~>
I'd strongly suggest reading through this deep dive to better understand the SupportsShouldProcess facility: Everything you wanted to know about ShouldProcess
Mathias R. Jessen's helpful answer provides an alternative to using the common -Confirm parameter, via a slightly different mechanism based on a custom -Force switch that inverts the logic of your own attempt (prompt is shown by default, can be skipped with -Force).
If you want to make your -Confirm-based approach work:
Call .ShouldProcess() yourself at the start of the process block, which presents the familiar confirmation prompt, ...
... and, if the method returns $true - indicating that the use confirmed - perform the desired operations after setting a local copy of the $ConfirmPreference preference variable to 'None', so as to prevent the cmdlets involved in your operations to each prompt as well.
function New-FolderStructure {
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory, ValueFromPipeline)]
[string] $NewFolderName,
[string] $LiteralPath = $PWD,
[string] $InputFile = "zChildDirectoryNames.txt"
)
# This block is called once, before pipeline processing begins.
begin {
$directories = Get-Content -LiteralPath "$LiteralPath\$InputFile"
}
# This block is called once if parameter $NewFolderName is bound
# by an *argument* and once for each input string if bound via the *pipeline*.
process {
# If -Confirm was passed, this will prompt for confirmation
# and return `$true` if the user confirms.
# Otherwise `$true` is automatically returned, which also happens
# if '[A] Yes to All' was chosen for a previous input string.
if ($PSCmdlet.ShouldProcess($NewFolderName)) {
# Prevent the cmdlets called below from *also* prompting for confirmation.
# Note: This creates a *local copy* of the $ConfirmPreference
# preference variable.
$ConfirmPreference = 'None'
If (-not (Test-Path -LiteralPath "$LiteralPath\$NewFolderName")) {
$directories | ForEach-Object { New-Item -ItemType Directory -Path "$LiteralPath\$NewFolderName" -Name $_ }
}
Else {
Write-Host "A $NewFolderName folder already exists"
}
}
}
}
Note:
I've changed your function's name from Create-NewFolderStructure to New-FolderStructure to comply with PowerShell's naming conventions, using the approved "verb" New and streamlined the code in a number of ways.
ValueFromPipeline was added as a property to the $NewFolderName parameter so as to support passing multiple values via the pipeline (e.g.
'foo', 'bar' | New-FolderStructure -Confirm)
Note that the process block is then called for each input string, and will also prompt for each, unless you respond with [A] Yes to All); PowerShell remembers this choice between process calls and makes .ShouldProcess() automatically return $true in subsequent calls; similarly, responding to [L] No to All automatically returns $false in subsequent calls.
The need to set $ConfirmPreference to 'None' stems from the fact that PowerShell automatically translates the caller's use of -Confirm into a function-local $ConfirmPreference variable with value 'Low', which makes all cmdlets that support -Confirm act as if -Confirm had been passed.
.ShouldProcess() does not pick up in-function changes to the value of this $ConfirmPreference copy, so it is OK to set it to 'None', without affecting the prompt logic of subsequent process invocations.

PowerShell Set Mandatory Property Based on Condition

I need to prompt a user to enter the full path of an executable only if it cannot be found in the system PATH.
I have a parameter block like this:
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)] [string]$oc_cli,
)
This determines if the executable exists in PATH:
$oc_in_path = [bool] (Get-Command "oc.exe" -ErrorAction Ignore)
I want to set the $oc_cli parameter as mandatory only if $oc_in_path is FALSE.
I have tried
$oc_in_path = [bool] (Get-Command "oc.exe" -ErrorAction Ignore)
function example-function {
param (
[Parameter(Mandatory=$oc_in_path)] [string]$oc_cli,
)
Write-Host "Do stuff"
}
example-function
which throws error Attribute argument must be a constant or a script block
I have also tried
[CmdletBinding()]
param (
if (! ( [bool] (Get-Command "oc.exe" -ErrorAction Ignore) )) {
[Parameter(Mandatory=$true)] [string]$oc_cli
}
)
How can I set the Mandatory property based on a condition?
Try the following, which relies on being able to specify default parameter values via arbitrary commands, using $(...), the subexpression operator:
[CmdletBinding()]
param (
[Parameter()] # in the absence of property values, this is optional.
[string] $oc_cli = $(
if ($cmd = Get-Command -ea Ignore oc.exe) { # oc.exe found in $env:PATH
$cmd.Path
} else { # prompt user for the location of oc.exe
do {
$userInput = Read-Host 'Specify the path of executable ''oc.exe'''
} until ($cmd = (Get-Command -ea Ignore $userInput))
$cmd.Path
}
)
)
"`$oc_cli: [$oc_cli]"
Note:
The Mandatory property of the Parameter attribute is not used, as it would - in the absence of an argument being passed - unconditionally prompt for an argument (parameter value).
The above would still accept a potentially non-existent $oc_cli argument if passed explicitly; you could use a ValidateScript attribute to prevent hat.

Better way of multiplayer IFs with PowerShell

Im wondering if I am tackling this in the best way or if there is a better way of achieving my task.
I've written a function in PowerShell which takes different parameters, but some of the parameters won't work together.
So for example if I'm running the function and specifying a computerName parameter then I can't also pass list of multiple computer names.
I know I can write multiple If statements as along the lines if If(($computerName) - and ($computerList)){Then write and error}
but there are several parameters not just two, so do I need to do an if for each set of parameters someone could type in, or is there a better way of me tackling this?
currently I have multiple Ifs like If $computerName -and !(log file) and $computerlist) then write an error etc.
The PowerShell-idiomatic solution here is to declare a parameter that accepts either:
function Get-Stuff
{
param(
[Parameter(ValueFromPipeline = $true)]
[string[]]$ComputerName
)
process {
foreach($Computer in $ComputerName){
# do stuff with each individual -ComputerName argument
}
}
}
Now the user can do both
Get-Stuff -ComputerName oneComputerName
... and
"many","computer","names" |Get-Stuff
or for that matter
$computers = Get-Content .\Computers.txt
Get-Stuff -ComputerName $computers
# or
$computers |Get-Stuff
I know I can write multiple If statements as along the lines of If(($computerName) -and ($computerList)){Then write and error}
You can, but this is generally a bad idea - to test whether an argument value was passed to a parameter is better done through the automatic variable $PSBoundParameters:
function Get-Stuff
{
param(
[string]$AParameter,
[string]$ADifferentOne
)
if($PSBoundParameters.ContainsKey('AParameter')){
# an argument was definitely provided to $AParameter
}
if($PSBoundParameters.ContainsKey('ADifferentOne')){
# an argument was definitely provided to $ADifferentOne
}
}
The answer to the implied question of "how do I declare and work with mutually exclusive parameters" is parameter sets:
function Verb-Noun
{
param(
[Parameter(Mandatory = $true, ParameterSetName = 'SingleComputer')]
[string]$ComputerName,
[Parameter(Mandatory = $true, ParameterSetName = 'MultipleComputers')]
[string[]]$ComputerList
)
if($PSCmdlet.ParameterSetName -eq 'SingleComputer'){
# just one, we can deal with $ComputerName
}
else {
# we got multiple names via $ComputerList
}
}
PowerShell now recognizes two distinct parameter sets, each of which only accepts one of our parameters:
PS ~> Get-Command Verb-Noun -Syntax
Verb-Noun -ComputerName <string> [<CommonParameters>]
Verb-Noun -ComputerList <string[]> [<CommonParameters>]

Parameter-specific usage of "SupportsShouldProcess

Please have a look at the code snippet bellow. I am making use of the SupportsShouldProcess parameter within the CmdletBinding decorator, which provides me with two additional "builtin" functions namely -whatif and -confirm
However, having to use the parameter SupportsShouldProcess on top provides the ability to all functions/parameters that are going to be used by the user. For the example below, my ParameterSetname="Help" also gets the ability.
So the question is: How can I provide -whatif and -confirm only to specific parameters/functions?
[CmdletBinding(DefaultParameterSetName="Help", SupportsShouldProcess)]
Param(
[parameter(ParameterSetName="Help")]
[switch]$Help,
# Client Management
[parameter(ParameterSetName="MoveAdComputerObjects", Position=0)]
[switch]$MoveAdComputerObjects,
# Additional parameters
[parameter(ParameterSetName="MoveAdComputerObjects", Mandatory)]
[ValidatePattern("foobar")]
[string]$Src,
[parameter(ParameterSetName="MoveAdComputerObjects", Mandatory)]
[ValidatePattern("foobar")]
[string]$Dst
)
Function Help
{
Write-Host
Get-Command -Syntax $MyInvocation.ScriptName
}
Function MoveAdComputerObjects
{
[CmdletBinding(SupportsShouldProcess)]
param(
[ValidatePattern("foobar")]
[string]$Src,
[ValidatePattern("foobar")]
[string]$Dst
)
# Do something useful
Write-Host $Src, $Dst
}
switch($PSCmdlet.ParameterSetName)
{
"Help" { Help }
"MoveAdComputerObjects" { MoveAdComputerObjects -Src $Src -Dst $Dst }
}