Repository navigation
Description
Summary of the new feature/enhancement
To complement Feature Idea: Revisiting and extending the ParameterSet Experience #12818
I would like to raise two dissenting ideas:
1. Reverse the set/parameter containment
Personally, I think that the current parameter notation is inside out
Currently, parameters contain parameters sets (each Parameter holds multiple attributes with a ParametersSetName)
but it should be visa versa: a parameter set should contain parameters.
By turning this around, you might also support (recursive) parameter subsets.
With all respect, adding additional configuration items (like groups and ParameterMutexName) might help to reduce the redundancy but eventually adds up to the complicity when combining them with the existing configuration items.
I even believe that naming parameters sets is inessential if using subsets.
2. Use PowerShell to build parameter sets.
Developers creating cmdlets are already quiet known with PowerShell and need to learn a complete different parameter syntax to get them straight for their functions.
Apart from the fact that you might reduce the redundancy and complexity by using variables and subsets, there is also another advantage in moving to PowerShell scripting: I guess that changes to the current parameter notification (like e.g. groups) will require an update of the PowerShell engine therefor engineers who like to supply some backwards compatibility (with e.g. Windows PowerShell) for their cmdlets/functions might be reluctant to use any new parameter features provided through the current parameter notation. A PowerShell framework using the DynamicParam section, might only require installing a module to supply backwards compatibility in customer scripts.
Proposed technical implementation details
Let's assume a Draw-Shape cmdlet that requires the following parameter (set) constrains:
- There are 3 possible shapes:
- Circle (which requires one
Radiusparameter) - Rectangle (which requires a
Widthand aHeightparameter) - Triangle (which requires 3 -
Side1,Side2andSide3- parameters)
- Circle (which requires one
- Each shape requires either of the (mutually exclusive) parameters below:
TransparencyOpacity
- And a optional color (default black) by either using
- a RGB scheme
RedGreenBlue
- or a HSL scheme
HueSatLum
- a RGB scheme
Using the current parameter notation, this requires:
a lot of parameter definitions...
function Draw-Shape {
[CmdletBinding()] param(
[Parameter(Position = 0, ParameterSetName = 'CircleTransparencyRGB', Mandatory = $True)]
[Parameter(Position = 0, ParameterSetName = 'CircleTransparencyHSL', Mandatory = $True)]
[Parameter(Position = 0, ParameterSetName = 'CircleOpacityRGB', Mandatory = $True)]
[Parameter(Position = 0, ParameterSetName = 'CircleOpacityHSL', Mandatory = $True)]
[int]$Radius,
[Parameter(Position = 0, ParameterSetName = 'SquareTransparencyRGB', Mandatory = $True)]
[Parameter(Position = 0, ParameterSetName = 'SquareTransparencyHSL', Mandatory = $True)]
[Parameter(Position = 0, ParameterSetName = 'SquareOpacityRGB', Mandatory = $True)]
[Parameter(Position = 0, ParameterSetName = 'SquareOpacityHSL', Mandatory = $True)]
[int]$Width,
[Parameter(Position = 1, ParameterSetName = 'SquareTransparencyRGB', Mandatory = $True)]
[Parameter(Position = 1, ParameterSetName = 'SquareTransparencyHSL', Mandatory = $True)]
[Parameter(Position = 1, ParameterSetName = 'SquareOpacityRGB', Mandatory = $True)]
[Parameter(Position = 1, ParameterSetName = 'SquareOpacityHSL', Mandatory = $True)]
[int]$Height,
[Parameter(Position = 0, ParameterSetName = 'TriangleTransparencyRGB', Mandatory = $True)]
[Parameter(Position = 0, ParameterSetName = 'TriangleTransparencyHSL', Mandatory = $True)]
[Parameter(Position = 0, ParameterSetName = 'TriangleOpacityRGB', Mandatory = $True)]
[Parameter(Position = 0, ParameterSetName = 'TriangleOpacityHSL', Mandatory = $True)]
[int]$Side1,
[Parameter(Position = 1, ParameterSetName = 'TriangleTransparencyRGB', Mandatory = $True)]
[Parameter(Position = 1, ParameterSetName = 'TriangleTransparencyHSL', Mandatory = $True)]
[Parameter(Position = 1, ParameterSetName = 'TriangleOpacityRGB', Mandatory = $True)]
[Parameter(Position = 1, ParameterSetName = 'TriangleOpacityHSL', Mandatory = $True)]
[int]$Side2,
[Parameter(Position = 2, ParameterSetName = 'TriangleTransparencyRGB', Mandatory = $True)]
[Parameter(Position = 2, ParameterSetName = 'TriangleTransparencyHSL', Mandatory = $True)]
[Parameter(Position = 2, ParameterSetName = 'TriangleOpacityRGB', Mandatory = $True)]
[Parameter(Position = 2, ParameterSetName = 'TriangleOpacityHSL', Mandatory = $True)]
[int]$Side3,
[Parameter(ParameterSetName = 'CircleTransparencyRGB', Mandatory = $True)]
[Parameter(ParameterSetName = 'SquareTransparencyRGB', Mandatory = $True)]
[Parameter(ParameterSetName = 'TriangleTransparencyRGB', Mandatory = $True)]
[Parameter(ParameterSetName = 'CircleTransparencyHSL', Mandatory = $True)]
[Parameter(ParameterSetName = 'SquareTransparencyHSL', Mandatory = $True)]
[Parameter(ParameterSetName = 'TriangleTransparencyHSL', Mandatory = $True)]
[int]$Transparency,
[Parameter(ParameterSetName = 'CircleOpacityRGB', Mandatory = $True)]
[Parameter(ParameterSetName = 'SquareOpacityRGB', Mandatory = $True)]
[Parameter(ParameterSetName = 'TriangleOpacityRGB', Mandatory = $True)]
[Parameter(ParameterSetName = 'CircleOpacityHSL', Mandatory = $True)]
[Parameter(ParameterSetName = 'SquareOpacityHSL', Mandatory = $True)]
[Parameter(ParameterSetName = 'TriangleOpacityHSL', Mandatory = $True)]
[int]$Opacity,
[Parameter(ParameterSetName = 'CircleTransparencyRGB')]
[Parameter(ParameterSetName = 'SquareTransparencyRGB')]
[Parameter(ParameterSetName = 'TriangleTransparencyRGB')]
[Parameter(ParameterSetName = 'CircleOpacityRGB')]
[Parameter(ParameterSetName = 'SquareOpacityRGB')]
[Parameter(ParameterSetName = 'TriangleOpacityRGB')]
[int]$Red,
[Parameter(ParameterSetName = 'CircleTransparencyRGB')]
[Parameter(ParameterSetName = 'SquareTransparencyRGB')]
[Parameter(ParameterSetName = 'TriangleTransparencyRGB')]
[Parameter(ParameterSetName = 'CircleOpacityRGB')]
[Parameter(ParameterSetName = 'SquareOpacityRGB')]
[Parameter(ParameterSetName = 'TriangleOpacityRGB')]
[int]$Green,
[Parameter(ParameterSetName = 'CircleTransparencyRGB')]
[Parameter(ParameterSetName = 'SquareTransparencyRGB')]
[Parameter(ParameterSetName = 'TriangleTransparencyRGB')]
[Parameter(ParameterSetName = 'CircleOpacityRGB')]
[Parameter(ParameterSetName = 'SquareOpacityRGB')]
[Parameter(ParameterSetName = 'TriangleOpacityRGB')]
[int]$Blue,
[Parameter(ParameterSetName = 'CircleTransparencyHSL')]
[Parameter(ParameterSetName = 'SquareTransparencyHSL')]
[Parameter(ParameterSetName = 'TriangleTransparencyHSL')]
[Parameter(ParameterSetName = 'CircleOpacityHSL')]
[Parameter(ParameterSetName = 'SquareOpacityHSL')]
[Parameter(ParameterSetName = 'TriangleOpacityHSL')]
[int]$Hue,
[Parameter(ParameterSetName = 'CircleTransparencyHSL')]
[Parameter(ParameterSetName = 'SquareTransparencyHSL')]
[Parameter(ParameterSetName = 'TriangleTransparencyHSL')]
[Parameter(ParameterSetName = 'CircleOpacityHSL')]
[Parameter(ParameterSetName = 'SquareOpacityHSL')]
[Parameter(ParameterSetName = 'TriangleOpacityHSL')]
[int]$Sat,
[Parameter(ParameterSetName = 'CircleTransparencyHSL')]
[Parameter(ParameterSetName = 'SquareTransparencyHSL')]
[Parameter(ParameterSetName = 'TriangleTransparencyHSL')]
[Parameter(ParameterSetName = 'CircleOpacityHSL')]
[Parameter(ParameterSetName = 'SquareOpacityHSL')]
[Parameter(ParameterSetName = 'TriangleOpacityHSL')]
[int]$Lum
)
}Using this prototype ParameterFunctions module, it requires the following knowledge. Sorry, there is no readme yet, but you can use the -? option to get the syntax and the description below:
There are 2 functions included in the module to create parameters and parameter-sets:
New-Parameter(aliasNew-Param, orNPM)
This function has 5 main parameters:
[int]$Position[type]$Type[string]$Name[object]$Default[switch]$Mandatory
Besides has a few parameters like HelpMessage to set certain parameter attributes.
It has no ParameterSetName option!
New-ParameterSet(aliasNew-ParamSet, orNPS)
Which has two parameters:
[RuntimeDefinedParameterDictionary[]]$ParameterSets[String]$SetName(Optional)
There can only be one root (default) parameter set (or a single parameter which is quiet pointless).
Each set might contain multiple (optional or mandatory) parameters or multiple (recursive) parameter sets.
Syntax
The basic syntax notation:
[x]
Optional parameter. The command runs whether or not you enter optional parameters.
function FMO {
[CmdletBinding()] param()
DynamicParam {
New-Param 0 ([string]) x
}
}[x|y]
Select between optional parameters.
function FMO {
[CmdletBinding()] param()
DynamicParam {
New-ParamSet @(
New-Param 0 ([string]) x
New-Param 1 ([string]) y
)
}
}<x>
Required option. If you omit a required option, the command line program returns an error message.
function Test {
[CmdletBinding()] param()
DynamicParam {
New-Param 0 ([string]) x -Mandatory
}
}<x|y>
Select between required options. For the command to run, you must select from the listed options. If you omit a required option, the command line program returns an error message.
function Test {
[CmdletBinding()] param()
DynamicParam {
New-ParamSet @(
New-ParamSet @(
New-Param ([string]) x -Mandatory
)
New-ParamSet @(
New-Param ([string]) y -Mandatory
)
)
}
}Further steps to come to the final result below you can find in the Parameters.Test.ps1:
function Draw-Shape {
[CmdletBinding()] param()
DynamicParam {
# Define the optional RGB and HSL subsets
$Color = @(
New-ParamSet @(
New-Param ([int]) Red
New-Param ([int]) Green
New-Param ([int]) Blue
)
New-ParamSet @(
New-Param ([int]) Hue
New-Param ([int]) Sat
New-Param ([int]) Lum
)
)
# Define the optional mutually exclusive Transparency/Opacity subsets
$Tint = @(
New-ParamSet @(
New-Param ([int]) Transparency -Mandatory
$Color
)
New-ParamSet @(
New-Param ([int]) Opacity -Mandatory
$Color
)
)
# Create a (single) main set
New-ParamSet @(
New-ParamSet @(
New-Param 0 ([int]) Radius -Mandatory
$Tint
)
New-ParamSet @(
New-Param 0 ([int]) Width -Mandatory
New-Param 1 ([int]) Height -Mandatory
$Tint
)
New-ParamSet @(
New-Param 0 ([int]) Side1 -Mandatory
New-Param 1 ([int]) Side2 -Mandatory
New-Param 2 ([int]) Side3 -Mandatory
$Tint
)
)
}
}Draw-Shape -?
NAME
Draw-Shape
SYNTAX
Draw-Shape [-Radius] <int> -Transparency <int> [-Red <int>] [-Green <int>] [-Blue <int>] [<CommonParameters>]
Draw-Shape [-Radius] <int> -Opacity <int> [-Red <int>] [-Green <int>] [-Blue <int>] [<CommonParameters>]
Draw-Shape [-Width] <int> [-Height] <int> -Transparency <int> [-Red <int>] [-Green <int>] [-Blue <int>] [<CommonParameters>]
Draw-Shape [-Width] <int> [-Height] <int> -Opacity <int> [-Red <int>] [-Green <int>] [-Blue <int>] [<CommonParameters>]
Draw-Shape [-Side1] <int> [-Side2] <int> [-Side3] <int> -Transparency <int> [-Red <int>] [-Green <int>] [-Blue <int>] [<CommonParameters>]
Draw-Shape [-Side1] <int> [-Side2] <int> [-Side3] <int> -Opacity <int> [-Red <int>] [-Green <int>] [-Blue <int>] [<CommonParameters>]
Draw-Shape [-Radius] <int> -Transparency <int> [-Hue <int>] [-Sat <int>] [-Lum <int>] [<CommonParameters>]
Draw-Shape [-Radius] <int> -Opacity <int> [-Hue <int>] [-Sat <int>] [-Lum <int>] [<CommonParameters>]
Draw-Shape [-Width] <int> [-Height] <int> -Transparency <int> [-Hue <int>] [-Sat <int>] [-Lum <int>] [<CommonParameters>]
Draw-Shape [-Width] <int> [-Height] <int> -Opacity <int> [-Hue <int>] [-Sat <int>] [-Lum <int>] [<CommonParameters>]
Draw-Shape [-Side1] <int> [-Side2] <int> [-Side3] <int> -Transparency <int> [-Hue <int>] [-Sat <int>] [-Lum <int>] [<CommonParameters>]
Draw-Shape [-Side1] <int> [-Side2] <int> [-Side3] <int> -Opacity <int> [-Hue <int>] [-Sat <int>] [-Lum <int>] [<CommonParameters>]Caveats
- It concerns a prototype and there might be quiet some bugs a limitation but I think it might eventually replace the current parameter notation
- Consider a (default) parameter Provider (I am lacking the C# knowledge to set this up 😌)
- It might be slower than the current parameter notation (consider a
-Staticoption which caches theRuntimeDefinedParameterDictionary) - Default value is not work (yet)
- Dynamic parameters are not automatically put in variables, workaround:
foreach ($Name in $PSBoundParameters.get_keys()) {
if (!(Test-Path variable:$Name)) { Set-Variable -Name $Name -Value $PSBoundParameters[$Name] }
}
Activity
iSazonov commentedon Oct 5, 2020
In general I like the idea to enhance the parameter defining -
What about additional level? I mean an optional parameter could require some mandatory/optional subparameters.
And what about C# notation?
vexx32 commentedon Oct 5, 2020
The main reason I'm unsure about this style of declaring params is that you have declare the same parameter multiple times if it happens to be in multiple sets. This creates a greater possibility of mistakes and more confusing UX if a parameter by the same name appears to behave multiple different ways depending on the other parameters specified with it.
I would love to see a rework of parameter declaration, but I don't think this is the right direction to go. 🤔
ThomasNieto commentedon Oct 5, 2020
Looking at the command syntax it looks like this cmdlet is trying to do too much. A good rule of thumb that I've seen with PowerShell parameter set best practices is that if you're getting this many, maybe its time to create dedicated cmdlets. This many parameter sets is also confusing to the user as the syntax diagram is too complex.
For your example the cmdlet could be broken down into multiple cmdlets:
Draw-Shapegets broken down into three cmdlets:Draw-Circle -Radius <int>Draw-Rectangle -Height <int> -Width <int>Draw-Triangle -Side1 <int> -Side2 <int> -Side3 <int>Dedicated color cmdlets can be created to define either of the two color schemes. Then a parameter set can be added to
Draw-*for the-RGBColoror-HSLColoror to be a common parameter. The same can be done with-Transparencyand-Opacity.New-RGBColor -Red <int> -Blue <int> -Red <int>New-HSLColor -Hue <int> -Sat <int> -Lum <int>This can take the 12 parameter sets down to just a couple by going this route. There was also an old RFC that defined mutually exclusive parameters without needing a new parameter set.
iSazonov commentedon Oct 6, 2020
I added more info in #12818.
iRon7 commentedon Oct 6, 2020
Rain Sallow (/u/ta11ow) (@vexx32) and Thomas Nieto (@ThomasNieto), the 'Draw-Shape' is just an extreme example which two mutually exclusive parameter sets almost at the root of the main set.
Yes, there are about 22 (sub)sets in the
Draw-Shapeexample with a total of 58 parameters (where the current parameters notation has 14 parameters with 72 parameters-sets). As Ilya (@iSazonov) pointed out in #12818, my general objective is to use a syntax diagram which is similar to the (expanded) syntax displayed by the help. In the existing solution, you might limit some of the parameter-sets by using configuration items likeDefaultParameterSetNamebut it is difficult to further reduce the redundancy of this rather flat design. This is mainly because the attributes that usually relate to the parameter (likepositionandmandatory) are contaminated with aParameterSetNameproperty that actually belongs to parameter-set. In the purposed design it is easy to script any redundancy using variables or loops:Taking the parameter-set of the current
Where-Objectcmdlet for another example:vexx32 commentedon Oct 6, 2020
Yeah, you can do that with
dynamicparambut you wouldn't likely be able to do anything remotely similar if we had builtin syntax for this (good luck parsing that; it'd be insanely complicated to try to make that work in anything like aparamblock).Also... just in general, dynamicparam tends to cause more problems that it solves, so IMO any design based on it is generally best avoided (even when prototyping other designs). YMMV I suppose, but I don't think this is an approachable angle for users.
iRon7 commentedon Oct 6, 2020
I have updated the module (see also below the design definitions) which allows a for the intuitive
dynamicparameterstatement:Test -?SYNTAX Test -Man1 <string> [-Opt2 <string>] [-Opt1 <string>] [<CommonParameters>] Test [-Opt1 <string>] [<CommonParameters>]Unfortunately, I do not have enough C# experience to comment on this.
iRon7 commentedon Oct 6, 2020
Rain Sallow (/u/ta11ow) (@vexx32)
Agree, it is not in the intention to align the current
Param()notation with the purpose. Besides, as already noted, that would require an engine update where thedynamicparampurpose might only require a module which could even supply some downward compatibility with older (Windows) PowerShell versions.I don't think you can make a fair comparison (yet) between using the the current
Param()notation anddynamicparamscripting because there are currently no functions, cmdlets, frameworks (or provider), type accelerators, guidance, etc. to support the developers in using thedynamicparamsection. There are only a few .Net constructors (which you need to tie together) to work with. The issue is that the current parameter notation is far from DRY, very verbose and not as lean as you might expect. For this reason, I guess that there are a lot of developers (including myself) that often just lump all parameters together and throw an error from within input processing methods when comes to an incorrect parameter constrain.iSazonov commentedon Oct 6, 2020
I'd expect symmetric behavior for all levels.
Script and binary developers should get the same capabilities otherwise your work will be only partial research.
iRon7 commentedon Oct 6, 2020
That depends on the actual implementation. In this purpose, I trying to follow the syntax diagram¹, where subsets (
...[...][...[...][...]]) are not just extensions of the parent set but exclusive optional sets or in case all subsets contain at least one mandatory parameter, they are considered mutually exclusive sets².[MyParam]is created like:New-Param MyParamMyParamis created like:New-Param MyParam -Mandatory2) If it just concerns a single subset, all of the containing parameters are simply added to the parent parameters (unsure about this as you might as well put all the parameters simply in the parent set. I need to give this some more thoughts).See also below the design definitions and above reworked "What about additional level? I mean an optional parameter could require some mandatory/optional subparameters." answer to Ilya (@iSazonov).
iRon7 commentedon Oct 9, 2020
I have updated the prototype module to better support optional parameters (see the updated answer to Ilya (@iSazonov) "an optional parameter could require some mandatory/optional subparameters" comment) which also results in a more symmetric behavior (see the updated answer to Vex32 "I'd expect symmetric behavior for all levels" comment).
(Sub)set design
Below is the description how the
ParameterSetNameof each parameter is set with respect to their (sub)set containers.SetNamesupplied, theParameterSetNameis initially set to'0'(a string containing a zero).SetNameis added to:ParameterSetNameattribute of each optional parameterParameterSetNameattribute of each mandator parameter unless the set also contains subsetsParameterSetNameof the each attribute of each containing parameter is prefixedSetName(or'0', if no setname is supplied) separated by a dot'.'.ParameterSetNameparameter attribute in a subset that is already prefixed with a'0'(e.g.'0.1') will get a new unique index prefix (e.g.'1.1'). In the case of a named subset, the developer is responsible to supply unique names for each subset.Note that with each subset iteration, the whole parameter set (including the containing parameter attributes) is cloned. This is to support parameter sets supplied in variable (which eventually require an unique
ParameterSetName).(Sub)set usage:
The basic idea of this purpose is to follow the standard parameter syntax diagram similar to the Help (
-?) syntax output..There is one exception where it concerns the definition of a single parameter:
[MyParam], is created like:New-Param MyParamMyParam, is created like:New-Param MyParam -MandatoryEvery parameter subset is exclusive
This means that a user can select a specific subset (and supply the contained mandatory parameters) but can't choose parameters out of different subsets
This means that a user must choose one specific subset
This means that the user might choose one subset (or none of the subsets)
The user is required to supply both mandatory parameters of the selected subset and the mandatory parameters of its parent set.
microsoft-github-policy-service commentedon Nov 16, 2023
This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.
microsoft-github-policy-service commentedon Nov 16, 2023
This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.
microsoft-github-policy-service commentedon Nov 16, 2023
This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.
microsoft-github-policy-service commentedon Nov 23, 2023
This issue has been marked as "No Activity" as there has been no activity for 6 months. It has been closed for housekeeping purposes.