Cmdlets without a dll

PowerShell defines a number of different types of command. Functions are written in PowerShell (normally), and Cmdlets are written in a compiled language (C# more often than not).

Classes, introduced with PowerShell 5, offer the possibility of creating a Cmdlet in PowerShell. The problem is, defining a class or adding a type is not enough to “register” a Cmdlet. Import-Module handles this step for compiled modules, but not script modules.

The first step is to define a simple class, inheriting from PSCmdlet with all the appropriate attributes.

using namespace System.Management.Automation

[CmdletAttribute('Get', 'Something')]
[Outputtype([String])]
class GetSomethingCommand : PSCmdlet {
    [Parameter()]
    [String]$Parameter
    
    [Void] ProcessRecord() {
        $this.WriteObject('Doing something')
        
        if ($this.Parameter) {
            $this.WriteObject($this.Parameter)
        }
    }
}

Once the class has been defined it needs to be imported. Getting at the part of PowerShell that allows a Cmdlet to be imported is a bit more difficult. SessionStateInternal appears to be the right choice of objects to build, but it is a non-public class which expects a non-public argument, ExecutionContext.

As the $ExecutionContext variable holds an EngineIntrinsics object instance it has to be put aside. An alternative is to call the GetExecutionContextFromTLS static method. This is a non-public method on the non-public class, LocalPipeline. Getting the method via reflection provides an avenue for invoking it.

$type = [PowerShell].Assembly.GetType('System.Management.Automation.Runspaces.LocalPipeline')
$method = $type.GetMethod(
    'GetExecutionContextFromTLS',
    [Reflection.BindingFlags]'Static,NonPublic'
)

The method itself does not require an argument, but as it is non-public there is a need to pass binding flags.

$context = $method.Invoke(
    $null,
    [Reflection.BindingFlags]'Static,NonPublic',
    $null,
    $null,
    (Get-Culture)
)

Now the ExecutionContext has been acquired it’s time to create the SessionStateInternal object. Get the type and a constructor which accepts ExecutionContext:

$type = [PowerShell].Assembly.GetType('System.Management.Automation.SessionStateInternal')
$constructor = $type.GetConstructor(
    [Reflection.BindingFlags]'Instance,NonPublic',
    $null,
    $context.GetType(),
    $null
)

Once the constructor is in hand, create the SessionStateInternal object:

$sessionStateInternal = $constructor.Invoke($context)

A simple check of the current path work towards showing whether or not this is actually the right object.

$currentLocation = $type.GetProperty(
    'CurrentLocation',
    [Reflection.BindingFlags]'Instance,NonPublic'
)
$currentLocation.GetValue($sessionStateInternal)

The SessionStateInternal class has a number of methods for handler addition of functions, Cmdlets, Providers, and so on. The method used to add a Cmdlet expects an object of type SessionStateCmdletEntry. That may be created as follows.

$sessionStateCmdletEntry = New-Object System.Management.Automation.SessionStateCmdletEntry(
    'Get-Something',
    [GetSomethingCommand],
    $null
)

Using GetMethod along with this object allows selection of the correct method from SessionStateInternal which can be immediately invoked.

$method = $type.GetMethod(
    'AddSessionStateEntry',
    [Reflection.BindingFlags]'Instance,NonPublic',
    $null,
    $sessionStateCmdletEntry.GetType(),
    $null
)
# Invoke the method.
$method.Invoke($sessionStateInternal, $sessionStateCmdletEntry)

Putting the whole thing together, a little function may be created.

Cmdlets without a dll
Share this