Tuesday, December 29, 2020

PowerShell: Enumerations

PowerShell 5.1 introduced classes (which were partially reviewed in post PowerShell: Classes Static Constructors, Static Methods, and Static Properties) and enumerations. Microsoft's documentation on enumeration describes them as (see About Enum):


An example of a too commonly defined (regardless of language) enumeration is as follows where the enumeration values are Error, Warn, and Info:


When displayed, by default, an enumeration value is displayed as text but each enumeration value is associated with a numeric value (Error = 0, Warn = 1, and Info = 2). The following script snippet demonstrates this:


The output from the above snippet is as follows:


An example of an enumeration (LogLevel) used in a script is the following code which implements the SimpleLogEntry class:


The LogLevel enumeration is a property of the class declared and line 18 and is accessible as the return value of the GetLogLevel method declared at line 32.

Enumerations can be compared using standard numeric operators such as -eq, -ne, -le, -lt, -ge, and -gt. An example of comparing two enumerations is line 98 below which limits the logs displayed to being Error only or Error/Warning or Error/Warn/Info (a.k.a. the current log level):


The enumerations numeric values can also be used as array index. Consider the following enumeration also used the SimpleLogEntry class:


The code which converts a log entry to a line of text is as follows where each property of the SimpleLogEntry class is separated by a delimiter (a tab character) when converted to a string using the ToString method:


The LogItemIndex enumeration is used in a constructor to access elements within an array at lines 28, 29, and 31 allowing an instance of SimpleLogEntry to be created from its string representation:


Notice in the code snippet above that a text value was converted to an instance of LogLevel at lines 25-26 using PowerShell's explicit cast operator:
    $this.logLevel = [LogLevel]$items[[LogItemIndex]::LogLevel]

The test function for the SimpleLogEntry class is implemented as follows:
  1. Create an instance of SimpleLogEntry using the constructor that takes a LogLevel enumeration and a message parameter (lines 77-78)
  2. Get the string representation of the SimpleLogEntry using the ToString method (line line 79)
  3. Creates a second instance of SimpleLogEntry using the constructor that takes a string (a line of text created from ToString) as a parameter (lines 80-81)
  4. Get the string representation of the second SimpleLogEntry instance from ToString (line 82)
  5. Compare the string representation of both SimpleLogEntry instances and if they are equal the test was successful (lines 84-86)
  6. If the two instance of SimpleLogEntry do not match, throw an exception (line 88)

Appendix A: Script in its Entirety

Set-StrictMode -Version 3.0

enum LogLevel {
    Error
    Warn
    Info
}

enum LogItemIndex {
    Date
    LogLevel
    Message
}

class SimpleLogEntry {
    hidden static [string] $delimiter

    hidden [DateTime]  $dateTime

    hidden [LogLevel]  $logLevel

    hidden [string]  $message

    static SimpleLogEntry() {
        [SimpleLogEntry]::delimiter = "`t"
    }

    SimpleLogEntry([LogLevel] $logLevel, [string] $message) {
        $this.logLevel = $logLevel
        $this.message = $message
        $this.dateTime = (Get-Date).ToUniversalTime()
    }

    SimpleLogEntry([string] $logLine) {
        [string[]] $items = 
          $logLine -split [SimpleLogEntry]::delimiter

        $this.logLevel = [LogLevel]$items[[LogItemIndex]::LogLevel]
        $this.message = $items[[LogItemIndex]::Message]
        $this.dateTime = 
          ([DateTime]$items[[LogItemIndex]::Date]).ToUniversalTime()
    }    

    [DateTime] GetDateTime() {
        return $this.dateTime
    }

    [string] GetDateTimeText() {
        return $this.dateTime.ToString('o')
    }

    [LogLevel] GetLogLevel() {
        return $this.logLevel
    }

    [string] GetMessage() {
        return $this.message            
    }

    [string] ToString() {
        return `
       "$($this.GetDateTimeText())$([SimpleLogEntry]::delimiter)" + 
            "$($this.logLevel)$([SimpleLogEntry]::delimiter)" + 
            "$($this.message)"
    }
}

function Test-SimpleLogEntry {
    param
    (
        [Parameter(Mandatory=$true)]
        [LogLevel] $logLevel,
        [Parameter(Mandatory=$true)]
        [string] $message
    )
    
    [SimpleLogEntry] $logEntry = 
           [SimpleLogEntry]::new($logLevel, $message)
    [string] $line = $logEntry.ToString()
    [SimpleLogEntry] $logEntryFromParse = 
                        [SimpleLogEntry]::new($line)
    [string] $lineFromParse = $logEntryFromParse.ToString()

    if ($lineFromParse -eq $line) {
        return $logEntry
    }

    throw "Log entries differ '$lineFromParse' -ne '$line'"

function Write-Log {
    param
    (
        [Parameter(Mandatory=$true)]
        [SimpleLogEntry] $logEntry
    )

    if ($logEntry.GetLogLevel() -gt $globalLogLevel) {
        return 
    }

    Write-Host $logEntry
}

[LogLevel] $globalLogLevel = [LogLevel]::Warn

try {
    [SimpleLogEntry] $logEntry = 
        Test-SimpleLogEntry `
            ([LogLevel]::Error) 'A message to log'

    Write-Log $logEntry
    $logEntry = Test-SimpleLogEntry `
                  ([LogLevel]::Warn) 'Any log message'
    Write-Log $logEntry
    $logEntry = Test-SimpleLogEntry `
                  ([LogLevel]::Info) 'Another log message'
    Write-Log $logEntry

    Write-Host 'Success'
}

catch {
    $errorRecord = $_    
    Write-Host $errorRecord.Exception
    Write-Host $errorRecord[0].InvocationInfo.PositionMessage
    Write-Host 'Error'
}

No comments :

Post a Comment