Skip to content

Make parameter sets of advanced functions/scripts more robust and more predictable #11237

@mklement0

Description

@mklement0

Important:

  • The proposal below is undoubtedly a change that cannot be made without breaking existing code.
    However, I don't think the existing problems can be fixed while maintaining backward compatibility.
    Therefore, use of an implementation of this proposal would have to be on an opt-in basis by future function / script authors, via an optional feature or, should there ever be one, a "vNext" PowerShell version that is allowed to break backward compatibility in the interest of fixing many longstanding problems.

  • This will probably have to become an RFC, if there's sufficient interest - this issue is meant to get the conversion started and gauge that interest.

Summary of the new feature/enhancement

There are several problems with the existing parameter-set handling:

  • In the absence of explicitly designating a default parameter set, PowerShell tries to infer one, but does so inconsistently (Advanced functions require specifying a default parameter set with two or more explicit parameter sets #11143) and sometimes incorrectly (Binding a parameter via the pipeline can break implicit parameter-set selection #11235).

  • $PSCmdlet.ParameterSetName, meant to reflect the parameter set in effect (selected by the combination of arguments and/or pipeline input), obscurely and confusingly reflects __AllParameterSets in argument-less invocations or in invocations that comprise parameters without explicit set association.

    • __AllParameterSets is also the internal name of the "meta" set automatically assigned to parameters without explicit set association, and it signals that a given parameter belongs to all parameter sets (if any exist beyond the implied unnamed one).
    • As such, the name __AllParameterSets should be considered an implementation detail, and making it do double duty as the name of the effective set is obscure and confusing.
  • Ill-defined parameter sets do not become apparent until runtime, as opposed to parse time, including misspelled attribute property names. Problems may not be discovered until later, when a specific combination of arguments and inputs is used on invocation (e.g., unintentionally allowing / disallowing argument-less invocation).

  • Designating an otherwise undefined default parameter set as the default - e.g., [CmdletBinding(DefaultParameterSetName='PossiblyAccidentalDefault')] is quietly accepted (and an invocation without arguments or only with parameters not explicitly associated with sets make it the effective on).

    • While this technique is actually currently required to implement a function that explicitly allows passing no arguments alongside parameters with explicit set associations (it is also the easiest way to allow invocations binding all-parameter-sets parameters only), it is non-obvious and brittle.
  • On a minor note, the Name suffix in the attribute property names seems unnecessarily verbose (ParameterSetName, DefaultParameterSetName).

Proposed technical implementation details (optional)

  • Enforce the following at parse time to signal that there's a fundamental problem with the function / script that needs resolving, as opposed to the current situational failures that depend on the specifics of a given invocation. This also has the advantage of being able to provide specific error messages with clear resolution directions.

    • If an explicit parameter set is associated with at least one parameter, enforce having to explicitly designate a default parameter set in [CmdletBinding()]. This avoids the obscurity of the current, situational inference of the default and instead signals explicit intent.

    • Do not permit designating an undefined parameter set as the default in the [CmdletBinding()] attribute - the name must refer to a set named in at least one [Parameter()] attribute.

    • If feasible, ensure up front that there are no unknown / mistyped property names in the [CmdletBinding()], [Parameter()], ... attributes.

  • Make '' (empty string) the (non)-name for the implied, unnamed parameter set to become effective in argument-less invocations and invocations with all-parameter-sets parameters only.

    • [CmdletBinding(DefaultParameterSetName='')] will signal the explicit intent to allow invocations without arguments altogether, as well as invocations comprising all-parameter-sets parameters only.
    • $PSCmdlet.DefaultParameterSetName will then reflect '' (the empty string) in such invocations.
  • Allow omitting the wordy Name suffix from the DefaultParameterSetName and ParameterSetName attribute properties.

Example

Consider a function Write-Message, which should wrap Write-Host as follows:

  • Print a default message, if no arguments are passed at all.
  • If only a -Message argument is passed, print that message as-is.
    If one of the mutually exclusive -AsError or -AsWarning switches is passed, print the (default or explicit) message in a switch-specific color.

Current declaration:

function Write-Message {
  # * Without artificially designating a default parameter set, 
  #   argument-less and -Message-only invocations don't work.
  # * If  'ArtificalName' happens to be a leftover / mistyped name, you may 
  #   accidentally be enabling undesired invocations.
  [CmdletBinding(DefaultParameterSetName = 'ArtificalName')]
  param(
    # Optional message that has a default.
    [string] $Message = 'Completed.',

    # Note: Whether you use `Mandatory` or not makes no difference here.
    [Parameter(ParameterSetName = 'err')] [switch] $AsError,
    [Parameter(ParameterSetName = 'warn')] [switch] $AsWarning
  )

  $writeHostArgs = @{ Object = $Message }

  if ($AsError) { $writeHostArgs.ForegroundColor = 'Red' }
  elseif ($AsWarning) { $writeHostArgs.ForegroundColor = 'Yellow' }

  Write-Host @writeHostArgs
}

Declaration with the proposal implemented:

function Write-Message {
  # * Designating '' as the default parameter set unambiguously signals the intent to 
  #   allow argument-less and all-parameter-sets-parameters-only invocations.
  # * Any other name would have to refer to a set explicitly mentioned in at least one
  #    [Parameter()] attribute.
  [CmdletBinding(DefaultParameterSet = '')]
  param(
    # Optional message that has a default.
    [string] $Message = 'Completed.',

    # Note: Whether you use `Mandatory` or not makes no difference here.
    [Parameter(ParameterSet = 'err')] [switch] $AsError,
    [Parameter(ParameterSet = 'warn')] [switch] $AsWarning
  )

  $writeHostArgs = @{ Object = $Message }

  if ($AsError) { $writeHostArgs.ForegroundColor = 'Red' }
  elseif ($AsWarning) { $writeHostArgs.ForegroundColor = 'Yellow' }

  Write-Host @writeHostArgs
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Issue-Enhancementthe issue is more of a feature request than a bugResolution-No ActivityIssue has had no activity for 6 months or more

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions