PowerShell Classes and Enums
Run a command with me.
Get-Help 'about_Classes'
This could have been the whole blog post. There is so much useful information in Microsoft’s documentation, directly integrated with PowerShell’s help system.
Classes were added in PowerShell 5 and the feature lets people familiar with traditional object-oriented programming get to know PowerShell from a different angle. Desired State Configuration makes use of classes, and there are many areas where writing a PowerShell class could be a solution. But when is it the right one?
I’ve seen classes used in different ways, both where they solved a problem in a very elegant and structured way, but also where they could have been replaced by a simple, more clearly defined PowerShell function. It can sometimes be hard to know when to look at classes as an option.
PowerShell Classes
I believe that the best way to learn code is to write code, so in this post we will together look at the different parts of a class, how to build in PowerShell and how to utilize the features that come with it. If you use Visual Studio Code with the PowerShell extension, there’s a snipped called ex-class
to create a quick example class to base your own class from.
class MyClass {
# Property: Holds name
[String] $Name
# Constructor: Creates a new MyClass object, with the specified name
MyClass([String] $NewName) {
# Set name for MyClass
$this.Name = $NewName
}
# Method: Method that changes $Name to the default name
[void] ChangeNameToDefault() {
$this.Name = "DefaultName"
}
}
A class is defined with a the keyword class
followed by the name of the class and curly brackets. Everything belonging to the class is defined within the scope of those brackets, such as properties and methods. Above is a basic example which has a name property and lets you change it using a method.
Methods
Methods, you say? I’ve seen those when running Get-Member
but compared to properties I’ve never actually created one. How do they work?
You can think of a method as a function belonging to an object. A member function, so to say. Let’s look at the objects returned from Get-ChildItem
when run in a file system as an example.
PipeHow:\Blog> $Files = Get-ChildItem 'C:\Temp' -File
PipeHow:\Blog> $Files
Directory: C:\Temp
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2020-05-27 11:51 9 test.txt
-a--- 2020-05-27 11:51 6 test1.txt
PipeHow:\Blog> $Files | Get-Member -MemberType Method
TypeName: System.IO.FileInfo
Name MemberType Definition
---- ---------- ----------
AppendText Method System.IO.StreamWriter AppendText()
CopyTo Method System.IO.FileInfo CopyTo(string destFileName), System.IO.FileInfo CopyTo(string destFileName, bool overwrite)
Create Method System.IO.FileStream Create()
CreateText Method System.IO.StreamWriter CreateText()
Decrypt Method void Decrypt()
Delete Method void Delete()
Encrypt Method void Encrypt()
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetLifetimeService Method System.Object GetLifetimeService()
GetObjectData Method void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context), void ISerializable.GetObj…
GetType Method type GetType()
InitializeLifetimeService Method System.Object InitializeLifetimeService()
MoveTo Method void MoveTo(string destFileName), void MoveTo(string destFileName, bool overwrite)
Open Method System.IO.FileStream Open(System.IO.FileMode mode), System.IO.FileStream Open(System.IO.FileMode mode, System.IO.FileAccess access), System.IO.FileStream…
OpenRead Method System.IO.FileStream OpenRead()
OpenText Method System.IO.StreamReader OpenText()
OpenWrite Method System.IO.FileStream OpenWrite()
Refresh Method void Refresh()
Replace Method System.IO.FileInfo Replace(string destinationFileName, string destinationBackupFileName), System.IO.FileInfo Replace(string destinationFileName, string d…
ToString Method string ToString()
Using Get-Member
we can find many methods on the FileInfo
objects, this makes sense since they represent files and there are many operations we can do with them. The methods are often ways to modify the specific object, in this case we could among other things copy the file, move it or delete it using its member methods.
A method has an orderly structure that makes it quick to see what data it returns, if any, and what parameters it takes. Looking at the method CopyTo
from above we can see that it has two variants, so-called overloads. Overloads are used when the same method can be run with different parameters, similar to parameter sets for PowerShell functions.
Calling members of an object is done with a dot, but something that sets properties and methods apart is that methods are called by using parentheses after the method, where the parameters are provided. Even if no input is needed for the method, such as Delete
in the list above, the parentheses are still part of the syntax to call the method.
Trying to call a method without parentheses will therefore give you a different result than you might expect. Not to say it’s not useful though, because that syntax is to list the overloads of the specified method. Let’s look at an example for the first file we found in our folder.
PipeHow:\Blog> $Files[0].CopyTo
OverloadDefinitions
-------------------
System.IO.FileInfo CopyTo(string destFileName)
System.IO.FileInfo CopyTo(string destFileName, bool overwrite)
There are two overloads of CopyTo
that take different sets of parameters. Presumably one lets us copy the file to a destination and the other one lets us also overwrite any existing file there with the same name.
PipeHow:\Blog> $Files[0].CopyTo('C:\Temp\test2.txt')
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2020-05-27 11:51 9 test2.txt
We now have three files in our folder, but what if we try to copy the same file again now that it also exists with the new name?
PipeHow:\Blog> $Files[0].CopyTo('C:\Temp\test2.txt')
MethodInvocationException: Exception calling “CopyTo” with “1” argument(s): “The file ‘C:\Temp\test2.txt’ already exists.”
That was expected. So let’s try the other overload and enable overwrite.
PipeHow:\Blog> $Files[0].CopyTo('C:\Temp\test2.txt', $true)
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2020-05-27 11:51 9 test2.txt
That was a simple example of how overloads can differ in behavior based on the parameters provided. You will find many other methods and overloads if you dive into the objects you work with daily.
Going back to the overload definitions, and even the syntax of a method, we can also see the return type of the method. Methods compared to functions can only output one object or collection of objects. It can also only output once, as the final thing of the method’s code path, and the type of the output needs to be specified when writing the code. This provides you with clear information from just a glance if you’re interested in knowing what a method provides you.
If we take a look back at the method we used above we will see that we already have the information of what it gives us back, the return type is specified in the very start of the method.
System.IO.FileInfo CopyTo(string destFileName, bool overwrite)
System.IO.FileInfo
is the return type, meaning we get the information about the file copy we just created in case we want to further do something with it.
The method Delete
on the other hand has the return type [void]
which meaning it does not return anything.
PipeHow:\Blog> $Files | Get-Member Delete
TypeName: System.IO.FileInfo
Name MemberType Definition
---- ---------- ----------
Delete Method void Delete()
Confirming this is as simple as using the function on our file.
PipeHow:\Blog> $Files[0].Delete()
No output, no error, and when looking at our directory the file is gone.
Static Members
Members of a class can also be static. Compared to above where we needed a file object to copy from, static members lets you access them even without an instance of an object. A commonly used static method is the IsNullOrEmpty
from the [string]
class, used to validate whether or not a string is null or empty, though I’ll admit I’m more of a IsNullOrWhiteSpace
kind of guy.
These methods don’t need you to already have a string to work with, you call them directly through the string class using the double colon operator ::
, the static member accessor.
PipeHow:\Blog> [string]::IsNullOrWhiteSpace(" ")
True
We can see all static members of a class by using Get-Member
on the class and specifying the parameter -Static
.
PipeHow:\Blog> [string] | Get-Member -Static
TypeName: System.String
Name MemberType Definition
---- ---------- ----------
Compare Method static int Compare(string strA, string strB, bool ignoreCase), static int Compare(string strA, string strB, System.StringComparison comparisonType), static int …
CompareOrdinal Method static int CompareOrdinal(string strA, string strB), static int CompareOrdinal(string strA, int indexA, string strB, int indexB, int length)
Concat Method static string Concat(System.Object arg0), static string Concat(System.Object arg0, System.Object arg1), static string Concat(System.Object arg0, System.Object a…
Copy Method static string Copy(string str)
Create Method static string Create[TState](int length, TState state, System.Buffers.SpanAction[char,TState] action)
Equals Method static bool Equals(string a, string b), static bool Equals(string a, string b, System.StringComparison comparisonType), static bool Equals(System.Object objA, S…
Format Method static string Format(string format, System.Object arg0), static string Format(string format, System.Object arg0, System.Object arg1), static string Format(strin…
GetHashCode Method static int GetHashCode(System.ReadOnlySpan[char] value), static int GetHashCode(System.ReadOnlySpan[char] value, System.StringComparison comparisonType)
Intern Method static string Intern(string str)
IsInterned Method static string IsInterned(string str)
IsNullOrEmpty Method static bool IsNullOrEmpty(string value)
IsNullOrWhiteSpace Method static bool IsNullOrWhiteSpace(string value)
Join Method static string Join(char separator, Params string[] value), static string Join(char separator, Params System.Object[] values), static string Join[T](char separat…
new Method string new(char[] value), string new(char[] value, int startIndex, int length), string new(System.Char*, System.Private.CoreLib, Version=4.0.0.0, Culture=neutra…
ReferenceEquals Method static bool ReferenceEquals(System.Object objA, System.Object objB)
Empty Property static string Empty {get;}
In general you will see more static methods than properties among the members of a class, but properties can be static too as shown by the Empty
property.
PipeHow:\Blog> [string]::Empty -eq ''
True
As we can see, this is simply another way to write ''
or ""
. Note that properties are not referred to with parentheses, the way methods are.
An Example Class - Money
As an exercise, let’s build a class to represent money. As a simple start we can add a property to store the amount of money that our object has.
class Money {
[int] $Amount
}
Between each example in this section, assume that I run the code defining the class again to re-define the class in my PowerShell session, assume that I create a new [Money]
object using the line below.
PipeHow:\Blog> $Money = New-Object Money
Creating a class isn’t harder than that, but what is hard is to see how a simple representation of an of money amount is going to be very useful.
PipeHow:\Blog> $Money
Amount
------
0
PipeHow:\Blog> $Money.Amount = 20
PipeHow:\Blog> $Money
Amount
------
20
Let’s add to our class. We want to be able to specify currency, and add a so-called constructor. You can think of the constructor as a method that is run when the object is created. The constructor is always, and needs to be, named the same as the class. Just like methods it can have different overloads and is in fact what is called in the background when using the cmdlet New-Object
, as well as the new
method you may see once in a while using the .NET syntax of object instantiation. We could for example use it like [datetime]::new()
.
This syntax also lets us quickly look at what different constructors a class has by not adding the parentheses.
PipeHow:\Blog> [datetime]::new
OverloadDefinitions
-------------------
datetime new(long ticks)
datetime new(long ticks, System.DateTimeKind kind)
datetime new(int year, int month, int day)
datetime new(int year, int month, int day, System.Globalization.Calendar calendar)
datetime new(int year, int month, int day, int hour, int minute, int second)
datetime new(int year, int month, int day, int hour, int minute, int second, System.DateTimeKind kind)
datetime new(int year, int month, int day, int hour, int minute, int second, System.Globalization.Calendar calendar)
datetime new(int year, int month, int day, int hour, int minute, int second, int millisecond)
datetime new(int year, int month, int day, int hour, int minute, int second, int millisecond, System.DateTimeKind kind)
datetime new(int year, int month, int day, int hour, int minute, int second, int millisecond, System.Globalization.Calendar calendar)
datetime new(int year, int month, int day, int hour, int minute, int second, int millisecond, System.Globalization.Calendar calendar, System.DateTimeKind kind)
Without getting too side-tracked, let’s add the currency property and constructor to our class.
class Money {
[int] $Amount
[string] $Currency
# We can have several constructors, as well as empty ones for default values
Money() {
$this.Currency = 'SEK'
}
# We specify parameters comma separated as "[type] name"
# Not specifying type will make it "System.Object"
Money([int] $Amount, [string] $Currency) {
# Set the $Amount property to the value of the $Amount parameter
$this.Amount = $Amount
$this.Currency = $Currency
}
}
Looking at the code we can see something that is used only in PowerShell classes. The variable $this
refers to the current instance of the class.
If we take a look at our class with Get-Member
we can see that our class looks just like any other .NET object, and will also have a few methods inherited from System.Object
by default, such as ToString
and GetType
. We can also see our constructors by looking at the new
method.
PipeHow:\Blog> $Money | Get-Member
TypeName: Money
Name MemberType Definition
---- ---------- ----------
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
Amount Property int Amount {get;set;}
Currency Property string Currency {get;set;}
PipeHow:\Blog> [Money]::new
OverloadDefinitions
-------------------
Money new()
Money new(int Amount, string Currency)
For our third and final iteration of the class, let’s go deeper (arguably too deep for an example) and add live data for exchange rates from an API, and functionality to validate the currency string using an enum.
Enums
Enum? Isn’t that a huge bird? No, that’s the emu. An enum is an enumeration, a collection of labels representing pre-defined values, often used for validation for data where you know that it can only contain a value amongst a known set. Think of it as similar to ValidateSet
used for function parameters.
In PowerShell you may have encountered the System.DayOfWeek
enum when looking at datetime
objects.
PipeHow:\Blog> Get-Date | Get-Member DayOfWeek
TypeName: System.DateTime
Name MemberType Definition
---- ---------- ----------
DayOfWeek Property System.DayOfWeek DayOfWeek {get;}
PipeHow:\Blog> [System.DayOfWeek]::Wednesday.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True DayOfWeek System.Enum
PipeHow:\Blog> [int][System.DayOfWeek]::Wednesday
3
An enum is behind the scenes simply a set of defined numbers, and an instance of an enum is one of the numbers or sometimes a combination of them. This means we can also cast the enum to numbers if needed. The numbers start at 0 by default but the labels can be set manually. If only one label is set to a number, the following numbers will simply increment the number by 1.
In PowerShell you can define an enum using the enum
keyword and separating each value by line.
Bit Flags
There is also a way to create a structure known as bit flags in PowerShell, by using the [Flags()]
attribute on the enum. This lets an instance of the enum have several values, and be represented by a sum of the numbers that the values represent. For it to work properly, each label must be set to a power of two value.
[Flags()]
enum Weather {
Sun = 1
Rain = 2
Wind = 4
Hail = 8
Snow = 16
}
We could then say that the weather is a combination, by using a sum of its numbers.
PipeHow:\Blog> [Weather]7
Sun, Rain, Wind
A Currency Enum
To finish our Money
class we want to represent the currencies using an enum, and change the type of the property $Currency
to the new enum [Currency]
below.
enum Currency {
SEK
EUR
USD
}
Simple! But what if we want the enum to be dynamic and read an external source of currencies? It’s not possible to create a PowerShell enum without explicitly naming each label, but we can generate a string of the code to create the enum, and run it using Invoke-Expression
.
Let’s first get all the different currencies using the public Exchange rates API published by the European Central Bank.
PipeHow:\Blog> $RatesData = (Invoke-RestMethod 'https://api.exchangeratesapi.io/latest').rates
PipeHow:\Blog> $RatesData
CAD : 0,1438382767
HKD : 0,8059452649
ISK : 14,5979178311
PHP : 5,2440722602
DKK : 0,7062891354
HUF : 33,1176643331
CZK : 2,5646296524
GBP : 0,0841942726
...
Next we will gather the names of each property, and store them in a list that we can use for our enum string.
PipeHow:\Blog> $Rates = ($RateData | Get-Member -MemberType NoteProperty).Name
Finally we will create our string and generate each label of the enum as part of the string, as a one-liner but with adding new-lines in the loop.
# Inside the currency brackets we loop through each rate and add new lines
$CurrencyEnum = "enum Currency{$(foreach ($Rate in $Rates){"`n$Rate`"})`n}"
Invoke-Expression $CurrencyEnum
Creating an enum in PowerShell is similar to C#, and you may see other solutions online using Add-Type
with C# code instead of Invoke-Expression
. Remember to not use these cmdlets on any strings you don’t have full control over, or you may end up running malicious code that someone has injected.
PipeHow:\Blog> [enum]::GetValues([Currency])
AUD
BGN
BRL
CAD
CHF
CNY
CZK
DKK
...
All of the different currencies are now in our enum, so let’s put together our final version of the class.
The Final Money Class
To finalize our class we add live data for exchange rates from the same API, and validation for our currency using our new enum.
class Money {
[int] $Amount
# Change to Currency type instead of string to force input to be a valid option
[Currency] $Currency
# Static hashtable to store rates of chosen currency
static [hashtable] $ExchangeRates
Money() {
# Call hidden initialize method with default values
$this.Init(0,'SEK')
}
Money([int] $Amount, [Currency] $Currency) {
$this.Init($Amount, $Currency)
}
# Create a hidden method to use from constructors
hidden Init([int] $Amount, [Currency] $Currency) {
$this.Amount = $Amount
$this.Currency = $Currency
# Create or reset the static ExchangeRates hashtable
[Money]::ExchangeRates = @{}
# Get live rates from API
$Rates = (Invoke-RestMethod "https://api.exchangeratesapi.io/latest?base=$Currency").rates
# Go through rates and add each value to the hashtable with name as key
foreach ($Rate in ($Rates | Get-Member -MemberType NoteProperty).Name) {
[Money]::ExchangeRates[$Rate] = $Rates.$Rate
}
}
# Add method to convert the current amount to a chosen currency
[decimal] GetAmountAsCurrency([Currency] $Currency) {
return $this.Amount * [Money]::ExchangeRates[$Currency.ToString()]
}
}
A few things have been added and changed as you can see. Firstly we changed the $Currency
property to be of the type [Currency]
, our dynamic enum that we generated. This makes sure that it will always be of a valid currency whenever we pass values to the different methods.
PowerShell does not have support for so-called constructor chaining, a way to call a constructor from another constructor. This would have let us provide default values in the no-parameter constructor to our overloaded one. Instead I opted to create a hidden method called Init
that takes the same parameters and sets them, to have a consistent initializing of our class instance.
There is a new static hashtable called ExchangeRates
which holds all the current exchange rates, but since it is static it is not bound to a specific object. Instead there will only ever be one hashtable at a time, which comes with its own challenges, but we could expand on this implementation to clear it only when needed and minimize the amount of API calls between new [Money]
objects.
Finally we use the static hashtable in our new method GetAmountAsCurrency
where we return the current amount converted to any specified currency with live rates from the API, validated using our enum.
There are endless ways to use classes and enums, and many more neat things you can do with them than we went through today. I hope you found your interest sparked to learn more!