Using parametersets to constrain more than one mutually exclusive dependency - powershell

When a script's signature is differentiated (singularly) by an argument, parameter sets make sense to me.
Example:
.\myscript.ps1 -InputFile [-Optional1] [-Optional2]...
.\myscript.ps1 -ArrayOfNames [-Optional1] [-Optional2]...
My question is: Are parameter sets the logical choice when you wish to support parallel (or multiple) dependencies as explained below?
Here is my current scenario.
I'm adding support for an existing script that queries logs containing time stamps. The script should accept a csv file or an array of smtp addresses to identify which users to query.
The script should also support begin and end date parameters or an integer value to facilitate reporting n number of days in the past, calculated from the current date.
The outcome I wish to support is:
.\myScript -InputFile -StartDate -EndDate [-Optional1] [-Optional2]...
.\myScript -InputFile -LastNumDays [-Optional1] [-Optional2]...
.\myScript -Smtp -StartDate -EndDate [-Optional1] [-Optional2]...
.\myScript -Smtp -LastNumDays [-Optional1] [-Optional2]...
Either of the following two parameter definitions work well if I don't attempt to combine my two requirements:
[Parameter(Mandatory=$true, ParameterSetName="Input")]
[ValidateScript({Test-Path -Path $_ -PathType Leaf})][string] $InputFile,
[Parameter(Mandatory=$true, ParameterSetName="NoInput")]
[ValidateNotNullOrEmpty()][String[]] $Smtp
Get-Help displays expected usage as:
.\myScript.ps1 -InputFile <String> [<CommonParameters>]
.\myScript.ps1 -Smtp <String[]> [<CommonParameters>]
If I configure the following instead:
[Parameter(Mandatory=$true, ParameterSetName="NotRange")]
[ValidateNotNullOrEmpty()][int] $LastNumDays = 30, # init 30 days default
[Parameter(Mandatory=$true, ParameterSetName="Range")]
[ValidateNotNullOrEmpty()][Alias("Start")] [DateTime] $StartDate,
[Parameter(Mandatory=$true, ParameterSetName="Range")]
[ValidateNotNullOrEmpty()][Alias("End")] [DateTime] $EndDate
Get-Help displays expected usage as:
.\myScript.ps1 -LastNumDays <Int32> [<CommonParameters>]
.\myScript.ps1 -StartDate <DateTime> -EndDate <DateTime> [<CommonParameters>]
The problem is that I can't seem to incorporate both my dependencies as described at the beginning of this post. An example of just one of my unsuccessful attempts to combine these two logical dependencies using parameter sets is as follows:
[Parameter(Mandatory=$true, ParameterSetName="Input")]
[ValidateScript({Test-Path -Path $_ -PathType Leaf})][string] $InputFile,
[Parameter(Mandatory=$true, ParameterSetName="NoInput")]
[ValidateNotNullOrEmpty()][String[]] $Smtp,
[Parameter(Mandatory=$true, ParameterSetName="NotRange")]
[Parameter(Mandatory=$true, ParameterSetName="Input")]
[Parameter(Mandatory=$true, ParameterSetName="NoInput")]
[ValidateNotNullOrEmpty()][int] $LastNumDays = 30, # init 30 days default
[Parameter(Mandatory=$true, ParameterSetName="Range")]
[Parameter(Mandatory=$true, ParameterSetName="Input")]
[Parameter(Mandatory=$true, ParameterSetName="NoInput")]
[ValidateNotNullOrEmpty()][Alias("Start")] [DateTime] $StartDate,
[Parameter(Mandatory=$true, ParameterSetName="Range")]
[Parameter(Mandatory=$true, ParameterSetName="Input")]
[Parameter(Mandatory=$true, ParameterSetName="NoInput")]
[ValidateNotNullOrEmpty()][Alias("End")] [DateTime] $EndDate
Get-Help results are incorrect b/c the first two usage statements allow LastNumDays and Start/EndDate parameters to be used at the same time:
.\myScript.ps1 -InputFile <String> -LastNumDays <Int32> -StartDate <DateTime> -EndDate <DateTime> [<CommonParameters>]
.\myScript.ps1 -Smtp <String[]> -LastNumDays <Int32> -StartDate <DateTime> -EndDate <DateTime> [<CommonParameters>]
.\myScript.ps1 -LastNumDays <Int32> [<CommonParameters>]
.\myScript.ps1 -StartDate <DateTime> -EndDate <DateTime> [<CommonParameters>]
I've tested different combinations of mandatory true/false and including/omitting my named parameter sets with no success.
I'm suspecting now that my requirements may not be fitting for the use case parameter sets were intended to support but am left to wonder what pattern and practice I should be using instead.
How can I properly define usage syntax for these two dependencies if not using parameter sets? I feel that I must avoid resorting to tests in my code that announce dependencies that are not defined in Get-Help.
Thank you!

You need to make each parameter set unique, so PowerShell can distinguish one from another. For simplicity reasons I'll name the parameter sets A through D:
A: -InputFile -StartDate -EndDate
B: -InputFile -LastNumDays
C: -Smtp -StartDate -EndDate
D: -Smtp -LastNumDays
Now associate each parameter with each parameter set it appears in:
Param(
[Parameter(Mandatory=$true, ParameterSetName="A")]
[Parameter(Mandatory=$true, ParameterSetName="B")]
[string]$InputFile,
[Parameter(Mandatory=$true, ParameterSetName="C")]
[Parameter(Mandatory=$true, ParameterSetName="D")]
[String[]]$Smtp,
[Parameter(Mandatory=$true, ParameterSetName="B")]
[Parameter(Mandatory=$true, ParameterSetName="D")]
[int]$LastNumDays,
[Parameter(Mandatory=$true, ParameterSetName="A")]
[Parameter(Mandatory=$true, ParameterSetName="C")]
[DateTime]$StartDate,
[Parameter(Mandatory=$true, ParameterSetName="A")]
[Parameter(Mandatory=$true, ParameterSetName="C")]
[DateTime]$EndDate
)
Output:
PS C:\> .\test.ps1 -?
test.ps1 -InputFile <string> -LastNumDays <int> [<CommonParameters>]
test.ps1 -InputFile <string> -StartDate <datetime> -EndDate <datetime> [<CommonParameters>]
test.ps1 -Smtp <string[]> -LastNumDays <int> [<CommonParameters>]
test.ps1 -Smtp <string[]> -StartDate <datetime> -EndDate <datetime> [<CommonParameters>]
Note that it's pointless to provide a default value for a mandatory parameter (-LastNumDays), because you're required to provide a value anyway.

Related

Levels of parameter sets for a cmdlet

What I want is to get-help to output the below for my cmdlet
SYNTAX
Get-Somehting -A <Object> [-Package <Object>] [<CommonParameters>]
Get-Somehting -A <Object> [-Names <string[]>] [<CommonParameters>]
Get-Somehting -B <Object> [-Package <Object>] [<CommonParameters>]
Get-Somehting -B <Object> [-Names <string[]>] [<CommonParameters>]
The following
Function Get-Somehting{
[CmdletBinding()]
Param(
[Parameter(Mandatory,
ParameterSetName='A')]
[System.Object]$A,
[Parameter(Mandatory,
ParameterSetName='B')]
[System.Object]$B,
[Parameter(Mandatory,
ParameterSetName='package')]
[System.Object]$Package,
[Parameter(Mandatory,
ParameterSetName='Names')]
[String[]]$Names)
Process{
}
}
gives me
SYNTAX
Get-Somehting -A <Object> [-Package <Object>] [-Names <string[]>] [<CommonParameters>]
Get-Somehting -B <Object> [-Package <Object>] [-Names <string[]>] [<CommonParameters>]
One of the most annoying and coolest features is parameter set names. What's cool is you can properly define your input for the 'path' you want your code to take, and rather check what 'path' was input instead of checking all the different input parameters. You want to be very careful when defining them, however, as pwsh REALLY wants to end up with just one invoked 'path', so you have to properly define your expected paths:
Function Get-Something {
[CmdletBinding()]
Param(
[Parameter(Mandatory,ParameterSetName = 'AName')]
[Parameter(Mandatory,ParameterSetName = 'APackage')]
[System.Object]$A,
[Parameter(Mandatory,ParameterSetName = 'BName')]
[Parameter(Mandatory,ParameterSetName = 'BPackage')]
[System.Object]$B,
[Parameter(Mandatory,ParameterSetName = 'AName')]
[Parameter(Mandatory,ParameterSetName = 'BName')]
[System.Object]$Package,
[Parameter(Mandatory,ParameterSetName = 'APackage')]
[Parameter(Mandatory,ParameterSetName = 'BPackage')]
[String[]]$Names)
Process {
$PSCmdlet.ParameterSetName
}
}
It's kind of messy, but it also makes sense as in your case you want to always have an "a or b" path and with that always define either names or packages, so you end up with 4 possible 'paths'.

Function's multiple ParameterSets work when being declared from a script, but not when being imported from a module

I wrote a 'wrapper' function for the Send-MailMessage function with a hard-coded SmtpServer argument (that's all it does). Here's the header declaring the sets:
function SendMessage {
[CmdletBinding(DefaultParameterSetName='Path')]
Param(
[Parameter(Mandatory,
ParameterSetName='Path',
HelpMessage='the path and filename to the message you would like to send')]
[String]$Path,
[Parameter(Mandatory,
HelpMessage="A string containing the message which you want to send",
ParameterSetName='Msg')]
[String]$Msg,
[Parameter(Mandatory,
HelpMessage='Your Admin Office 365 credentials',
ParameterSetName='Path')]
[Parameter(Mandatory,
ParameterSetName='Msg')]
[Alias('Credentials')]
[System.Management.Automation.PSCredential]$Cred,
[Parameter(Mandatory,
HelpMessage='The address to which you want to send the message',
ParameterSetName='Path')]
[Parameter(Mandatory,
ParameterSetName='Msg')]
[String[]]$To,
[Parameter(Mandatory,
HelpMessage='The email from which you want to send the message',
ParameterSetName='Path')]
[Parameter(Mandatory,
ParameterSetName='Msg')]
[String]$From,
[Parameter(Mandatory,
HelpMessage='The subject of the email to send out',
ParameterSetName='Path')]
[Parameter(Mandatory,
ParameterSetName='Msg')]
[ValidateNotNullOrEmpty()]
[String]$Subject,
[Parameter(ParameterSetName='Path',
HelpMessage='[Optional] If you want it cc''d to anyone')]
[Parameter(ParameterSetName='Msg')]
[String[]]$CC
)
# do function stuff
}
The function has two ParameterSets: Path and Msg.
When I run it as a .ps1 script, I can see both sets:
PS U:\> test.ps1
PS U:\> gcm SendMessage -Syntax
SendMessage -Path <string> -Cred <pscredential> -To <string[]> -From <string> -Subject <string> [-CC <string[]>] [<CommonParameters>]
SendMessage -Msg <string> -Cred <pscredential> -To <string[]> -From <string> -Subject <string> [-CC <string[]>] [<CommonParameters>]
But when I import it as a module, I can only see the Path set:
PS U:\> Import-Module -Name test.psm1
PS U:\> gcm SendMessage -Syntax
SendMessage [-Cred] <pscredential> [-Path] <string> [-To] <string> [-From] <string> [-Subject] <string> [[-CC] <string[]>] [<CommonParameters>]
PS U:\>
I would like to be able to import both sets when I import the module.
Is there anything obvious which I am missing?
Thank you.
EDIT: I am using PS version 5.1, and have been testing using the ISE.
Testing scripts and function with the ISE makes it so complex. variables and functions in ISE is always available in global scope. Test this in PowerShell console.
Open a new PowerShell console, and dot source test.ps1
. c:\test.ps1
Get-Command SendMessage -Syntax
Open another PowerShell console and run Import-Module
Import-Module c:\Test.ps.1 -Verbose
Get-Command SendMessage -syntax

Powershell parameter sets and optional parameters

I'm writing a function for which two parameters should be exclusive and optional.
Here are valid inputs:
new-event -Title sometitle -Text sometext -TimestampHappened 1234567 -SomeOtherOptionalParam somestring
new-event -Title sometitle -Text sometext -DateHappened (get-date) -SomeOtherOptionalParam somestring
new-event -Title sometitle -Text sometext -SomeOtherOptionalParam somestring
new-event -Title sometitle -Text sometext
Here is an invalid input:
new-event -Title sometitle -Text sometext -DateHappened (get-date) -TimestampHappened 1234567 -SomeOtherOptionalParam somestring
Here is my code so far:
[CmdletBinding()]
# Most parameters belong to Default, New-Event:ByDate and New-Event:ByTimestamp parameter sets
param (
[Parameter(
Position=0,
Mandatory=$True,
ParameterSetName="Default"
)]
[Parameter(
Position=0,
Mandatory=$True,
ParameterSetName="New-Event:ByDate"
)]
[Parameter(
Position=0,
Mandatory=$True,
ParameterSetName="New-Event:ByTimestamp"
)]
[ValidateNotNullOrEmpty()]
[String]$Title,
[Parameter(
Position=1,
Mandatory=$True,
ParameterSetName="Default"
)]
[Parameter(
Position=1,
Mandatory=$True,
ParameterSetName="New-Event:ByDate"
)]
[Parameter(
Position=1,
Mandatory=$True,
ParameterSetName="New-Event:ByTimestamp"
)]
[ValidateNotNullOrEmpty()]
[String]$Text,
[Parameter(
Position=2,
Mandatory=$False,
ParameterSetName="New-Event:ByDate"
)]
[ValidateNotNullOrEmpty()]
[datetime]$DateHappened,
[Parameter(
Position=2,
Mandatory=$False,
ParameterSetName="New-Event:ByTimestamp"
)]
[ValidateNotNullOrEmpty()]
[Double]$TimestampHappened,
[Parameter(
Position=3,
Mandatory=$False,
ParameterSetName="Default"
)]
[Parameter(
Position=3,
Mandatory=$False,
ParameterSetName="New-Event:ByDate"
)]
[Parameter(
Position=3,
Mandatory=$False,
ParameterSetName="New-Event:ByTimestamp"
)]
[String]$SomeOtherParam,
...
Here is what I get when I call Get-Help:
PS> get-help New-Event
NAME
New-Event
SYNOPSIS
Post an event to the stream.
SYNTAX
New-Event [-Title] <String> [-Text] <String> [[-TimestampHappened] <Double>] [[-Priority] <String>] [[-Hostname] <String>] [[-Tags] <String[]>] [[-AlertType] <String>] [<CommonParameters>]
New-Event [-Title] <String> [-Text] <String> [[-DateHappened] <DateTime>] [[-Priority] <String>] [[-Hostname] <String>] [[-Tags] <String[]>] [[-AlertType] <String>] <String>] [<CommonParameters>]
New-Event [-Title] <String> [-Text] <String> [[-Priority] <String>] [[-Hostname] <String>] [[-Tags] <String[]>] [[-AlertType] <String>] [<CommonParameters>]
However here is the error I get when I try to call the function with only the two mandatory parameters:
New-Event -Title test -Text text
New-Event : Parameter set cannot be resolved using the specified named parameters.
At line:1 char:1
+ New-Event -Title test -Text text
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [New-Event], ParameterBindingException
+ FullyQualifiedErrorId : AmbiguousParameterSet,New-Event
I'm missing something here, but I can't figure out what...
How can I get two parameters that are mutually exclusive and optional?
This makes perfect sense. You have 3 parameter sets, and the 2 mandatory parameters are included on every set. How could PowerShell determine which set you meant to use?
Luckily the [CmdletBinding()] attribute can take a parameter that helps with this exact case: DefaultParameterSetName. Setting this allows PowerShell to use this set in the case of (certain) ambiguities. Use it like so:
[CmdletBinding(DefaultParameterSetName='Default')]
Note that in this case, you named it default; it could have been named anything.

PowerShell Invoke-Command Argumentlist

I'm struggling to pass my arguments to my custom made function.
This works fine:
Delete-OldFiles -Target $Target -OlderThanDays $OlderThanDays -LogName Auto_Clean.log
These don't work at all:
# (using -ScriptBlock ${Function:Delete-OldFiles} for all calls)
Invoke-Command -ArgumentList ( ".$Target", ".$OlderThanDays", ".$LogName Auto_Clean.log")
Invoke-Command -ArgumentList #("$Target", "$OlderThanDays", "Auto_Clean.log")
Invoke-Command -ArgumentList #({-OlderThanDays "10", -Target "E:\Share\Dir1", -LogName "Auto_Clean.log"})
Invoke-Command -ArgumentList (,#('$Target','$OlderThanDays','Auto_Clean.log'))
Can you help me on how to pass the following parameters correctly:
Param(
[Parameter(Mandatory=$False,Position=1)]
[String]$Server,
[Parameter(Mandatory=$True,Position=2)]
[ValidateScript({Test-Path $_})]
[String]$Target,
[Parameter(Mandatory=$True,Position=3)]
[Int]$OlderThanDays,
[Parameter(Mandatory=$True,Position=4)]
[String]$LogName,
[switch]$CleanFolders
)
When you use Invoke-Command -ArgumentList arguments are passed to script block as positional parameters. Looking at you param() block: it was not designed with using positional parameters in mind... Having optional parameters first, and mandatory after means you always need to provide optional parameter, or use named parameters for all mandatory ones.
In other words: having optional parameter in the first position will stop you from running this code in this fashion with Invoke-Command.
To test your code locally you should try it this way:
Delete-OldFiles $Target $OlderThanDays "Auto_Clean.log"
I would expect it to prompt you for LogName parameter.

Powershell #{} Pipeline Error

I have two custom powershell functions meant to be used like:
Get-Users | Select LoginName | Get-UserJobStatus -StartDate 2013-02-19 -EndDate 2013-02-21
When this is ran, I receive an error from the web service:
#{LoginName=UserName} not found
Which makes sense, because the username that's passed to the web service function should be just UserName and not #{LoginName=username}. How do I format that correctly?
function Global:Get-BackupJobStatus
{
[cmdletBinding()]
Param(
[Parameter(
Mandatory=$True,
HelpMessage='Specific account name.',
ParameterSetName='LoginName',
ValueFromPipeline=$True
)]
[System.String[]]$LoginName = $null,
[Parameter(
Mandatory=$false,
HelpMessage='Start date for report.'
)]
[System.DateTime]$startDate,
[Parameter(
Mandatory=$false,
HelpMessage='Ending date for report.'
)]
[System.DateTime]$EndDate
)
Begin {
$tempXml = New-Object System.Xml.XmlDocument
Write-Output $StartDate.toString("yyyy-MM-dd")
}
Process{
$tempXml.Load("https://webservicecall/info.do?LoginName="+$LoginName+"&Date="+$startDate.toString("yyyy-MM-dd"))
}
End {
Write-Output $tempXml.Users.User
}
}
The problem is that your function is expecting a string-object as the loginname input and you are sending a pscustomobject(created by select) with a property called loginname. To fix this, use select-object's -ExpandProperty parameter to send the value of loginnameinstead of objects with that property. Like this:
Get-Users | Select -ExpandProperty LoginName |
Get-UserJobStatus -StartDate 2013-02-19 -EndDate 2013-02-21
Alternately, you could add ValueFromPipelineByPropertyName=$true to allow you to bind the LoginName property of the object in the pipeline to the $LoginName parameter
If you're going to use "ValuefromPipeline", then it needs to be just the value:
Get-Users | Select -ExpandProperty LoginName | Get-UserJobStatus -StartDate 2013-02-19 -EndDate 2013-02-21