Private/SharedFunctions.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# Private functions shared by Configuration and Logging scripts.

. $PSScriptRoot\ModuleState.ps1

<#
.SYNOPSIS
Ensures at most one of the switch values passed as an argument is set.
 
.DESCRIPTION
Ensures at most one of the switch values passed as an argument is set.
 
.NOTES
This function is NOT intended to be exported from this module.
#>

function Private_ValidateSwitchParameterGroup (
    [parameter(Mandatory=$True)]
    [ValidateNotNull()]
    [switch[]]$SwitchList,
    
    [parameter(Mandatory=$True)]
    [ValidateNotNull()]
    [ValidateNotNullOrEmpty()]
    [string]$ErrorMessage
    )
{
    # Can't use "if ($SwitchList.Count -gt 1)..." because it will always be true, even if no
    # switches are set when calling the parent function. If one of the switch parameters is not
    # set it will still be passed to this function but with value $False.
    # Could use ".Where{$_}" but ".Where{$_ -eq $True}" is easier to understand.
    if ($SwitchList.Where{$_ -eq $True}.Count -gt 1)
    {
        throw [ArgumentException] $ErrorMessage
    }
}

<#
.SYNOPSIS
Function called by ValidateScript to check if the specified host color name is valid when
passed as a parameter.
 
.DESCRIPTION
If the specified color name is valid this function returns $True. If the specified color name
is not valid the function throws an exception rather than returning $False.
 
.NOTES
Allows multiple parameters to be validated in a single place, so the validation code does not
have to be repeated for each parameter.
 
Throwing an exception when the color name is invalid allows us to specify a custom error message.
If the function simply returned $False PowerShell would generate a standard error message that
does not indicate why the validation failed.
#>

function Private_ValidateHostColor (
    [Parameter(Mandatory=$True)]
    [string]$ColorToTest
    )
{    
    $validColors = @('Black', 'DarkBlue', 'DarkGreen', 'DarkCyan', 
        'DarkRed', 'DarkMagenta', 'DarkYellow', 'Gray', 'DarkGray', 'Blue', 'Green', 'Cyan', 
        'Red', 'Magenta', 'Yellow', 'White')
    
    if ($validColors -notcontains $ColorToTest)
    {
        throw [ArgumentException] "INVALID TEXT COLOR ERROR: '$ColorToTest' is not a valid text color for the PowerShell host."
    }
            
    return $True
}

<#
.SYNOPSIS
Function called by ValidateScript to check if the specified log level is valid when passed as a
parameter.
 
.DESCRIPTION
If the specified log level is valid this function returns $True. If the specified log level is
not valid the function throws an exception rather than returning $False.
 
.NOTES
Allows multiple parameters to be validated in a single place, so the validation code does not
have to be repeated for each parameter.
 
Throwing an exception when the log level is invalid allows us to specify a custom error message.
If the function simply returned $False PowerShell would generate a standard error message that
does not indicate why the validation failed.
#>

function Private_ValidateLogLevel (
    [Parameter(Mandatory=$True)]
    [string]$LevelToTest, 

    [parameter(Mandatory=$False)]
    [switch]$ExcludeOffLevel
    )
{    
    $validLevels = @('OFF', 'ERROR', 'WARNING', 'INFORMATION', 'DEBUG', 'VERBOSE')
    if ($ExcludeOffLevel.IsPresent)
    {
        $validLevels[0] = $Null
    }
    
    if ($validLevels -notcontains $LevelToTest)
    {
        throw [ArgumentException] "INVALID LOG LEVEL ERROR: '$LevelToTest' is not a valid log level."
    }
            
    return $True
}

<#
.SYNOPSIS
Gets a Timestamp format string from a specified message format string.
 
.DESCRIPTION
Parses a message format string to find a {Timestamp} field placeholder. If the Timestamp
plceholder is found and it contains a datetime format string the datetime format string will be
returned. If there is no Timestamp plceholder, or if the Timestamp placeholder has no datetime
format string, then $Null will be returned.
 
.NOTES
This function is NOT intended to be exported from this module.
#>

function Private_GetTimestampFormat ([string]$MessageFormat)
{
    # The regex can handle zero or more white spaces (spaces or tabs) between the curly braces
    # and the placeholder name. eg "{ Timestamp}", '{ Timestamp }". It can also handle
    # zero or more white spaces before or after the colon that separates the placeholder name
    # from the datetime format string. Note that (?: ... ) is a non-capturing group so $Matches
    # should contain at most two groups:
    # $Matches[0] : The overall match. Always present if the {Timestamp} placeholder is
    # present;
    # $Matches[1] : The datetime format string. Only present if the {Timestamp}
    # placeholder has a datetime format string.
    # Note the first colon in the regex pattern is part of the non-capturing group specifier.
    # The second colon in the regex pattern represents the separator between the placeholder name
    # and the datetime format string, eg {Timestamp:d}
    $regexPattern = "{\s*Timestamp\s*(?::\s*(.+?)\s*)?\s*}"
    
    # -imatch is a case insensitive regex match.
    # No need to compile the regex as it won't be used often.
    $isMatch = $MessageFormat -imatch $regexPattern
    if ($isMatch -and $Matches.Count -ge 2)
    {
        return $Matches[1].Trim()
    }

    return $Null
}

<#
.SYNOPSIS
Gets message format info from a message format string.
 
.DESCRIPTION
Parses a message format string to determine which fields will appear in the log message and what
their format strings are, if applicable. The results are returned in a hash table.
 
.OUTPUTS
A hash table with the following keys:
    RawFormat: The format string passed into this function;
 
    WorkingFormat: A modified format string with the field placeholders replaced with variable
        names. The variable names that may be embedded in the WorkingFormat string are:
 
        $Message : Replaces field placeholder {Message};
                             
        $Timestamp : Unlike other fields, Timestamp must include a datetime format string.
                            If no datetime format string is included in the Timestamp placeholder
                            it will default to 'yyyy-MM-dd hh:mm:ss.fff'.
                             
                            The Timestamp placeholder will be replaced with
                            "$($Timestamp.ToString('<datetime format string>'))".
                             
                            Examples:
                                1) Field placeholder '{Timestamp:d}' will be replaced by
                                    "$($Timestamp.ToString('d'))";
 
                                2) Field placeholder '{Timestamp}' will use the default datetime
                                    format string so will be replaced by
                                    "$($Timestamp.ToString('yyyy-MM-dd hh:mm:ss.fff'))";
 
        $CallerName : Replaces field placeholder {CallerName};
 
        $MessageLevel : Replaces field placeholder {MessageLevel};
 
        $Category : Replaces field placeholder {Category};
         
    FieldsPresent: An array of strings representing the names of fields that will appear in the
        log message. Field names that may appear in the array are:
 
        "Message" : Included if the RawFormat string contains field placeholder {Message};
                             
        "Timestamp" : Included if the RawFormat string contains field placeholder
                            {Timestamp};
 
        "CallerName" : Included if the RawFormat string contains field placeholder
                            {CallerName};
 
        "MessageLevel" : Included if the RawFormat string contains field placeholder
                            {MessageLevel};
 
        "Category" : Included if the RawFormat string contains field placeholder
                            {Category}.
 
.NOTES
This function is NOT intended to be exported from this module.
#>

function Private_GetMessageFormatInfo([string]$MessageFormat)
{
    $messageFormatInfo = @{
                            RawFormat = $MessageFormat
                            WorkingFormat = ""
                            FieldsPresent = @()
                        }

    $workingFormat = $MessageFormat

    # -ireplace is a case insensitive find and replace.
    # The regex can handle zero or more white spaces (spaces or tabs) between the curly braces
    # and the placeholder name. eg "{ Messages}", '{ Messages }".
    # No need to compile the regex as it won't be used often.
    $modifiedText = $workingFormat -ireplace '{\s*Message\s*}', '${Message}'
    if ($modifiedText -ne $workingFormat)
    {
        $messageFormatInfo.FieldsPresent += "Message"
        $workingFormat = $modifiedText
    }

    $timestampFormat = Private_GetTimestampFormat $workingFormat
    if (-not $timestampFormat)
    {
        $timestampFormat = $script:_defaultTimestampFormat
    }
    # Escape the first two "$" because we want to retain them in the replacement text. Do
    # not escape the "$" in "$timestampFormat" because we want to expand that variable.
    $replacementText = "`$(`$Timestamp.ToString('$timestampFormat'))"

    $modifiedText = $workingFormat -ireplace '{\s*Timestamp\s*(?::\s*.+?\s*)?\s*}', $replacementText
    if ($modifiedText -ne $workingFormat)
    {
        $messageFormatInfo.FieldsPresent += "Timestamp"
        $workingFormat = $modifiedText
    }

    $modifiedText = $workingFormat -ireplace '{\s*CallerName\s*}', '${CallerName}'
    if ($modifiedText -ne $workingFormat)
    {
        $messageFormatInfo.FieldsPresent += "CallerName"
        $workingFormat = $modifiedText
    }

    $modifiedText = $workingFormat -ireplace '{\s*MessageLevel\s*}', '${MessageLevel}'
    if ($modifiedText -ne $workingFormat)
    {
        $messageFormatInfo.FieldsPresent += "MessageLevel"
        $workingFormat = $modifiedText
    }

    $modifiedText = $workingFormat -ireplace '{\s*Category\s*}', '${Category}'
    if ($modifiedText -ne $workingFormat)
    {
        $messageFormatInfo.FieldsPresent += "Category"
        $workingFormat = $modifiedText
    }

     $messageFormatInfo.WorkingFormat = $workingFormat

     return $messageFormatInfo
}