Powershell: Is it time for a "cmdlet" keyword?

Created on 3 Feb 2019  路  16Comments  路  Source: PowerShell/PowerShell

I think maybe it's time we replace function + cmdletbinding with a new and improved keyword that would make it easier for authors to "fall into the pit of success" and write "advanced" script functions the "best" way ...

Right now, a "best practices" function has to include a lot of repetitive boilerplate work (something like this):

function Build-Noun {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipelineByPropertyName)]$Parameter
    )
    end {
        # This try/catch ensures that if someone downstream uses `throw` ...
        # or is otherwise misusing/altering ErrorActionPreference ...
        # we catch it here, rather than passing the problem to our callers
        try {
            <# YOUR CODE HERE # >
        } catch {
            throw $_ # the $_ is important, don't remove it
        } finally {
            <# -- dispose, if necessary -- #>
        }
    }
}

I would like to see that wrapped up in a new cmdlet (or advancedfunction or function2 ...) keyword which would be the _new_ recommended way of writing commands in modules, and would, for instance:

  • imply CmdletBinding
  • simplify error handling by:

    • add a OnError common parameter #6010

    • wrapping the begin, process, end blocks in a try/catch/finally and rethrow so throw works the same as $PSCmdletBinding.ThrowTerminatingError does

    • adding a disposing block #6673 (called within the finally) for cleaning up handles

  • imply ValueFromPipelineByPropertyName (you'd opt out instead of opting in)
  • make the process block the default block, instead of end (like filter does)

We could perhaps even:

  • make these more strict, like class syntax:

    • use lexical scoping

    • make Write-Output mandatory for output

    • force OutputType documentation and adherence

  • make DynamicParameters easier to use: add accelerators, and allow streaming output of dynamic parameter objects instead of requiring explicit creation of the RuntimeDefinedParameterDictionary

... what else?

Issue-Discussion Issue-Enhancement

Most helpful comment

I'd really like to see proper Path and LiteralPath parameter a bit more "built-it". To get all the standard behaviors of built-in cmdlets: wilcard expansion, write appropriate error on non-existing path (when path should exist), proper pipeline binding behavior (aliasing PSPath), etc. Not sure exactly how this would be implemented but I've often wanted a PathParamter() attribute e.g.:

function Get-Foo {
    [CmdletBinding()]
    param(
        [PathParameter(Position=0, ExpandWildcards)]
        [string[]]
        $Path,

        [PathParameter(Position=0, PathMustExist)]
        [string[]]
        $LiteralPath,   
    )
    ...
}

By the time you see $Path, it has had the supplied Path argument transformed into one or more paths (full expanded). And if a required path does not exist, PowerShell would write the appropriate error record for the function.

All 16 comments

  • The process block should be the default block if not specified.
  • Correct support for -ErrorAction Ignore
  • Use lexical scoping instead of dynamic scoping

I'd really like to see proper Path and LiteralPath parameter a bit more "built-it". To get all the standard behaviors of built-in cmdlets: wilcard expansion, write appropriate error on non-existing path (when path should exist), proper pipeline binding behavior (aliasing PSPath), etc. Not sure exactly how this would be implemented but I've often wanted a PathParamter() attribute e.g.:

function Get-Foo {
    [CmdletBinding()]
    param(
        [PathParameter(Position=0, ExpandWildcards)]
        [string[]]
        $Path,

        [PathParameter(Position=0, PathMustExist)]
        [string[]]
        $LiteralPath,   
    )
    ...
}

By the time you see $Path, it has had the supplied Path argument transformed into one or more paths (full expanded). And if a required path does not exist, PowerShell would write the appropriate error record for the function.

@rkeithhill I think perhaps a separate [PathTransform] would be better than attempting to merge it with the base [Parameter] attribute.

I would also like this construct to 'swallow' any break coming from called commands and also scope internal breaks to just this command.

On top of that some easy way to return in any of 3 blocks would be nice to have (so that I can stop processing in Begin w/o even starting Process block).

One things I would really like is the ability to have a way to trust the output type of a function.

Today we have OutputType which allows to specifiy which type will be returned from a that specific function.

Unfortunatley as specified in the documentation:

The OutputType attribute value is only a documentation note. It is not derived from the function code or compared to the actual function output. As such, the value might be inaccurate.

Parhaps we could think of having the possibility to have something like a strict keyword in the function declaration. It would check the output type with the one returned from a function, and throw an error if the wrong type is returned

Something like: [cmdletBinding(Strict)] maybe?

I could imagine as well, that if 'strict' is set, that the usage of the return statement would become mandatory.

This would help in predicting the behaviour of our functions.

IMHO a cmdlet keyword built on top of the class keyword would be best (i.e. a cmdlet would be a very specific implementation of class). I shared my thoughts on this on this thread.

@Stephanevg forcing a function to use return as a keyword is counterproductive, as it prevents the ability to output data as it is processing, forcing it to collate before outputting anything.

@rkeithhill @vexx32 do you maybe want to submit a PR to my Path attribute, or do you think it really needs to be in the box (also)?

@Stephanevg @vexx32 I think we could strongly type the OutputType and still use Write-Output to allow streaming ...

I think such an attribute should be in the box. While the Transform attributes aren't perfect, they are a very useful way to ensure input is in the correct form without cluttering the begin{} blocks of a function. Something as fundamental to PowerShell's operation as the _correct handling of paths_ should absolutely be in the box as a prebuilt solution which can be extended if needed via inheritance.

As for outputtype, I agree that strongly-typing one's outputtype is a good idea. I don't think it should throw if there is a reasonably clear conversion path from the output object and the target output type, though. I also don't think we need to enforce return; after all, the overall return types of BeginProcessing, ProcessRecord, and EndProcessing are _all_ void. I don't think it makes sense to muddy the implementation there.

However, enforcing the use of Write-Output or $PSCmdlet.WriteObject() may be a worthwhile consideration... _if_ such an operation can be made equally performant to the current "just drop it to output". Currently, I know Write-Output is a bit slower, but I'm not sure if WriteObject() has a similar performance impact, though I'd expect it to be about as fast as dropping directly into the output stream.

@vexx32 (sorry, I didn't meant to lead you further off topic -- I don't disagree that it should be in the box, I filed issues about it a decade ago before I wrote my own solution).

Maybe the answer to the Write-Output/return question is that cmdlet should expose $PSCmdlet as $this and you'd be required to call that for output if you didn't want to _end_ your block. That would certainly fit in with the idea of making these actual class-based as Kirk has suggested, and ...

Perhaps we make these cmdlet objects instead of function objects -- i.e. same precedence as compiled cmdlets, and can only be exposed from a module, etc. Then, switching to this and to class-like semantics (with $this.WriteOutput()) would make even more sense.

MAYBE we even create a new file extension .pscmd which is equivalent to the cmdlet keyword. We could make modules collect all .pscmd files that are in the module (sub)folders and "compile" them at module import time...

That's almost exactly where my mind was heading @Jaykul, including a new file extension, but I liked .psx1 (fits current file extension names), for a new type of module that is compiled, not interpreted. That's discussed on the same thread I linked to earlier, the idea being that .psx1 files (class modules) would have a limited set of things you could do in them.

I was originally thinking psx1 files could only be used to define cmdlets or classes and include other files that also define cmdlets or classes (dot source? using? a new include keyword? not sure what's the best semantic for that in this context). Functions and other PowerShell commands wouldn't be allowed -- psm1 files already allow you to include those in modules, and there's nothing stopping you from having a psd1 file with a psx1 file or a psd1 file with both psx1 and psm1 files (leveraging RequiredModules and NestedModules according to your needs). Essentially, the psx1 file extension identifies that the content is compiled (which .pscmd would also accomplish, but I like the notion of a compiled module).

To further the discussion of the classy cmdlets, Another paradigm often requested by more advanced users is the ability to inherit a base cmdlet that contains common parameters. That's something some what trivial to do in C# but impossible in native PS.

It's not always trivial in C#, at least not for all cmdlets. Some cmdlets are configured as sealed, preventing other devs from inheriting from them. That's gotten in my way a number of times, but can be partially worked around by creating proxy cmdlets (like proxy functions, but cmdlets), but you need to redefine parameters with that model. I suppose I should submit some PRs to change some of those cmdlets so that they are not sealed, because PowerShell is open source and I don't believe having cmdlet classes as sealed does anything but get in the way when an advanced dev wants to do something creative with existing functionality.

@KirkMunro I'm specifically talking about a base class you create and then other classes you create derive from it. For example, if you have an API wrapper module, you will likely have a Session and other common parameters on pretty much all get/set/remove/update cmdlets. In Native PS, you have to manually define all of those common parameters in every advanced function. In c# it is trivial to derive from your own base class to reduce code repetition.

How much of this can be prototyped via a source-to-source compiler? That might be a nice way to experiment and prove out the idea, before it gets added to PSCore proper.

I started prototyping a source-to-source transformer to implement some of these ideas. I would love to hear what people think: bugs, ideas, whatever. https://github.com/cspotcode/pwsh-transpiler

The eventual goal is that you can write modules assuming the future syntax proposed in this thread, and the transformer will rewrite your functions to give you those behaviors. Eventually, when these capabilities are built-in to PowerShell Core, your code will not need to be transformed.

What I've written so far only took me the past couple hours; I think with a bit more time we could make a productive, useful set of transformers.

EDIT: I can make anyone a collaborator on the repo if you want to start pushing code.

Was this page helpful?
0 / 5 - 0 ratings