2

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.

  • There are four (4) time-specifying parameters
  • None are mandatory
  • $Since and $StartTimestamp are mutually exclusive
  • $Until and $EndTimestamp are mutually exclusive
  • The script will translate $Since and $Until strings into an appropriate [datetime]

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
)
mklement0
  • 382,024
  • 64
  • 607
  • 775
lit
  • 14,456
  • 10
  • 65
  • 119
  • 4
    Yes, parameter sets would grow exponentially. DynamicParam seems like overkill. I would question the use of two separate parameters for what is logically one parameter that allows for specifying a time in different formats. You could use a single parameter of type `object` that accepts both a `string` and a `datetime` and cast the type in the body of the function. Validation could be done using `ValidateScript`. – zett42 Jun 11 '23 at 13:43
  • @zett42 - I wanted to have the "word" parameters in order to provide tab-completion for the -Since and -Until parameters. Is it possible to do that if the parameter type is `object`? – lit Jun 11 '23 at 17:15
  • 1
    @lit, combine zett42's `ValidateScript` suggestion with an `ArgumentCompleter` attribute: See [this answer](https://stackoverflow.com/a/75204718/45375) for an example (it uses `[string]`, but `[object]` should work too). – mklement0 Jun 11 '23 at 17:20

1 Answers1

5

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
}
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • @mklement0 - Many thanks for your great answer (as usual). I have coded both of these up. ParameterSets work, but are, as you say, cumbersome. For the second solution, if -Since is not specified, $SinceTimestamp needs to be the beginning of time. `(Get-Date -Year 1 -Month 1 -Day 1)`? If -Until is not specified, it should be the current time. `(Get-Date)`. – lit Jun 12 '23 at 12:39
  • My pleasure, @lit, and thanks for the nice feedback. I've updated the answer to add the default values: `Get-Date 0` is the simplest way to get the earliest supported timestamp; it is equivalent to `[datetime] 0` and therefore `[datetime]::new(0)`, i.e. you're passing a _ticks_ value of `0`. – mklement0 Jun 12 '23 at 13:35
  • 1
    I am -not- asking that you revise the code. This has been most helpful. I may need to make a change. Using `-Since Today` and `-Until Today` probably means `(Get-Date).Date` (midnight) and `(Get-Date)` (now) respectively. Or, perhaps this should just be a documentation issue. `-Until Now` would be more explicit. – lit Jun 13 '23 at 19:17