lit
lit

Reputation: 16226

How to support multiple, mutually exclusive parameters?

I must write a script to get instances of Thing. Each Thing contains an event timestamp. I need to allow the user to specify a timestamp range.

How can I use a ParameterSet to disable the use of both $Since and $StartTimestamp AND disable the use of both $Until and $EndTimestamp?

Posts about using multiple ParameterSets appear to grow exponentially with the number of parameters. Is this really the way?

There are posts about using a DynamicParam. I do not yet see where a DynamicParam would be appropriate for this. Is it?

[CmdletBinding()]
param (
    [Parameter(Mandatory=$true)]
    [string] $ThingName

    ,[ValidateSet('Today', 'Yesterday', 'LastWeek')]
    [string] $Since

    ,[datetime] $StartTimestamp

    ,[ValidateSet('Today', 'Now', 'Yesterday', 'LastWeek')]
    [string] $Until

    ,[datetime] $EndTimestamp
)

Upvotes: 3

Views: 242

Answers (1)

mklement0
mklement0

Reputation: 437062

Mutually exclusive parameters are indeed hard to implement using parameter sets, as of this writing (PowerShell v7.3.4):

  • GitHub issue #5175 is a long-standing feature request to make defining mutually exclusive parameters easier, but it has
  • GitHub issue #12818 is a more recent and more comprehensive feature covering other aspects of parameter sets as well.

A solution with the current features is possible, but cumbersome:

Note: I'm assuming that you want to allow only the following combinations (this complements the description of what you want to prevent in your question), and this positive formulation can expressed be via parameter sets:

  • -ThingName only, or combined with any of the following:
  • -Since only, -Until only
  • -StartTimestamp only, -EndTimeStamp only
  • -Since combined with -EndTimeStamp
  • -Until combined with -StartTimeStamp
[CmdletBinding(DefaultParameterSetName='ThingAlone')]
param (
    [Parameter(Mandatory, Position=0)]
    [string] $ThingName
    ,    
    [Parameter(Mandatory, ParameterSetName='SinceAlone')]
    [Parameter(Mandatory, ParameterSetName='StartSinceEndUntil')]
    [Parameter(Mandatory, ParameterSetName='StartSinceEndTimestamp')]
    [ValidateSet('Today', 'Yesterday', 'LastWeek')]
    [string] $Since
    ,
    [Parameter(Mandatory, ParameterSetName='StartTimestampAlone')]
    [Parameter(Mandatory, ParameterSetName='StartTimestampEndUntil')]
    [Parameter(Mandatory, ParameterSetName='StartTimestampEndTimestamp')]
    [datetime] $StartTimestamp
    ,
    [Parameter(Mandatory, ParameterSetName='UntilAlone')]
    [Parameter(Mandatory, ParameterSetName='StartSinceEndUntil')]
    [Parameter(Mandatory, ParameterSetName='StartTimestampEndUntil')]
    [ValidateSet('Today', 'Now', 'Yesterday', 'LastWeek')]
    [string] $Until
    ,
    [Parameter(Mandatory, ParameterSetName='EndTimeStampAlone')]
    [Parameter(Mandatory, ParameterSetName='StartSinceEndTimestamp')]
    [Parameter(Mandatory, ParameterSetName='StartTimestampEndTimestamp')]
    [datetime] $EndTimestamp
)

$PSCmdlet.ParameterSetName

Resulting syntax diagram (invoke the script with -?):

YourScript.ps1 [-ThingName] <string> [<CommonParameters>]
YourScript.ps1 [-ThingName] <string> -Since <string> -EndTimestamp <datetime> [<CommonParameters>]
YourScript.ps1 [-ThingName] <string> -Since <string> -Until <string> [<CommonParameters>]
YourScript.ps1 [-ThingName] <string> -Since <string> [<CommonParameters>]
YourScript.ps1 [-ThingName] <string> -StartTimestamp <datetime> -EndTimestamp <datetime> [<CommonParameters>]
YourScript.ps1 [-ThingName] <string> -StartTimestamp <datetime> -Until <string> [<CommonParameters>]
YourScript.ps1 [-ThingName] <string> -StartTimestamp <datetime> [<CommonParameters>]
YourScript.ps1 [-ThingName] <string> -Until <string> [<CommonParameters>]
YourScript.ps1 [-ThingName] <string> -EndTimestamp <datetime> [<CommonParameters>]

Taking a step back:

  • As zett42 points out, you can bypass the need for mutual exclusion if you provide only a single, polymorphous parameter for the start and end timestamp, respectively.

  • To that end, declare these parameters as [object] (so they can accept a value of any type), and:

    • use a [ValidateScript()] attribute to ensure that a value passed by the user can either be parsed as a [datetime] instance or is one of the predefined symbolic names, such as Today.

    • In order to also support tab-completion, use an [ArgumentCompleter()] attribute that completes the symbolic names.

    • Note: The arrays of symbolic names are duplicated in the two attributes below:

      • In a stand-alone script that cannot be avoided, but in a function (e.g. as part of a module) you could define the arrays only once.
[CmdletBinding()]
param (
    [Parameter(Mandatory, Position=0)]
    [string] $ThingName
    ,
    [ArgumentCompleter({
      param($cmd, $param, $wordToComplete)
      'Today', 'Yesterday', 'LastWeek' -like "$wordToComplete*"
    })]
    [ValidateScript({
      if ($_ -notin 'Today', 'Yesterday', 'LastWeek' -and $null -eq ($_ -as [datetime])) {
        throw "Invalid -Since argument."
      }
      $true
    })]
    [object] $Since
    ,
    [ArgumentCompleter({
      param($cmd, $param, $wordToComplete)
      'Today', 'Now', 'Yesterday', 'LastWeek' -like "$wordToComplete*"
    })]
    [ValidateScript({
      if ($_ -notin 'Today', 'Now', 'Yesterday', 'LastWeek' -and $null -eq ($_ -as [datetime])) {
        throw "Invalid -Until argument."
      }
      $true
    })]
    [object] $Until
)

# Translate the -Since and -Until arguments into [datetime] instances.
$i = 0
$sinceTimestamp, $untilTimestamp = 
    $Since, $Until | ForEach-Object {
      switch ($_) {
        $null { if ($i -eq 0) { Get-Date -Date 0 } else { Get-Date }; break }
        Now { Get-Date; break }
        Today { (Get-Date).Date; break }
        Yesterday  { (Get-Date).Date.AddDays(-1); break }
        LastWeek  { (Get-Date).Date.AddDays(-7); break }
        Default { $_ -as [datetime] }
      }
      ++$i
    }
if ($untilTimestamp -lt $sinceTimestamp) { Throw "The -Since argument must predate the -Until argument." }

# Diagnostic output.
[pscustomobject] @{
  Since = $sinceTimestamp
  Until = $untilTimestamp
}

Upvotes: 6

Related Questions