Closed SteveL-MSFT closed 8 years ago
It looks like the link above should instead be the following: https://github.com/PowerShell/PowerShell-Language-RFC/blob/master/1-Draft/RFC0001-Mutual-Exclusion-Parameters-and-Properties.md
The original link is missing the 1-Draft folder between master and the file.
Fixed, thanks! That link was before we thought it was a good idea to have the folders.
Is it feasible to define both methods and allow the cmdlet author to choose whichever style they prefer? If not then I'd personally pursue the [MutuallyExclusive()] attribute since it's more concrete as to what is being excluded against another.
My preference is the MutuallyExclusive syntax. When you have a lot of properties it's easier to see them all grouped together then have the ExclusionSet spread over all properties.
I do wonder however how hard it is to set these attributes for a large number of parameters. But that's an issue that we already have today and that's not easily solved with syntax (I think). Having run time validation to make sure that you don't exclude a property from all combinations would be nice.
I feel like both the existing ParameterSet syntax and the MutuallyExclusive syntax hide the intent. Since we're specifying "use cases", could we list the use cases with the distinguishing parameters?
For instance: [UseCase('Pipeline Input',DistinguishingParameters='InputObject')] [UseCase('Standard Path',DistinguishingParameters='Path')] [UseCase('Literal Path',DistinguisingParameters='LiteralPath')]
This is more readable in my mind, and you don't have to do a logic puzzle to figure out what's going on.
Often times when I use ParameterSets, the internals of the cmdlet are so different that it would be more convenient if PowerShell could use multimethods on cmdlets instead rather than having me clutter the internals of the begin, process, and end blocks with if tests to do my own dispatching.
I'd rather have multiple dispatch on cmdlets rather than this feature which just slightly tweaks ParameterSets.
I'd like to clarify - This RFC is only for Powershell classes, right? Not for Advanced Functions or Cmdlets?
If so, I tend toward the proposed usage of [MutuallyExclusive()], although I'd like to see the attribute in the class definition rather than outside of it.
The "motivation" section mentions parameters for advanced functions as well.
While I could see the [MutuallyExclusive()] attribute making sense in a class, when writing an advanced function, this would separate part of the definition of each parameter from the parameter itself. This doesn't make sense from an organizational standpoint, and it also makes the code more difficult to read.
Here's an example of an advanced function, with the MutuallyExclusive attribute added before the start of the function. When going over parameter definitions, it's tough to see which parameters are mutually exclusive, since that attribute is placed in a completely different place than the parameter attributes.
# Prop2 and Prop3 are mutually exclusive
[MutuallyExclusive("Prop2", "Prop3")]
function Invoke-Stuff {
[CmdletBinding(DefaultParameterSetName = 'ParameterSet1',
SupportsShouldProcess = $true)]
param(
[Parameter(ParameterSetName = 'ParameterSet1',
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true)]
[Alias('p1')]
[String] $Prop1,
[Parameter(ParameterSetName = 'ParameterSet1',
Mandatory = $true,
Position = 0)]
[Alias('p2')]
[ValidateScript({Test-Path $_})]
[string] $Prop2,
[Parameter(Mandatory = $false)]
[Alias('p3')]
[ValidateNotNullOrEmpty()]
[string] $Prop3,
[Parameter(ParameterSetName = 'ParameterSet2')]
[Alias('p4')]
[string] $Prop4
)
# ...
}
I like the ExclusionSet suggestion much better, since this becomes a parameter attribute. This can be logically placed with all the other attributes of each parameter.
Alternate suggestion, use ParameterSet attribute that defaults like adding a [Parameter(ParameterSetName='Name')] attribute, also include the ability to write a negative parameter set, e.g. [ParameterSet(Exclude='Name')] would mean all parameter sets except Name, or alternatively accept multiple parameter set names [ParameterSet('Set1','Set2')] for explicit set definitions.
class Fu
{
[ParameterSet('ByName')]
[string] $Name
[ParameterSet('ById')]
[string] $Id
[Ensure] $Ensure = ([Ensure]::Present)
[string] $Description
# Don't allow DisplayName when Name is used
[ParameterSet(Exclude='ByName')]
[string] $DisplayName
}
function Set-TargetResource
{
[CmdletBinding()]
param
(
[Parameter(Mandatory)]
[ParameterSet('ByName')]
[string] $Name,
[Parameter(Mandatory)]
[ParameterSet('ById')]
[string] $Id,
[Ensure] $Ensure = ([Ensure]::Present),
[string] $Description,
[ParameterSet(Exlcude='ByName')]
[string] $DisplayName,
)
}
@mattmcnabb intent is that this would be for both functions and classes. I realize now that the sample only shows for classes, but the attributes would be the same for functions
@SteveL-MSFT Quite all right, I see now that the header title mentions functions as well. Thanks!
I strongly support @adweigert suggestion. This seems far better, clearer, and more in the spirit of attributes than that put forward in the RFC. Frankly both alternatives in the RFC just seem to complicate things more than necessary.
I especially like @adweigert suggestion that we could include multiple parameter sets in the same attribute. Not being able to do so introduces a need for duplicating the rest of the parameter attributes.
@adweigert Good suggestion, however, it seems that the problem you are trying to solve is not exactly the same as the intent of this RFC. We'll update the RFC to clarify the intent as well as adding some real examples based on invoke-command and/or where-object. Hopefully we'll get this out before the holidays.
If the feature is geared directly towards DSC resource designers in v5, then I agree what I proposed would not be needed as the ParameterSet feature is already part of function/cmdlet design with [Parameter(ParameterSetName=
If this is specifically for v5 DSC class resource designers, then simply using something along the lines of [DscPropertySet([[Name=]
Things could be tricky around the resource key properties and different parameter sets, maybe the handling of that could be explained more.
Example:
[DscResource()]
class MyResource
{
[DscProperty(Key,ParameterSetName='ByName')]
[string] $Name
[DscProperty(Key,ParameterSetName='ById')]
[Guid] $Id
[bool] $Enabled
# Required for ById property set
[DscProperty(Mandatory, ParameterSetName='ById')]
[string] $DisplayName
/* implementation omitted */
}
I meant to comment on this a long time ago. Then there were holidays. And busy work after the holidays. Nothing quite like cutting it down to the wire...
I love that you're using an RFC process for language enhancements. I think that is very, very important at this stage of PowerShell's development. Thank you for opening these ideas up for discussion.
For this particular issue, I don't think that the proposed design is thought out well enough. It seems very narrow in vision and isn't really looking at the big picture.
I think it makes sense to analyze this from the perspective of cmdlets and advanced functions first, since they have been around a lot longer than classes and there is much more data to sample. For these types of commands, there are several challenges that come to mind when I think about mutual exclusivity among parameters in PowerShell:
Get-Command Invoke-Command -Syntax
, as well as the results of [System.Management.Automation.ProxyCommand]::GetParamBlock([System.Management.Automation.CommandMetadata]::new((Get-Command Invoke-Command)))
in PowerShell 5, and you'll get a feel for why simply saying "ParameterSets" is not a solution to the mutually exclusive problem. The former clearly illustrates how overwhelming many parameter sets can be for the end user, and the latter clearly illustrates how challenging it can be to create commands with many parameter sets for the author. With this specific example, there are a number of design bugs in the Invoke-Command command that are very likely the result of the author not being able to identify them among the 15 parameter sets that are included in the command definition. Yet while many parameter sets complicate a command like this, providing mutual exclusivity support to allow authors to reduce the number of parameter sets is very likely not enough to isolate the parameters that matter so that users can clearly identify what is needed when using a command to get something done.[<CommonParameters>]
, which represents the built-in, common parameters that apply to every command, including -ErrorAction, -PipelineVariable, and others. Imagine what the syntax would look like if every parameter set included these parameters in the syntax -- the result would be overwhelming. Having the ability to displace parameters that apply in multiple locations in the syntax is a helpful technique -- more on that later.Whether or not you agree that these are challenges and whether or not you think they are in scope for this RFC, I would really like to see the RFC expanded upon to include impacts to syntax output in Get-Help/Get-Command -Syntax output, as well as in command authoring of a complex command like Invoke-Command. We should have a clear picture how both authors and end users will be helped by whatever solution is provided.
With these challenges in mind, since the problem seems to be larger than simply mutual exclusivity among parameters, I'd like to propose an alternate solution (draft, does not cover all use cases yet, but I need to share it before the RFC deadline closes):
Since there are multiple relationships between parameters/properties that are worth considering, I don't think either the MutuallyExclusive attribute or the ExclusionSet attribute set us up for solving many of the issues identified here. I would prefer to see an alternative ValidateParameter attribute as well as a ParameterGroupName property for the current Parameter attribute. Here's an example showing both of these attributes in an Invoke-Command function that tries to provide the same functionality that exists in the current Invoke-Command cmdlet, but with less complexity in the definition (and fixing some of the issues in the current definition as well):
function Invoke-Command {
[CmdletBinding(DefaultParameterSetName='ScriptBlock')]
[OutputType([System.Object])]
param(
[Parameter(ParameterSetName='ScriptBlock', Mandatory=$true, Position=0)]
[ValidateNotNull()]
[Alias('Command')]
[scriptblock]
$ScriptBlock,
[Parameter(ParameterSetName='FilePath', Mandatory=$true, Position=0)]
[ValidateNotNull()]
[Alias('PSPath')]
[string]
$FilePath,
[Alias('Args')]
[System.Object[]]
$ArgumentList,
[Parameter(ValueFromPipeline=$true)]
[psobject]
$InputObject,
[ValidateSet('LocalComputer','PsSession','RemoteComputer','Uri','VmById','VmByName','ContainerById','ContainerByName')]
[string]
$TargetType = 'LocalComputer',
[ValidateParameter('TargetType', Eq='LocalComputer')]
[switch]
$NoNewScope,
[Parameter(Mandatory=$true, Position=1)]
[ValidateParameter('TargetType', Eq='PsSession')]
[ValidateNotNullOrEmpty()]
[System.Management.Automation.Runspaces.PSSession[]]
$Session,
[Parameter(Mandatory=$true, Position=1)]
[ValidateParameter('TargetType', Eq='RemoteComputer')]
[ValidateNotNullOrEmpty()]
[Alias('Cn')]
[string[]]
$ComputerName,
[ValidateParameter('TargetType', Eq='RemoteComputer')]
[ValidateRange(1, 65535)]
[int]
$Port,
[ValidateParameter('TargetType', Eq='RemoteComputer')]
[switch]
$UseSsl,
[Parameter(Mandatory=$true, Position=1)]
[ValidateParameter('TargetType', Eq='Uri')]
[ValidateNotNullOrEmpty()]
[Alias('URI','CU')]
[uri[]]
$ConnectionUri,
[ValidateParameter('TargetType', Eq='Uri')]
[switch]
$AllowRedirection,
[Parameter(ValueFromPipelineByPropertyName=$true)]
[ValidateParameter('TargetType', In='RemoteComputer','Uri')]
[ValidateParameter('TargetType', In='VmById','VmByName', Mandatory=$true)]
[pscredential]
[System.Management.Automation.CredentialAttribute()]
$Credential,
[ValidateParameter('TargetType', In='RemoteComputer','Uri')]
[System.Management.Automation.Runspaces.AuthenticationMechanism]
$Authentication,
[ValidateParameter('TargetType', In='RemoteComputer','Uri')]
[System.Management.Automation.Remoting.PSSessionOption]
$SessionOption,
[ValidateParameter('TargetType', In='RemoteComputer','Uri')]
[switch]
$EnableNetworkAccess,
[Parameter(ValueFromPipelineByPropertyName=$true)]
[ValidateParameter('TargetType', In='RemoteComputer','Uri')]
[ValidateNotNullOrEmpty()]
[string]
$ConfigurationName,
[Parameter(ValueFromPipelineByPropertyName=$true)]
[ValidateParameter('TargetType', Eq='RemoteComputer')]
[ValidateNotNullOrEmpty()]
[string]
$ApplicationName,
[ValidateParameter('TargetType', In='ComputerName','Uri')]
[ValidateNotNullOrEmpty()]
[string]
$CertificateThumbprint,
[ValidateParameter('TargetType', In='ComputerName','Uri')]
[Alias('Disconnected')]
[switch]
$InDisconnectedSession,
[ValidateParameter('TargetType', In='ComputerName','Uri')]
[ValidateParameter('InDisconnectedSession', IsPresent=$true)]
[ValidateNotNullOrEmpty()]
[string[]]
$SessionName,
[Parameter(Mandatory=$true, Position=1, ValueFromPipelineByPropertyName=$true)]
[ValidateParameter('TargetType', Eq='VmById')]
[ValidateNotNullOrEmpty()]
[Alias('VMGuid')]
[guid[]]
$VmId,
[Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[ValidateParameter('TargetType', Eq='VmByName')]
[ValidateNotNullOrEmpty()]
[string[]]
$VmName,
[Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[ValidateParameter('TargetType', Eq='ContainerById')]
[ValidateNotNullOrEmpty()]
[string[]]
$ContainerId,
[Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[ValidateParameter('TargetType', Eq='ContainerByName')]
[ValidateNotNullOrEmpty()]
[string[]]
${ContainerName},
[ValidateParameter('TargetType', In='ContainerById','ContainerByName')]
[switch]
${RunAsAdministrator},
[Parameter(ParameterGroupName='RemoteCommandOptionParameters')]
[ValidateParameter('TargetType', Ne='LocalComputer')]
[int]
$ThrottleLimit,
[Parameter(ParameterGroupName='RemoteCommandOptionParameters')]
[ValidateParameter('TargetType', Ne='LocalComputer')]
[switch]
$AsJob,
[Parameter(ParameterGroupName='RemoteCommandOptionParameters')]
[ValidateParameter('TargetType', Ne='LocalComputer')]
[Alias('HCN')]
[switch]
$HideComputerName,
[Parameter(ParameterGroupName='RemoteCommandOptionParameters')]
[ValidateParameter('TargetType', Ne='LocalComputer')]
[string]
$JobName
)
# Implementation goes here
}
Similarly, here is an alternate representation of the command syntax that could show up in Get-Help or Get-Command -Syntax output for that same command:
SYNTAX
Invoke-Command [-ScriptBlock] <ScriptBlock> [-ArgumentList <Object[]>] [-InputObject <PSObject>] [-TargetType
<String>] [<TargetTypeDependentParameters>] [<CommonParameters>]
Invoke-Command [-FilePath] <String> [-ArgumentList <Object[]>] [-InputObject <PSObject>] [-TargetType <String>]
[<TargetTypeDependentParameters>] [<CommonParameters>]
[<TargetTypeDependentParameters>]
LocalComputer : [-NoNewScope]
PsSession : [-Session] <PSSession[]> [<RemoteCommandOptionParameters>]
RemoteComputer : [-ComputerName] <String[]> [-Port <Int32>] [-UseSsl] [-Credential <PSCredential>]
[-Authentication <AuthenticationMechanism>] [-SessionOption <PSSessionOption>]
[-EnableNetworkAccess] [-ConfigurationName <String>] [-ApplicationName <String>]
[-CertificateThumbprint <String>] [-InDisconnectedSession] [-SessionName <String>]
[<RemoteCommandOptionParameters>]
Uri : [-ConnectionUri] <Uri[]> [-AllowRedirection] [-Credential <PSCredential>] [-Authentication
<AuthenticationMechanism>] [-SessionOption <PSSessionOption>] [-EnableNetworkAccess]
[-ConfigurationName <String>] [-CertificateThumbprint <String>] [-InDisconnectedSession]
[-SessionName <String>] [<RemoteCommandOptionParameters>]
VmById : [-VmId] <Guid[]> [-Credential] <PSCredential> [<RemoteCommandOptionParameters>]
VmByName : -VmName <String[]> [-Credential] <PSCredential> [<RemoteCommandOptionParameters>]
ContainerById : -ContainerId <String[]> [-RunAsAdministrator] [<RemoteCommandOptionParameters>]
ContainerByName : -ContainerName <String[]> [-RunAsAdministrator] [<RemoteCommandOptionParameters>]
[<RemoteCommandOptionParameters>]
[-ThrottleLimit <Int32>] [-AsJob] [-JobName <String>] [-HideComputerName]
Compare these to the results of the two PowerShell commands mentioned much earlier showing the poor authoring experience and help experience that are provided today for this particular command.
Pros:
Cons:
Regardless of which way this proposal goes moving forward, I would like to see practical examples for existing, published commands that could take advantage of the additional parameter validation that is being added, both from the author's perspective and from the end users perspective, so that we're making authoring easier, making documentation smarter, and making command use easier. For what it's worth, I attempted to design this for help documentation first, and then once I found a syntax that was easier to consume and that fit my mental model better when using complex commands, then I tried to figure out what metadata I could add to parameters that would allow PowerShell to give me that documentation. Looking at this from a class-perspective first doesn't take documentation into account, and downplays some of the bigger issues that come from multiple parameter sets for authors/end users that are worth solving.
That's enough from me for now. I wrote this about 6 times before finally putting thoughts together here to share. Hopefully what I'm trying to illustrate here actually makes a little sense to you.
I just remembered this morning one other area where the user is heavily impacted by the direction this goes: user experiences wrapped around specific PowerShell commands. It's not hard to imagine that such experiences could be built to wrap around PowerShell classes as well. For an example, consider this:
Show-Command -Name Invoke-Command
The UX with that dialog is not abyssmal, but it's much more challenging than it needs to be. If you were to build a UX on top of commands that had additional metadata such as the proposed ValidateParameterAttribute and ParameterGroupName property of the Parameter attribute, you could build something much more intuitive that breaks a command down into the way people think.
Other places where the UX built from command (or class) metadata matters is in Azure Automation and SMA. In these environments, runbook activities auto-generated from metadata surrounding PowerShell entities like commands (and maybe classes at some point) can be better tailored according to how the author of the command defines that command. You can imagine a toolset where while building a command, you see the syntax output for that command displayed inside another pane in the tool where you are creating the command, as well as the Show-Command UI output auto-updating according to the command definition.
How these experiences will be impacted (hopefully improved) by changes to parameter/property metadata should be considered by enhancements to metadata like this RFC is proposing.
Thanks for all the feedback. At this time, I'm going to withdraw this RFC. The feedback makes it clear that the current proposal doesn't completely solve the problem without introducing new problems in readability. Any RFC for this issue should also (as @KirkMunro pointed out) include addressing making help more readable as generating many parameter sets makes it much less readable for end users. The original spark that started this RFC was for DSC classes, but certainly any solution needs to work for general PowerShell classes as well as advanced functions. If anyone wants to continue to pursue this, please submit a new RFC.
Use this to track comments for https://github.com/PowerShell/PowerShell-Language-RFC/blob/master/1-Draft/RFC0001-Mutual-Exclusion-Parameters-and-Properties.md