Powershell ParameterSet error when two or more parameters are used - powershell

I have a script that prints out log information, basically text. The script has 3 parameters: 'ID', 'Text' and 'Day', where I could filter the information based on those parameters. It's like this:
function Get-Log{
[CmdletBinding(DefaultParameterSetName="All")]
Param(
[Parameter(
ParameterSetName="ByID"
)]
[Int]$ID,
[Parameter(
ParameterSetName="ByText"
)]
[String]$Text,
[Parameter(
ParameterSetName="ByDay"
)]
[String]$Day
)
...
}
It works fine if I call the function with no or only one parameter, Ex.:
Get-Log
Get-Log -Text "sample"
But when I use two or more parameters, like:
Get-Log -Text "sample" -Day "29/04/2016"
It spills out parameter set error:
Get-Log : Parameter set cannot be resolved using the specified named parameters
+ get-log -Text "sample" -Day "29/04/2016"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Get-Change], ParameterBind
ingException
+ FullyQualifiedErrorId : AmbiguousParameterSet,Get-Change
Is there a way to specify a different parameter set used when 2 or more parameters are specified? Or maybe a better way to do that?

The way I use parameter sets is to define one "mode" of the script along with the parameters that "mode" would have.
Example, the RunOnce param set has two mandatory parameters, Server and Date.
[Parameter(Mandatory=$false,ParameterSetName="RunOnce",
HelpMessage="Enter ServerName to schedule reboot for.")]
[switch]$RunOnce,
[Parameter(Mandatory=$true,ParameterSetName="RunOnce")]
[string]$server,
[Parameter(Mandatory=$true,ParameterSetName="RunOnce")]
[string]$date,
Used as such Function -RunOnce -Server "blah" -Date "04/29/2016 18:20"

Related

Reject parameters not in Param() statement

I have a powershell script that starts with
Param([switch]$NoDownload, [switch]$NoUnpack, [switch]$NoExtract, [switch]$NoImport, [switch]$NoBackup)
and I was very happy because I thought that it would provide automatic parameter validation. Until one day I made a mistake and wrote:
powershell -f myscript.ps1 -NoDowload
(notice the lack of n), and it happily downloaded something I didn't want it to.
How do I tell the powershell parameter handling machinery that the only valid parameters are the ones I explicitly state in the Param statement?
Add a CmdletBinding attribute to the param() block - this makes PowerShell treat your script/function like a cmdlet - rather than a "simple" function - and it will apply much more rigorous parameter binding validation, including throwing errors when you attempt to bind a non-existing parameter name:
[CmdletBinding()]
param(
[switch]$NoDownload,
[switch]$NoUnpack,
[switch]$NoExtract,
[switch]$NoImport,
[switch]$NoBackup
)
PS ~> .\myScript.ps1 -NoDowload
myScript.ps1 : A parameter cannot be found that matches parameter name 'NoDowload'.
At line:1 char:16
+ .\myScript.ps1 -NoDowload
+ ~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [myScript.ps1], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : NamedParameterNotFound,myScript.ps1
Adding an explicit [Parameter()] attribute to any defined parameter will also implicitly make PowerShell treat your script/function as "advanced", even in the absence of a [CmdletBinding()] attribute:
param(
[switch]$NoDownload,
[switch]$NoUnpack,
[switch]$NoExtract,
[switch]$NoImport,
[switch]$NoBackup,
[Parameter(Mandatory = $false, DontShow = $true)]
$DummyParameterThatTriggersCmdletBinding
)

Consume $args while also using parameter set names

Consider the following toy example script test.ps1:
Param(
[Parameter(ParameterSetName='readfile',Position=0,Mandatory=$True)]
[string] $FileName,
[Parameter(ParameterSetName='arg_pass',Mandatory=$True)]
[switch] $Ping
)
if ($Ping.isPresent) {
&$env:ComSpec /c ping $args
} else {
Get-Content $FileName
}
The desired effect would be that
.\test.ps1 FILE.TXT
displays the contents of FILE.TXT and
.\test.ps1 -Ping -n 5 127.0.0.1
pings localhost 5 times.
Unfortunately, the latter fails with the error
A parameter cannot be found that matches parameter name 'n'.
At line:1 char:18
+ .\test.ps1 -Ping -n 5 127.0.0.1
+ ~~
+ CategoryInfo : InvalidArgument: (:) [test.ps1], ParameterBindingException
+ FullyQualifiedErrorId : NamedParameterNotFound,test.ps1
This is just a minimal example, of course.
In general, I am looking for a way to introduce a [switch] parameter to my script that lives inside its own parameter set and when that switch is present, I want to consume all remaining arguments from the commandline and pass them on to another commandline application. What would be the way to do this in PowerShell?
You can use the ValueFromRemainingArguments parameter attribute. I would also recommend specifying a default parameter set name in CmdletBinding. Example:
[CmdletBinding(DefaultParameterSetName="readfile")]
param(
[parameter(ParameterSetName="readfile",Position=0,Mandatory=$true)]
[String] $FileName,
[parameter(ParameterSetName="arg_pass",Mandatory=$true)]
[Switch] $Ping,
[parameter(ParameterSetName="arg_pass",ValueFromRemainingArguments=$true)]
$RemainingArgs
)
if ( $Ping ) {
ping $RemainingArgs
}
else {
Get-Content $FileName
}
(Aside: I don't see a need for & $env:ComSpec /c. You can run commands in PowerShell without spawning a copy of cmd.exe.)

Parameter validateset wildcard

Is it possible to make a Paramater validateset work with a wildcard?
I would want on the * places to accept 0-100.
param
(
[Parameter(Mandatory=$True)]
[validateset("6.1.*.*")]
[string]$variable
)
Error message:
Cannot validate argument on parameter 'variable'. The argument "6.1.1.0" does not belong to the set "6.1.." specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.
+ CategoryInfo : InvalidData: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : ParameterArgumentValidationError
Since it looks like you're looking to validate a version, you may want to declare the parameter of the type [version] and use the ValidateScript attribute to validate the value rather than using string matching:
function Test-Version {
param(
[ValidateScript({
$_.Major -eq '6' -and
$_.Minor -eq '1' -and
$_.Build -in (0..100) -and
$_.Revision -in (0..100) -or
$(throw 'Wrong Version')
})]
[version]$Version
)
}
No, that's what [ValidatePattern()] is for:
param(
[Parameter(Mandatory=$True)]
[ValidatePattern('6\.1\.\d{1,3}\.\d{1,3}')]
[string]$variable
)
It takes a regular expression as the parameter.
[ValidateSet()] is meant to be used if there is a small, constant set of values. PowerShell also provides autocompletion for these. For example:
[ValidateSet('Windows', 'Mac', 'Linux')
$OperatingSystem
See this article for more parameter validation attributes.

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.

Powershell: Args/params not being populated

I have a PowerShell script:
param (
[Parameter(Mandatory=$true)][string]$input,
[Parameter(Mandatory=$true)][string]$table
)
Write-Host "Args:" $Args.Length
Get-Content $input |
% { [Regex]::Replace($_, ",(?!NULL)([^,]*[^\d,]+[^,]*)", ",'`$1'") } |
% { [Regex]::Replace($_, ".+", "INSERT INTO $table VALUES (`$1)") }
The Write-Host part is for debugging.
I run it as .\csvtosql.ps1 mycsv.csv dbo.MyTable (from powershell shell), and get
Args: 0
Get-Content : Cannot bind argument to parameter 'Path' because it is an empty s
tring.
At C:\temp\csvtosql.ps1:7 char:12
+ Get-Content <<<< $input |
+ CategoryInfo : InvalidData: (:) [Get-Content], ParameterBinding
ValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorEmptyStringNotAl
lowed,Microsoft.PowerShell.Commands.GetContentCommand
I get exactly the same error with any parameters that I pass, also the same error if I try to use named parameters.
What can cause parameters not to be passed in?
UPDATE: PowerShell ISE asks me for these parameters using GUI prompts, then gives me the same error about them not being passed in.
Unless you marked a parameter with the ValueFromRemainingArguments attribute (indicates whether the cmdlet parameter accepts all the remaining command-line arguments that are associated with this parameter), Args is "disabled". If all you need is the arguments count call the special variable:
$PSBoundParameters.Count
Do not mix. Make use of $args or parameters.
Also do note that $input is a special variable, don't declare it as a parameter. http://dmitrysotnikov.wordpress.com/2008/11/26/input-gotchas/
You're calling your script with positional parameters (i.e. unnamed) and PowerShell doesn't know how to map them to your script parameters. You need to either call your script using the parameter names:
.\csvtosql.ps1 -input mycsv.csv -table dbo.MyTable
or update your script to specify your preferred order of positional parameters:
param (
[Parameter(Mandatory=$true,Position=0)]
[string]
$input,
[Parameter(Mandatory=$true,Position=1)]
[string]
$table
)