I have this script that can be called in two ways:
MyScript -foo path\to\folder
or
MyScript -bar path\to\folder
(That is, I can either pass a switch plus a folder or a string argument plus a folder.)
I have tried to put parameter declarations into my script as to reflect that syntax:
param(
[parameter(Mandatory=$false)] [switch]$foo,
[parameter(Mandatory=$false)] [String]$bar,
[parameter(Mandatory=$true)] [System.IO.FileInfo]$path
)
But then I have to pass path explicitly to invoke the script:
MyScript -l -path path\to\folder
So (how) can I do that making both bar and path positional parameters?
Note: If I have picked an extraordinarily stupid syntax for invoking the script, I can still change it.
A couple of things: You need to use parameter sets to tell PowerShell that there are mutually exclusive ways to invoke your script; that is to say, you cannot use the switch and the string at the same time. The sets also serve to allow you to set the position of both $bar and $filepath to be at index 0. Switches don't need to be positionally placed as they are not ambiguous to the binder and be placed anywhere. Also, at least one parameter in each set should be mandatory.
function test-set {
[CmdletBinding(DefaultParameterSetName = "BarSet")]
param(
[parameter(
mandatory=$true,
parametersetname="FooSet"
)]
[switch]$Foo,
[parameter(
mandatory=$true,
position=0,
parametersetname="BarSet"
)]
[string]$Bar,
[parameter(
mandatory=$true,
position=1
)]
[io.fileinfo]$FilePath
)
#"
Parameterset is: {0}
Bar is: '{1}'
-Foo present: {2}
FilePath: {3}
"# -f $PSCmdlet.ParameterSetName, $bar, $foo.IsPresent, $FilePath
}
The CmdletBinding attribute is needed to specify which parameter set should be the default if the function is invoked without parameters.
Here's the syntax help for the above configuration:
PS> test-set -?
NAME
test-set
SYNTAX
test-set [-Bar] <string> [-FilePath] <FileInfo> [<CommonParameters>]
test-set [-FilePath] <FileInfo> -Foo [<CommonParameters>]
And here's the output for various invocations:
PS> test-set barval C:\temp\foo.zip
Parameterset is: BarSet
Bar is: 'barval'
-Foo present: False
FilePath: C:\temp\foo.zip
PS> test-set -foo c:\temp\foo.zip
Parameterset is: FooSet
Bar is: ''
-Foo present: True
FilePath: c:\temp\foo.zip
Hope this helps.
As explained here you can specify the parameter position of your parameter.
[parameter(Position=0)]
Related
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
I have a Powershell function that takes 3 arguments (two of them optional), parameters for function are defined like this:
Param(
[parameter(Mandatory=$true, ValueFromPipeline=$true)]
[String]
$text,
[parameter(Mandatory=$false)]
[String]
$from = "auto",
[parameter(Mandatory=$false)]
[String]
$to = "lt"
)
If I run the script with 1, 2 or 3 command line parameters - everything works fine. If I run with no parameters, but pass value from pipeline - everything works fine. Working examples:
Foo "hello"
Foo "hola" es
Foo "hola" es lt
"hello" | Foo
I want to be able to pass value from pipeline to the first argument and provide additional command line parameters, but I recieve error: "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".
There is one working example, if i provide parameters by name:
"hello" | Foo -from en -to ru
But I don't really like typing the name of parameter all the time. Can this be ommited, is there any shortcut? Not working (wishlist) examples (any will do):
"hello" | Foo en es
"hello" | Foo - en es
"hello" | Foo -- en es
You could make -Text the last positional parameter:
Param(
[Parameter(Position=2, Mandatory=$true, ValueFromPipeline=$true)]
[String]$text,
[Parameter(Position=0, Mandatory=$false)]
[String]$from = "auto",
[Parameter(Position=1, Mandatory=$false)]
[String]$to = "lt"
)
but then you'll have to call the function with named parameters when not reading the manadatory parameter from a pipeline or calling the function with 3 arguments:
Foo -Text 'something'
otherwise you'll be prompted for input:
PS C:\> Foo 'something'
cmdlet Foo at command pipeline position 1
Supply values for the following parameters: _
So, basically you can choose in which scenario you want to provide parameter names:
for the optional parameters when reading from the pipeline, or
for the mandatory parameter when not reading from the pipeline.
I have a script that i'd like the user to be able to enter a string or use a file(array) that my script can cycle through.
Can this be done with a parameter?
I'd like to be able to do something like this
script.ps1 -file c:\users\joerod\desktop\listofusers.txt
or
script.ps1 -name "john doe"
Sure, but you will need to pick the default parameterset to use when a positional parameter is used since the type of both parameters is a string e.g.:
[CmdletBinding(DefaultParameterSetName="File")]
param(
[Parameter(Position=0, ParameterSetName="File")]
[string]
$File,
[Parameter(Position=0, ParameterSetName="Name")]
[string]
$Name
)
if ($psCmdlet.ParameterSetName -eq "File") {
... handle file case ...
}
else {
... must be name case ...
}
Where the DefaultParameterSetName is essential is when someone specifies this:
myscript.ps1 foo.txt
If the default parametersetname specified, PowerShell can't tell which parameterset should be used since both position 0 parameters are the same type [string]. There is no way to disambiguate which parameter to place the argument in.
I'm writing a PowerShell script that's a wrapper to an .exe. I want to have some optional script params, and pass the rest directly to the exe. Here's a test script:
param (
[Parameter(Mandatory=$False)] [string] $a = "DefaultA"
,[parameter(ValueFromRemainingArguments=$true)][string[]]$ExeParams # must be string[] - otherwise .exe invocation will quote
)
Write-Output ("a=" + ($a) + " ExeParams:") $ExeParams
If I run with the a named param, everything is great:
C:\ > powershell /command \temp\a.ps1 -a A This-should-go-to-exeparams This-also
a=A ExeParams:
This-should-go-to-exeparams
This-also
However, if I try to omit my param, the first unnamed param is assigned to it:
C:\ > powershell /command \temp\a.ps1 This-should-go-to-exeparams This-also
a=This-should-go-to-exeparams ExeParams:
This-also
I would expect:
a=DefaultA ExeParams:
This-should-go-to-exeparams
This-also
I tried adding Position=0 to the param, but that produces the same result.
Is there a way to achieve this?
Maybe a different parameter scheme?
By default, all function parameters are positional. Windows PowerShell assigns position numbers to parameters in the order in which the parameters are declared in the function. To disable this feature, set the value of the PositionalBinding argument of the CmdletBinding attribute to $False.
have a look at How to disable positional parameter binding in PowerShell
function Test-PositionalBinding
{
[CmdletBinding(PositionalBinding=$false)]
param(
$param1,$param2
)
Write-Host param1 is: $param1
Write-Host param2 is: $param2
}
The main answer still works in version 5 (according to comments, it may have been broken for a while in version 2).
There is another option: add Position to the ValueFromRemainingArgs parameter.
Sample CommandWrapper.ps1:
param(
$namedOptional = "default",
[Parameter(ValueFromRemainingArguments = $true, Position=1)]
$cmdArgs
)
write-host "namedOptional: $namedOptional"
& cmd /c echo cmdArgs: #cmdArgs
Sample output:
>commandwrapper hello world
namedOptional: default
cmdArgs: hello world
This appears to follow from PowerShell assigning parameter positions from the first parameter with a Position designated.
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.