Skip to content

Instantly share code, notes, and snippets.

@rikwatson
Created March 31, 2019 19:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rikwatson/38ceb86d92b068cbb67eba16061d38e2 to your computer and use it in GitHub Desktop.
Save rikwatson/38ceb86d92b068cbb67eba16061d38e2 to your computer and use it in GitHub Desktop.
Classes in powershell
# ===================================================================================
# Inspired by Bruce Payette's "Windows PowerShell in Action"
# Chapter 8 Script to add a CustomClass "keyword" to PowerShell
# http://manning.com/payette/
# ===================================================================================
function New-PSClass
( [string] $ClassName = { Throw "ClassName required for New-PSClass"}
, [scriptblock] $definition = { Throw "Definition required for New-PSClass"}
, $Inherit
)
{
#======================================================================
# These Subfunctions are used in Class Definition Scripts
#======================================================================
# - - - - - - - - - - - - - - - - - - - - - - - -
# Subfunction: constructor
# Assigns Constructor script to Class
# - - - - - - - - - - - - - - - - - - - - - - - -
function constructor ( [scriptblock] $script=$(Throw "Script is required for 'constructor' in $ClassName"))
{
if ($class.ConstructorScript)
{
Throw "Only one Constructor is allowed"
}
$class.ConstructorScript = $script
}
# - - - - - - - - - - - - - - - - - - - - - - - -
# Subfunction: note
# Adds Notes record to class if non-static
# - - - - - - - - - - - - - - - - - - - - - - - -
function note
{
param ( [string]$name={Throw "Note Name is Required"}
, $value=""
, [switch] $private
, [switch] $static
)
if ($static)
{
if ($private)
{
Throw "Private Static Notes are not supported"
}
Attach-PSNote $class $name $value
}
else
{
$class.Notes += @{Name=$name;DefaultValue=$value;Private=$private}
}
}
# - - - - - - - - - - - - - - - - - - - - - - - -
# Subfunction: method
# Add a method script to Class definition or
# attaches it to the Class if it is static
# - - - - - - - - - - - - - - - - - - - - - - - -
function method
( [string]$name=$(Throw "Name is required for 'method'")
, [scriptblock] $script=$(Throw "Script is required for 'method' $name in Class $ClassName")
, [switch] $static
, [switch]$private
, [switch]$override )
{
if ($static)
{
if ($private)
{
Throw "Private Static Methods not supported"
}
Attach-PSScriptMethod $class $name $script
}
else
{
$class.Methods[$name] = @{Name=$name;Script=$script;Private=$private;Override=$override}
}
}
# - - - - - - - - - - - - - - - - - - - - - - - -
# Subfunction: property
# Add a property to Class definition or
# attaches it to the Class if it is static
# - - - - - - - - - - - - - - - - - - - - - - - -
function property ([string]$name, [scriptblock] $get, [scriptblock] $set, [switch]$private, [switch]$override, [switch] $static )
{
if ($static)
{
if ($private)
{
Throw "Private Static Properties not supported"
}
Attach-PSProperty $class $name $get $set
}
else
{
$class.Properties[$name] = @{Name=$name;GetScript=$get;SetScript=$set;Private=$private;Override=$override}
}
}
$class = new-object Management.Automation.PSObject
# Class Internals
Attach-PSNote $class ClassName $ClassName
Attach-PSNote $class Notes @()
Attach-PSNote $class Methods @{}
Attach-PSNote $class Properties @{}
Attach-PSNote $class BaseClass $Inherit
Attach-PSNote $class ConstructorScript
Attach-PSNote $class PrivateName "__$($ClassName)_Private"
Attach-PSScriptMethod $class AttachTo {
function AttachAndInit($instance, [array]$parms)
{
$instance = __PSClass-AttachObject $this $instance
__PSClass-Initialize $this $instance $parms
$instance
}
$type = $Args[0].GetType()
[array]$parms = $Args[1]
if (($Args[0] -is [array]) -or ($Args[0] -is [System.Collections.ArrayList]))
{
# This handles the attachment of an array of objects
$objects = $Args[0]
foreach($object in $objects)
{
$(AttachAndInit $object $parms) > $null
}
}
else
{
AttachAndInit $Args[0] $parms
}
}
Attach-PSScriptMethod $class New {
$instance = new-object Management.Automation.PSObject
$this.AttachTo( $instance, $Args )
}
Attach-PSScriptMethod $class __LookupClassObject {
__PSClass-LookupClassObject $this $Args[0] $Args[1]
}
Attach-PSScriptMethod $class InvokeMethod {
__PSClass-InvokeMethod $this $Args[0] $Args[1] $Args[2]
}
Attach-PSScriptMethod $class InvokeProperty {
__PSClass-InvokePropertyMethod $this $Args[0] $Args[1] $Args[2] $Args[3]
}
# At last, execute definition script
$output = &$definition
if ($output -ne $null)
{
Throw "PSClass Definition has invalid output objects $output"
}
# return constructed class
$class
}
function Deserialize-PSClass ($deserialized)
{
$class = $deserialized.Class
if(-not $class.AttachTo)
{
Attach-PSScriptMethod $class AttachTo {
function AttachAndInit($instance)
{
$instance = __PSClass-AttachObject $this $instance
return $instance
}
AttachAndInit $Args[0]
}
Attach-PSScriptMethod $class __LookupClassObject {
__PSClass-LookupClassObject $this $Args[0] $Args[1]
}
Attach-PSScriptMethod $class InvokeMethod { param($MethodName, $instance, [array]$parms)
__PSClass-InvokeMethod $this $MethodName $instance $parms
}
Attach-PSScriptMethod $class InvokeProperty {
__PSClass-InvokePropertyMethod $this $Args[0] $Args[1] $Args[2] $Args[3]
}
Attach-PSNote $class PrivateName "__$($class.ClassName)_Private"
}
$instance = new-object Management.Automation.PSObject
$instance = $class.AttachTo($instance)
foreach($private in $class.Notes | ? { $_.Private })
{
$originalValue = $deserialized.$($deserialized.Class.PrivateName).$($private.Name)
if($originalValue -is [system.collections.arraylist] -and $originalValue[0].Class -ne $null)
{
$value = @()
for($i = 0; $i -lt $originalValue.Count; $i++)
{
$value += @(Deserialize-PSClass $originalValue[$i])
}
}
elseif($originalValue -isnot [system.collections.arraylist] -and $originalValue.Class)
{
$value = Deserialize-PSClass $originalValue
}
else
{
$value = $originalValue
}
$instance.$($instance.Class.PrivateName).$($private.Name) = $value
}
foreach($public in $class.Notes | ? { -not $_.Private })
{
$instance.$($public.Name) = $deserialized.$($public.Name)
}
return $instance
}
# ===================================================================================
# These helper Cmdlets should only be called by New-PSClass. They exist to reduce
# the amount of code attached to each PSClass object. They rely on context
# variables not passed as parameters.
# ===================================================================================
# __PSClass-Initialize
# Invokes Constructor Script and provides helper Base function to facilitate
# Inherited Constructors
# ===================================================================================
function __PSClass-Initialize ($class, $instance, $params)
{
function Base
{
if ($this.Class.BaseClass -eq $null)
{
Throw "No BaseClass implemented for $($this.Class.ClassName)"
}
__PSClass-Initialize $this.Class.BaseClass $this $Args
}
trap {
if ( $_.Exception.Message -match "Error Position:" )
{
$errorMsg = $_.Exception.Message
}
else
{
$errorMsg = $_.Exception.Message +
@"
Error Position:
"@ + $_.Exception.ErrorRecord.InvocationInfo.PositionMessage
}
$errorMsg = ($errorMsg -replace '(Exception calling ".*" with ".*" argument\(s\)\: ")(.*)','' )
Throw $errorMsg
}
if ($class.ConstructorScript)
{
$constructor = $class.ConstructorScript
$private = $Instance.($class.privateName)
$this = $instance
$constructor.InvokeReturnAsIs( $params )
}
}
# ===================================================================================
# __PSClass-AttachObject
# Attaches Notes, Methods, and Properties to Instance Object
# ===================================================================================
function __PSClass-AttachObject ($Class, [PSObject] $instance)
{
function AssurePrivate
{
if ($instance.($Class.privateName) -eq $null)
{
Attach-PSNote $instance ($class.privateName) (new-object Management.Automation.PSObject)
Attach-PSNote $instance.($class.privateName) __Parent
}
$instance.($class.privateName).__Parent = $instance
}
# - - - - - - - - - - - - - - - - - - - - - - - -
# Attach BaseClass
# - - - - - - - - - - - - - - - - - - - - - - - -
if ($Class.BaseClass -ne $null)
{
$instance = __PSClass-AttachObject $Class.BaseClass $instance
}
Attach-PSNote $instance Class $Class
# - - - - - - - - - - - - - - - - - - - - - - - -
# Attach Notes
# - - - - - - - - - - - - - - - - - - - - - - - -
foreach ($note in $Class.Notes)
{
if ($note.Private)
{
AssurePrivate
Attach-PSNote $instance.($Class.privateName) $note.Name $note.DefaultValue
}
else
{
Attach-PSNote $instance $note.Name $note.DefaultValue
}
}
# - - - - - - - - - - - - - - - - - - - - - - - -
# Attach Methods
# - - - - - - - - - - - - - - - - - - - - - - - -
foreach ($key in $Class.Methods.keys)
{
$method = $Class.Methods[$key]
$targetObject = $instance
# Private Methods are attached to the Private Object.
# However, when the script gets invoked, $this needs to be
# pointing to the instance object. $ObjectString resolves
# this for InvokeMethod
if ($method.private)
{
AssurePrivate
$targetObject = $instance.($Class.privateName)
$ObjectString = '$this.__Parent'
}
else
{
$targetObject = $instance
$ObjectString = '$this'
}
# The actual script is not attached to the object. The Script attached to Object calls
# InvokeMethod on the Class. It looks up the script and executes it
$instanceScriptText = $ObjectString + '.Class.InvokeMethod( "' + $method.Name + '", ' + $ObjectString + ', $Args )'
$instanceScript = $ExecutionContext.InvokeCommand.NewScriptBlock( $instanceScriptText )
Attach-PSScriptMethod $targetObject $method.Name $instanceScript -override:$method.Override
}
# - - - - - - - - - - - - - - - - - - - - - - - -
# Attach Properties
# - - - - - - - - - - - - - - - - - - - - - - - -
foreach ($key in $Class.Properties.keys)
{
$Property = $Class.Properties[$key]
$targetObject = $instance
# Private Properties are attached to the Private Object.
# However, when the script gets invoked, $this needs to be
# pointing to the instance object. $ObjectString resolves
# this for InvokeMethod
if ($Property.private)
{
AssurePrivate
$targetObject = $instance.($Class.privateName)
$ObjectString = '$this.__Parent'
}
else
{
$targetObject = $instance
$ObjectString = '$this'
}
# The actual script is not attached to the object. The Script attached to Object calls
# InvokeMethod on the Class. It looks up the script and executes it
$instanceScriptText = $ObjectString + '.Class.InvokeProperty( "GET", "' + $Property.Name + '", ' + $ObjectString + ', $Args )'
$getScript = $ExecutionContext.InvokeCommand.NewScriptBlock( $instanceScriptText )
if ($Property.SetScript -ne $null)
{
$instanceScriptText = $ObjectString + '.Class.InvokeProperty( "SET", "' + $Property.Name + '", ' + $ObjectString + ', $Args )'
$setScript = $ExecutionContext.InvokeCommand.NewScriptBlock( $instanceScriptText )
}
else
{
$setScript = $null
}
Attach-PSProperty $targetObject $Property.Name $getScript $setScript -override:$Property.Override
}
$instance
}
# ===================================================================================
# __PSClass-LookupClassObject
# intended to look up methods and property objects on the Class. However,
# it can be used to look up any Hash Table entry on the class.
#
# if the object is not found on the instance class, it searches all Base Classes
#
# $ObjectType is the HashTable Member
# $ObjectName is the HashTable Key
#
# it returns the Class and Hashtable entry it was found in
# ===================================================================================
function __PSClass-LookupClassObject ($Class, $ObjectType, $ObjectName)
{
$object = $Class.$ObjectType[$ObjectName]
if ($object -ne $null)
{
$Class
$object
}
else
{
if ($Class.BaseClass -ne $null)
{
$Class.BaseClass.__LookupClassObject($ObjectType, $ObjectName)
}
}
}
# ===================================================================================
# __PSClass-InvokeScript
# Used to invoke Method and Property scripts
# It adds an error handler so Script Info can be seen in the error
# It marshals $this and $private variables for the context of the script
# It provides a helper Invoke-BaseClassMethod for invoking base class methods
# ===================================================================================
function __PSClass-InvokeScript ($class, $script, $object, [array]$parms )
{
function Invoke-BaseClassMethod ($methodName, [array]$parms)
{
if ($this.Class.BaseClass -eq $null)
{
Throw "$($this.Class.ClassName) does not have a BaseClass"
}
$class,$method = $this.Class.BaseClass.__LookupClassObject('Methods', $MethodName)
if ($method -eq $null)
{
Throw "Method $MethodName not defined for $className"
}
__PSClass-InvokeScript $class $method.Script $this $parms
}
trap {
if ( $_.Exception.Message -match "Error Position:" )
{
$errorMsg = $_.Exception.Message
}
else
{
$errorMsg = $_.Exception.Message +
@"
Error Position:
"@ + $_.Exception.ErrorRecord.InvocationInfo.PositionMessage
}
$errorMsg = ($errorMsg -replace '(Exception calling ".*" with ".*" argument\(s\)\: ")(.*)','' )
Throw $errorMsg
}
$this = $object
$private = $this.($Class.privateName)
if($script -is [string])
{
[ScriptBlock]::Create($script).InvokeReturnAsIs( $parms )
}
else
{
$script.InvokeReturnAsIs( $parms )
}
}
# ===================================================================================
# __PSClass-InvokeMethod
# Script called by methods attached to instances. Looks up Method Script
# in instance class or in inherited class
# ===================================================================================
function __PSClass-InvokeMethod($Class, $MethodName, $instance, [array]$parms)
{
$FoundClass,$method = $Class.__LookupClassObject('Methods', $MethodName)
if ($method -eq $null)
{
Throw "Method $MethodName not defined for $($Class.ClassName)"
}
__PSClass-InvokeScript $FoundClass $method.Script $instance $parms
}
# ===================================================================================
# __PSClass-InvokePropertyMethod
# Script called by property scripts attached to instances. Looks up property Script
# in instance class or in inherited class
# ===================================================================================
function __PSClass-InvokePropertyMethod ($Class, $PropertyType, $PropertyName, $instance, [array]$parms)
{
$FoundClass,$property = $Class.__LookupClassObject('Properties', $PropertyName)
if ($property -eq $null)
{
Throw "Property $PropertyName not defined for $($Class.ClassName)"
}
if ($PropertyType -eq "GET")
{
__PSClass-InvokeScript $FoundClass $property.GetScript $instance $parms
}
else
{
__PSClass-InvokeScript $FoundClass $property.SetScript $instance $parms
}
}
# ===================================================================================
function Attach-PSNote
{
param ( [PSObject]$object=$(Throw "Object is required")
, [string]$name=$(Throw "Note Name is Required")
, $value
)
if (! $object.psobject.members[$name])
{
$member = new-object management.automation.PSNoteProperty `
$name,$value
$object.psobject.members.Add($member)
}
if($value)
{
$object.$name = $value
}
}
# ===================================================================================
function Attach-PSScriptMethod
{
param ( [PSObject]$object=$(Throw "Object is required")
, [string]$name=$(Throw "Method Name is Required")
, [scriptblock] $script
, [switch] $override
)
$member = new-object management.automation.PSScriptMethod `
$name,$script
if ($object.psobject.members[$name] -ne $null)
{
if ($override)
{
$object.psobject.members.Remove($name)
}
else
{
Throw "Method '$name' already exists with out 'override'"
}
}
$object.psobject.members.Add($member)
}
# ===================================================================================
function Attach-PSProperty
{
param ( [PSObject]$object=$(Throw "Object is required")
, [string]$name=$(Throw "Method Name is Required")
, [scriptblock] $get=$(Throw "get script is required on property $name in Class $ClassName")
, [scriptblock] $set
, [switch] $override
)
if ($set)
{
$scriptProperty = new-object management.automation.PsScriptProperty `
$name,$get,$set
}
else
{
$scriptProperty = new-object management.automation.PsScriptProperty `
$name,$get
}
if ( $object.psobject.properties[$name] -and $override)
{
$object.psobject.properties.Remove($name)
}
$object.psobject.properties.add($scriptProperty)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment