Powershell: Allow Functions and Cmdlets written in PowerShell to inherit Parameters

Created on 6 Apr 2018  路  13Comments  路  Source: PowerShell/PowerShell

For Cmdlets written in C# it is possible to inherit Parameters from other Cmdlets, but with the new features in recent PowerShell versions, more and more Cmdlets are written in PowerShell and it would be very helpful if Functions and Cmdlets written in PowerShell would be able to inherit Parameters from other Functions or Cmdlets.

This is especially important for Cmdlets interacting with REST endpoints which often have a large number of REST Commands which could result in several Cmdlets which share a common set of parameters.

This feature could be similar to [CmdletBinding()] which is already adding common Cmdlet parameters to a function.

Issue-Enhancement WG-Language

Most helpful comment

I floated the idea in the Improvements for classes #6418 thread to allow us to create cmdlets with PowerShell classes like we can with C#. That would be a way to provide this functionality.

All 13 comments

@ffeldhaus Can you provide some concrete examples of how you think this should look? Also some things to think through...

There is a difference between inheriting from a class and inheriting from a command: type inheritance is done statically at compile time (even with PowerShell classes). Command inheritance would have to be done dynamically (i.e. at run time) so that implies dynamic parameters. In fact, using the existing dynamic parameter mechanism, you can (somewhat awkwardly) do this today. Take a look at this comment from @jleechpe on a semi-related question from StackOverflow . The question is about mocking specifically but the code takes a function to mock and generates dynamic parameters based on the mocked function's parameters. This mechanism could be built into the runtime itself fairly easily. Note that this approach has two caveats: it will be slow relative to a straight function dispatch and it may be nondeterministic - the command you think you're inheriting from might not be the one the runtime finds.

An alternate approach that could resolve everything at compile time would be to have the command "inherit" from a class. What do you think about this approach?

Finally, what happens when there are the begin, process and end blocks in the base function. How do you see these being handled? Are they automatically called then the derived functions equivalent is called. Consider the case where the begin in the base function does some complex initialization with the parameters.

I think @ffeldhaus wants it in the case where you鈥檙e building a set of functions to access API calls. Since the parameters would mostly be the same being able to inherit would save code duplication (especially if changes needed to be made). I don鈥檛 think it鈥檚 a matter of calling the inherited function, just having it鈥檚 parameters made available (or at least I would have a use for it that way).

In my case it was for reports where I needed a set of criteria filters (first for an sql query and later for lists of machines) where the parameters were for different groups of devices or time spans. I ended up needing the filter functions for multiple reports and was building out the functions for users who needed the actual call to be as simple as possible.

I had to recreate the parameters in each report (and then update them every time the sets of criteria were changed because of additional use cases), so I ended up using the dynamic parameters so I didn鈥檛 need to remember to update the 5+ primary functions (type of report + criteria) every time the business wanted a new criteria option. Having a way to have a function inherit parameters and parameter sets natively would simplify the process.

The actual calls to the filter functions were as pipelines in the management function so the begin and end blocks processed fine.

Hmm... Well, could something like this be workable in terms of syntax?

param(
    [Parameter(ParentFunction = "Get-ChildItem")]
    $Path
)

One way or another, you'd still have to specify the parameter name, but this would allow you to 'borrow' the validation checks and any other attributes the parameter has in that function. I have very little idea of how parameter attributes actually work behind the scenes, but essentially PS should look at that attribute, go "OK, let's check Get-ChildItem for this parameter", take all its metadata from that function and apply it to the parameter here.

One concern I have with this type of 'inheritance' is that by design it can potentially obscure what the requirements of the parameter are. A potential method of mitigating that confusion is that the help for that parameter cannot be specified by the function; it is referenced and pulled from the parent function instead.

Something like that could work. However it wouldn鈥檛 help quite as much of you鈥檝e got 5 or 6 variables, if not more, that you鈥檇 be doing that for. If it were a similar construct that let you take all the parameters you鈥檇 still run the risk of obscuring the requirements, but if you only allow one inheritance from one function (and only of it鈥檚 parameters, not inherited ones (if any)) you鈥檇 at least limit the effects.

Hmm, yeah, that makes a lot more sense... So it'd probably look a bit more like... huh...

function Wrapper-Function {
    [CmdletBinding()]
    InheritParams('Get-ChildItem')
    #do stuff
}

That would be... a bit odd. It would also require changing probably a bit of the syntax checking logic around [CmdletBinding()] in the VS Code extension, because as-is if it's not directly followed by a param() block it calls it out as an error.

Not sure if that works better mimicking param() or as an attribute, but... hm. Interesting idea nonetheless.

I floated the idea in the Improvements for classes #6418 thread to allow us to create cmdlets with PowerShell classes like we can with C#. That would be a way to provide this functionality.

That would absolutely be the most effective way to get it done, I think. It would also make for an interesting addition in and of itself, being able to define C#-style cmdlets in native PS code...

I agree that being able to base Cmdlets on PowerShell classes would be the best way to get this implemented.

With C# there seems to be another way to share parameters between cmdlets by using Dynamic Parameters.

@ffeldhaus You can actually implement parameter sharing using dynamic parameters in script as illustrated in the rather long example below. The most significant line is this

 DynamicParam { Get-BaseParameters baseFunction }

Include this line in the derived function definition and you will "inherit" the properties from baseFunction. Here's the full example:

using namespace System.Management.Automation
using namespace System.Management.Automation.Internal

function Get-BaseParameters( $base )
{
    $base = Get-Command $base
    $common = [CommonParameters].GetProperties().name
    if ($base) 
    {
        $dict = [RuntimeDefinedParameterDictionary]::new()
        $base.Parameters.GetEnumerator().foreach{
            $val = $_.value
            $key = $_.key
            if ($key -notin $common)
            {
                $param = [RuntimeDefinedParameter]::new(
                    $key, $val.parameterType, $val.attributes)
                $dict.add($key, $param)
            }
        }
        return $dict
    }
}


<#
    Function to inherit from
#>
function baseFunction
{
    [CmdletBinding()]
    param (
        [Parameter()]
            $Foo,
        [Parameter()]
            $Bar,
        [Parameter()]
            $Baz
    )
}

<#
    Function that extends the base function
#>
function derivedFunction 
{
    [cmdletbinding()]
    param(
        [Parameter()]
            $Zork
    )

    DynamicParam { Get-BaseParameters baseFunction }

    End 
    {
        [PSCustomObject] $PSBoundParameters
    }
}

derivedFunction -foo stuff -bar moreStuff -baz soThere -Zork "ZORK!"

The problem with the dynamic parameter approach is that they end up as second class citizens. Try running New-proxycommand on any of the AD cmdlets and you will see what I mean.

gcm gci,get-aduser | % {[System.Management.Automation.CommandMetaData]::new($_)} | fl parameters

I've run into this to when building a module wrapping p4.exe. It has the basic structure of
p4 [options] command [arg ...]

arg is unique to the command, but options are common to all, and it was not pleasant to try to maintain these.
I ended up moving to C#. Worked great for me, but maybe not what we want to recommend.

One question here is what prevents us from writing cmdlets in PowerShell classes?
That way you could write a base class with the common parameters.

Or some mix - not sure if this is a good idea - but letting a param block inherit from a class, or another function.

function CommonParameters {
  param(
    [Parameter()] 
    [string] $Port
    ,
    [Parameter()] 
    [string] $User
  )
}

# or 

class CommonParameters {
  [Parameter()] 
  [string] $Port

  [Parameter()] 
  [string] $User
}

function Do-Stuff {
  param : CommonParameters (
    [switch] $Force 
  )
}
PS> gcm -Syntax Do-Stuff

Do-Stuff [[-Port] <string>] [[-User] <string>] [-Force] [<CommonParameters>]

Inheritance of parameters is often insufficient - you often want to also share some code. This isn't hard to do with functions, but classes do provide a cleaner way.

Just to add to the mix here, there's also the pain point when you derive from a cmdlet base class that has parameters with parametersets defined. Sometimes you want to add new parameters to the base sets in the derived class (or include base parameters in the derived sets), and you can't do this without overriding/shadowing the [Parameter] decorated properties and redefining a new parameterattribute. It would be nice to be able to do this more succinctly.

// override base parameter LiteralPath to use position 1 and include in this parameter set
[Parameter(Include=nameof(base.LiteralPath), Position=1, ParameterSetName='NewParamSet')]
[Parameter(Position=0, Mandatory=true, ParameterSetName='NewParamSet')]
public string NewParam1 { get; set; }

This could be implemented without having to rewire the entire engine by creating a CustomReflectionContext to surface an additional synthetic ParameterAttribute to the base property when the engine is discovering parameters. Worst case you'd have to scan the assembly once for this new [Parameter(Include=...)] directive, then create the context and scan again.

Thoughts, @lzybkr ?

Was this page helpful?
0 / 5 - 0 ratings