Can I write a class using PowerShell?

With PowerShell being built on top of the .NET framework, can I write my own custom class using PowerShell?

I'm not talking about instantiating .NET classes... that part is plain enough. I want to write my own custom classes using PowerShell scripts. Is it possible? So far my research leads me to say that this isn't possible.

I'm still learning PowerShell, and so far I haven't found an answer on this website, despite a few searches.


Take a look at the Add-Type cmdlet. It lets you write C# and other code in PowerShell. For example (from the above link), in a PowerShell window,

$source = @"
public class BasicTest
{
    public static int Add(int a, int b)
    {
        return (a + b);
    }

    public int Multiply(int a, int b)
    {
        return (a * b);
    }
}
"@

Add-Type -TypeDefinition $source

[BasicTest]::Add(4, 3)

$basicTestObject = New-Object BasicTest
$basicTestObject.Multiply(5, 2)

I suspect that the solution that you are looking for is PowerShell Modules. They perform the roles that classes typically perform in other languages. They give you a very simple, yet structured, way to reuse your code.

Here is how to get the functionality of classes in PowerShell using modules. At the command line you could do this:

New-Module -ScriptBlock {function add($a,$b){return $a + $b}; function multiply($a,$b){return $a * $b}; function supersecret($a,$b){return multiply $a $b}; export-modulemember -function add, supersecret}

Then you would be able to:

PS C:\> add 2 4
6
PS C:\> multiply 2 4
The term 'multiply' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:1 char:9
+ multiply <<<<  2 4
    + CategoryInfo          : ObjectNotFound: (multiply:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

PS C:\> supersecret 2 4
8

As you can see multiply is private within the module. More traditionally you would instantiate an object that is an instance of the module. That is done via the -AsCustomObject parameter:

$m = New-Module -ScriptBlock {function add($a,$b){return $a + $b}; function multiply($a,$b){return $a * $b}; function supersecret($a,$b){return multiply $a $b}; export-modulemember -function add, supersecret} -AsCustomObject

Then you could:

PS C:\> $m.add(2,4)
6
PS C:\> $m.multiply(2,4)
Method invocation failed because [System.Management.Automation.PSCustomObject] doesn't contain a method named 'multiply'.
At line:1 char:12
+ $m.multiply <<<< (2,4)
    + CategoryInfo          : InvalidOperation: (multiply:String) [], RuntimeException
    + FullyQualifiedErrorId : MethodNotFound

PS C:\> $m.supersecret(2,4)

8

This all demonstrates the use of dynamic modules meaning nothing is stored to disk for reuse. It is fine for very simple functionality. If you want to actually be able to read the code and reuse it in future sessions or scripts, however, you would want to store it in a .psm1 file and then store that file in a folder with the same name (minus the extension) as the file. Then you can import the module into your session at the command line or into another script.

As an example of this, let's say I took this code:

function Add{
    param(
            $a,
            $b
         )
    return $a + $b
}

function Multiply{
    param(
            $a,
            $b
         )
    return $a + $b
}

function SuperSecret{
    param(
            $a,
            $b
         )
    return Multiply $a $b
}
Export-ModuleMember -Function Add, SuperSecret

And saved it to a file called TestModule.psm1 in the folder: C:\Windows\System32\WindowsPowerShell\v1.0\Modules\TestModule

The Modules folder in the PowerShell install folder is a magic folder and any modules stored there are visible to the Import-Module cmdlet without having to specify a path. Now if we run Get-Module -List at the command line we see:

ModuleType Name                      ExportedCommands
---------- ----                      ----------------
Script     DotNet                    {}
Manifest   FileSystem                {Get-FreeDiskSpace, New-Zip, Resolve-ShortcutFile, Mount-SpecialFolder...}
Manifest   IsePack                   {Push-CurrentFileLocation, Select-CurrentTextAsVariable, ConvertTo-Short...
Manifest   PowerShellPack            {New-ByteAnimationUsingKeyFrames, New-TiffBitmapEncoder, New-Viewbox, Ne...
Manifest   PSCodeGen                 {New-Enum, New-ScriptCmdlet, New-PInvoke}
Manifest   PSImageTools              {Add-CropFilter, Add-RotateFlipFilter, Add-OverlayFilter, Set-ImageFilte...
Manifest   PSRss                     {Read-Article, New-Feed, Remove-Article, Remove-Feed...}
Manifest   PSSystemTools             {Test-32Bit, Get-USB, Get-OSVersion, Get-MultiTouchMaximum...}
Manifest   PSUserTools               {Start-ProcessAsAdministrator, Get-CurrentUser, Test-IsAdministrator, Ge...
Manifest   TaskScheduler             {Remove-Task, Get-ScheduledTask, Stop-Task, Add-TaskTrigger...}
Manifest   WPK                       {Get-DependencyProperty, New-ModelVisual3D, New-DiscreteVector3DKeyFrame...
Manifest   AppLocker                 {}
Manifest   BitsTransfer              {}
Manifest   PSDiagnostics             {}
Script     **TestModule**            {}
Manifest   TroubleshootingPack       {}
Manifest   Citrix.XenApp.Commands... {}

We can see that our module is ready to import. We can import it into the session and use it in the raw using:

Import-Module TestModule

Or once again we can instantiate an object:

$m = Import-Module TestModule -AsCustomObject

You can use the class keyword that was introduced in PowerShell 5.0

Here is an example by Trevor Sullivan. (Archived here.)

##################################################
####### WMF 5.0 November 2014 Preview ###########
##################################################
class Beer {
    # Property: Holds the current size of the beer.
    [Uint32] $Size;
    # Property: Holds the name of the beer's owner.
    [String] $Name;

    # Constructor: Creates a new Beer object, with the specified
    #              size and name / owner.
    Beer([UInt32] $NewSize, [String] $NewName) {
        # Set the Beer size
        $this.Size = $NewSize;
        # Set the Beer name
        $this.Name = $NewName;
    }

    # Method: Drink the specified amount of beer.
    # Parameter: $Amount = The amount of beer to drink, as an 
    #            unsigned 32-bit integer.
    [void] Drink([UInt32] $Amount) {
        try {
            $this.Size = $this.Size - $Amount;
        }
        catch {
            Write-Warning -Message 'You tried to drink more beer than was available!';
        }
    }

    # Method: BreakGlass resets the beer size to 0.
    [void] BreakGlass() {
        Write-Warning -Message 'The beer glass has been broken. Resetting size to 0.';
        $this.Size = 0;
    }
}

This works on Windows 10 Pro.

Test drive it like this:

# Create a new 33 centilitre beer, named 'Chimay'
$chimay = [Beer]::new(33, 'Chimay');

$chimay.Drink(10)
$chimay.Drink(10)

# Need more beer!
$chimay.Drink(200)

$chimay.BreakGlass()