Utility.PS.psm1
#Requires -Version 5.1 #Requires -PSEdition Core,Desktop <# .SYNOPSIS Utility.PS .DESCRIPTION This module contains cmdlets that extend the basic features of PowerShell. .NOTES ModuleVersion: 2.0.1 GUID: e6c8b1d2-a261-4a57-80a7-ea8080132c86 Author: Jason Thompson CompanyName: Microsoft Corporation Copyright: (c) 2023 Jason Thompson. All rights reserved. .FUNCTIONALITY Compress-Data, ConvertFrom-Base64String, ConvertFrom-ClixmlString, ConvertFrom-HexString, ConvertFrom-HtmlString, ConvertFrom-QueryString, ConvertFrom-SecureStringAsPlainText, ConvertFrom-UrlString, ConvertTo-Base64String, ConvertTo-ClixmlString, ConvertTo-Dictionary, ConvertTo-HexString, ConvertTo-HtmlString, ConvertTo-MarkdownTable, ConvertTo-PsParameterString, ConvertTo-PsString, ConvertTo-QueryString, ConvertTo-UrlString, Expand-Data, Format-DataSize, Format-NumberWithMetricUnit, Format-PropertyValue, Get-ContentEncoding, Get-PropertyValue, Get-RelativePath, Get-StrictModeVersion, Get-X509Certificate, Get-X509CertificateCrlDistributionPoints, Invoke-CommandAsSystem, New-SecureStringKey, Remove-Diacritics, Remove-InvalidFileNameCharacters, Remove-SensitiveData, Select-PsBoundParameters, Skip-NullValue, Test-IpAddressInSubnet, Test-PsElevation, Use-Progress, Write-HostPrompt .LINK https://github.com/jasoth/Utility.PS #> #region NestedModules Script(s) #region Compress-Data.ps1 <# .SYNOPSIS Compress data using DEFLATE (RFC 1951) and optionally GZIP file format (RFC 1952). .EXAMPLE PS >Compress-Data 'A string for compression' Compress string using Deflate. .INPUTS System.String .LINK https://github.com/jasoth/Utility.PS #> function Compress-Data { [CmdletBinding()] [Alias('Deflate-Data')] [OutputType([byte[]])] param ( # Value to convert [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [object] $InputObjects, # Output gzip format [Parameter(Mandatory = $false)] [switch] $GZip, # Level of compression [Parameter(Mandatory = $false)] [System.IO.Compression.CompressionLevel] $CompressionLevel = ([System.IO.Compression.CompressionLevel]::Optimal), # Input encoding to use for text strings [Parameter (Mandatory = $false)] [ValidateSet('Ascii', 'UTF32', 'UTF7', 'UTF8', 'BigEndianUnicode', 'Unicode')] [string] $Encoding = 'Default', # Set gzip OS header byte to unknown [Parameter(Mandatory = $false)] [switch] $GZipUnknownOS ) begin { function Compress ([byte[]]$InputBytes, [bool]$GZip) { try { $streamInput = New-Object System.IO.MemoryStream -ArgumentList @($InputBytes, $false) try { $streamOutput = New-Object System.IO.MemoryStream try { if ($GZip) { $streamCompression = New-Object System.IO.Compression.GZipStream -ArgumentList $streamOutput, $CompressionLevel, $true } else { $streamCompression = New-Object System.IO.Compression.DeflateStream -ArgumentList $streamOutput, $CompressionLevel, $true } $streamInput.CopyTo($streamCompression) } finally { $streamCompression.Dispose() } if ($GZip) { [void] $streamOutput.Seek(8, [System.IO.SeekOrigin]::Begin) switch ($CompressionLevel) { 'Optimal' { $streamOutput.WriteByte(2) } 'Fastest' { $streamOutput.WriteByte(4) } Default { $streamOutput.WriteByte(0) } } if ($GZipUnknownOS) { $streamOutput.WriteByte(255) } elseif ($PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows) { $streamOutput.WriteByte(11) } elseif ($IsLinux) { $streamOutput.WriteByte(3) } elseif ($IsMacOS) { $streamOutput.WriteByte(7) } else { $streamOutput.WriteByte(255) } } [byte[]] $OutputBytes = $streamOutput.ToArray() } finally { $streamOutput.Dispose() } } finally { $streamInput.Dispose() } Write-Output $OutputBytes -NoEnumerate } ## Create list to capture byte stream from piped input. [System.Collections.Generic.List[byte]] $listBytes = New-Object System.Collections.Generic.List[byte] } process { if ($InputObjects -is [byte[]]) { Write-Output (Compress $InputObjects -GZip:$GZip) -NoEnumerate } else { foreach ($InputObject in $InputObjects) { [byte[]] $InputBytes = $null if ($InputObject -is [byte]) { ## Populate list with byte stream from piped input. if ($listBytes.Count -eq 0) { Write-Verbose 'Creating byte array from byte stream.' Write-Warning ('For better performance when piping a single byte array, use "Write-Output $byteArray -NoEnumerate | {0}".' -f $MyInvocation.MyCommand) } $listBytes.Add($InputObject) } elseif ($InputObject -is [byte[]]) { $InputBytes = $InputObject } elseif ($InputObject -is [string]) { $InputBytes = [Text.Encoding]::$Encoding.GetBytes($InputObject) } elseif ($InputObject -is [bool] -or $InputObject -is [char] -or $InputObject -is [single] -or $InputObject -is [double] -or $InputObject -is [int16] -or $InputObject -is [int32] -or $InputObject -is [int64] -or $InputObject -is [uint16] -or $InputObject -is [uint32] -or $InputObject -is [uint64]) { $InputBytes = [System.BitConverter]::GetBytes($InputObject) } elseif ($InputObject -is [guid]) { $InputBytes = $InputObject.ToByteArray() } elseif ($InputObject -is [System.IO.FileSystemInfo]) { if ($PSVersionTable.PSVersion -ge [version]'6.0') { $InputBytes = Get-Content $InputObject.FullName -Raw -AsByteStream } else { $InputBytes = Get-Content $InputObject.FullName -Raw -Encoding Byte } } else { ## Non-Terminating Error $Exception = New-Object ArgumentException -ArgumentList ('Cannot compress input of type {0}.' -f $InputObject.GetType()) Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ParserError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'CompressDataFailureTypeNotSupported' -TargetObject $InputObject } if ($null -ne $InputBytes -and $InputBytes.Count -gt 0) { Write-Output (Compress $InputBytes -GZip:$GZip) -NoEnumerate } } } } end { ## Output captured byte stream from piped input. if ($listBytes.Count -gt 0) { Write-Output (Compress $listBytes.ToArray() -GZip:$GZip) -NoEnumerate } } } #endregion #region ConvertFrom-Base64String.ps1 <# .SYNOPSIS Convert Base64 String to Byte Array or Plain Text String. .EXAMPLE PS >ConvertFrom-Base64String "QSBzdHJpbmcgd2l0aCBiYXNlNjQgZW5jb2Rpbmc=" Convert Base64 String to String with Default Encoding. .EXAMPLE PS >"QVNDSUkgc3RyaW5nIHdpdGggYmFzZTY0dXJsIGVuY29kaW5n" | ConvertFrom-Base64String -Base64Url -Encoding Ascii Convert Base64Url String to String with Ascii Encoding. .EXAMPLE PS >[guid](ConvertFrom-Base64String "5oIhNbCaFUGAe8NsiAKfpA==" -RawBytes) Convert Base64 String to GUID. .INPUTS System.String .LINK https://github.com/jasoth/Utility.PS #> function ConvertFrom-Base64String { [CmdletBinding()] [OutputType([byte[]], [string])] param ( # Value to convert [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string[]] $InputObjects, # Use base64url variant [Parameter (Mandatory = $false)] [switch] $Base64Url, # Output raw byte array [Parameter (Mandatory = $false)] [switch] $RawBytes, # Encoding to use for text strings [Parameter (Mandatory = $false)] [ValidateSet('Ascii', 'UTF32', 'UTF7', 'UTF8', 'BigEndianUnicode', 'Unicode')] [string] $Encoding = 'Default' ) process { foreach ($InputObject in $InputObjects) { [string] $strBase64 = $InputObject if (!$PSBoundParameters.ContainsValue('Base64Url') -and ($strBase64.Contains('-') -or $strBase64.Contains('_'))) { $Base64Url = $true } if ($Base64Url) { $strBase64 = $strBase64.Replace('-', '+').Replace('_', '/').PadRight($strBase64.Length + (4 - $strBase64.Length % 4) % 4, '=') } [byte[]] $outBytes = [System.Convert]::FromBase64String($strBase64) if ($RawBytes) { Write-Output $outBytes -NoEnumerate } else { [string] $outString = ([Text.Encoding]::$Encoding.GetString($outBytes)) Write-Output $outString } } } } #endregion #region ConvertFrom-ClixmlString.ps1 <# .SYNOPSIS Convert Clixml serialized string to object. .EXAMPLE PS >ConvertFrom-ClixmlString '<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04"><S>A clixml serialized string</S></Objs>' Convert Clixml serialized string to object. .INPUTS System.String .LINK https://github.com/jasoth/Utility.PS #> function ConvertFrom-ClixmlString { [CmdletBinding()] [OutputType([object])] param ( # Value to convert [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string[]] $InputString ) process { #foreach ($_InputString in $InputString) { [System.Management.Automation.PSSerializer]::Deserialize($InputString) #} } } #endregion #region ConvertFrom-HexString.ps1 <# .SYNOPSIS Convert from Hex String .EXAMPLE PS >ConvertFrom-HexString "57 68 61 74 20 69 73 20 61 20 68 65 78 20 73 74 72 69 6E 67 3F" Convert hex byte string seperated by spaces to string. .EXAMPLE PS >"415343494920737472696E6720746F2068657820737472696E67" | ConvertFrom-HexString -Delimiter "" -Encoding Ascii Convert hex byte string with no seperation to ASCII string. .INPUTS System.String .LINK https://github.com/jasoth/Utility.PS #> function ConvertFrom-HexString { [CmdletBinding()] param ( # Value to convert [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string[]] $InputObject, # Delimiter between Hex pairs [Parameter (Mandatory = $false)] [string] $Delimiter = ' ', # Output raw byte array [Parameter (Mandatory = $false)] [switch] $RawBytes, # Encoding to use for text strings [Parameter (Mandatory = $false)] [ValidateSet('Ascii', 'UTF32', 'UTF7', 'UTF8', 'BigEndianUnicode', 'Unicode')] [string] $Encoding = 'Default' ) process { $InputObject = $InputObject -replace '\s', '' $listBytes = New-Object object[] $InputObject.Count for ($iString = 0; $iString -lt $InputObject.Count; $iString++) { [string] $strHex = $InputObject[$iString] if ($strHex -notmatch '^[A-Fa-f0-9\r\n]+$') { $Exception = New-Object System.Management.Automation.MethodInvocationException 'The input is not a valid hex string as it contains a non-hex character.' Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::InvalidData) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'ConvertFromHexFailureInvalidData' -TargetObject $InputObject return } if ($strHex.Substring(2, 1) -eq $Delimiter) { [string[]] $listHex = $strHex -split $Delimiter } else { [string[]] $listHex = New-Object string[] ($strHex.Length / 2) for ($iByte = 0; $iByte -lt $strHex.Length; $iByte += 2) { $listHex[[System.Math]::Truncate($iByte / 2)] = $strHex.Substring($iByte, 2) } } [byte[]] $outBytes = New-Object byte[] $listHex.Count for ($iByte = 0; $iByte -lt $listHex.Count; $iByte++) { $outBytes[$iByte] = [byte]::Parse($listHex[$iByte], [System.Globalization.NumberStyles]::HexNumber) } if ($RawBytes) { $listBytes[$iString] = $outBytes } else { $outString = ([Text.Encoding]::$Encoding.GetString($outBytes)) Write-Output $outString } } if ($RawBytes) { return $listBytes } } } #endregion #region ConvertFrom-HtmlString.ps1 <# .SYNOPSIS Convert HTML encoded string to string. .EXAMPLE PS >ConvertFrom-HtmlString 'A string with <html> encoding' Convert HTML encoded string to string. .INPUTS System.String .LINK https://github.com/jasoth/Utility.PS #> function ConvertFrom-HtmlString { [CmdletBinding()] [OutputType([string])] param ( # Value to convert [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string[]] $InputStrings ) process { foreach ($InputString in $InputStrings) { Write-Output ([System.Net.WebUtility]::HtmlDecode($InputString)) } } } #endregion #region ConvertFrom-QueryString.ps1 <# .SYNOPSIS Convert Query String to object. .EXAMPLE PS >ConvertFrom-QueryString '?name=path/file.json&index=10' Convert query string to object. .EXAMPLE PS >'name=path/file.json&index=10' | ConvertFrom-QueryString -AsHashtable Convert query string to hashtable. .INPUTS System.String .LINK https://github.com/jasoth/Utility.PS #> function ConvertFrom-QueryString { [CmdletBinding()] [OutputType([psobject])] [OutputType([hashtable])] param ( # Value to convert [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [AllowEmptyString()] [string[]] $InputStrings, # URL decode parameter names [Parameter(Mandatory = $false)] [switch] $DecodeParameterNames, # Converts to hash table object [Parameter(Mandatory = $false)] [switch] $AsHashtable ) process { foreach ($InputString in $InputStrings) { if ($AsHashtable) { [hashtable] $OutputObject = @{ } } else { [psobject] $OutputObject = New-Object psobject } if ($InputString) { if ($InputString[0] -eq '?') { $InputString = $InputString.Substring(1) } [string[]] $QueryParameters = $InputString.Split('&') foreach ($QueryParameter in $QueryParameters) { [string[]] $QueryParameterPair = $QueryParameter.Split('=') if ($DecodeParameterNames) { $QueryParameterPair[0] = [System.Net.WebUtility]::UrlDecode($QueryParameterPair[0]) } if ($OutputObject -is [hashtable]) { $OutputObject.Add($QueryParameterPair[0], [System.Net.WebUtility]::UrlDecode($QueryParameterPair[1])) } else { $OutputObject | Add-Member $QueryParameterPair[0] -MemberType NoteProperty -Value ([System.Net.WebUtility]::UrlDecode($QueryParameterPair[1])) } } } Write-Output $OutputObject } } } #endregion #region ConvertFrom-SecureString.ps1 <# .SYNOPSIS Converts a secure string to an encrypted standard string. .DESCRIPTION The ConvertFrom-SecureString cmdlet converts a secure string (System.Security.SecureString) into an encrypted standard string (System.String). Unlike a secure string, an encrypted standard string can be saved in a file for later use. The encrypted standard string can be converted back to its secure string format by using the ConvertTo-SecureString cmdlet. If an encryption key is specified by using the Key or SecureKey parameters, the Advanced Encryption Standard (AES) encryption algorithm is used. The specified key must have a length of 128, 192, or 256 bits because those are the key lengths supported by the AES encryption algorithm. If no key is specified, the Windows Data Protection API (DPAPI) is used to encrypt the standard string representation. .PARAMETER AsPlainText When set, ConvertFrom-SecureString will convert secure strings to the decrypted plaintext string as output. .PARAMETER Key Specifies the encryption key as a byte array. .PARAMETER SecureKey Specifies the encryption key as a secure string. The secure string value is converted to a byte array before being used as the key. .PARAMETER SecureString Specifies the secure string to convert to an encrypted standard string. .EXAMPLE PS >$SecureString = Read-Host -AsSecureString PS >$StandardString = ConvertFrom-SecureString $SecureString This command converts the secure string in the $SecureString variable to an encrypted standard string. The resulting encrypted standard string is stored in the $StandardString variable. .EXAMPLE PS >$SecureString = Read-Host -AsSecureString PS >$Key = (3,4,2,3,56,34,254,222,1,1,2,23,42,54,33,233,1,34,2,7,6,5,35,43) PS >$StandardString = ConvertFrom-SecureString $SecureString -Key $Key These commands use the Advanced Encryption Standard (AES) algorithm to convert the secure string stored in the $SecureString variable to an encrypted standard string with a 192-bit key. The resulting encrypted standard string is stored in the $StandardString variable. The first command stores a key in the $Key variable. The key is an array of 24 decimal numerals, each of which must be less than 256 to fit within a single unsigned byte. Because each decimal numeral represents a single byte (8 bits), the key has 24 digits for a total of 192 bits (8 x 24). This is a valid key length for the AES algorithm. The second command uses the key in the $Key variable to convert the secure string to an encrypted standard string. .INPUTS System.Security.SecureString You can pipe a SecureString object to this cmdlet. .OUTPUTS System.String This cmdlet returns the created plain text string. .NOTES To create a secure string from characters that are typed at the command prompt, use the AsSecureString parameter of the Read-Host cmdlet. When you use the Key or SecureKey parameters to specify a key, the key length must be correct. For example, a key of 128 bits can be specified as a byte array of 16 decimal numerals. Similarly, 192-bit and 256-bit keys correspond to byte arrays of 24 and 32 decimal numerals, respectively. Some characters, such as emoticons, correspond to several code points in the string that contains them. Avoid using these characters because they may cause problems and misunderstandings when used in a password. .LINK https://go.microsoft.com/fwlink/?LinkID=113287 .LINK https://learn.microsoft.com/powershell/module/microsoft.powershell.security/convertfrom-securestring .LINK ConvertTo-SecureString .LINK Read-Host .LINK https://github.com/jasoth/Utility.PS #> function ConvertFrom-SecureString { [CmdletBinding(DefaultParameterSetName = 'Secure', HelpUri = 'https://go.microsoft.com/fwlink/?LinkID=113287')] param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [securestring] ${SecureString}, [Parameter(ParameterSetName = 'PlainText')] [switch] ${AsPlainText}, [Parameter(ParameterSetName = 'Secure', Position = 1)] [securestring] ${SecureKey}, [Parameter(ParameterSetName = 'Open')] [byte[]] ${Key} ) begin { ## Command Extension if ($PSBoundParameters.ContainsKey('AsPlainText') -and $PSVersionTable.PSVersion -lt [version]'7.0') { if (${AsPlainText}) { return } else { [void] $PSBoundParameters.Remove('AsPlainText') } } ## Resume Command try { $outBuffer = $null if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) { $PSBoundParameters['OutBuffer'] = 1 } $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Security\ConvertFrom-SecureString', [System.Management.Automation.CommandTypes]::Cmdlet) $scriptCmd = { & $wrappedCmd @PSBoundParameters } $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) $steppablePipeline.Begin($PSCmdlet) } catch { throw } } process { ## Command Extension if (${AsPlainText} -and $PSVersionTable.PSVersion -lt [version]'7.0') { try { [IntPtr] $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) Write-Output ([System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($BSTR)) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) } return } ## Resume Command try { $steppablePipeline.Process($_) } catch { throw } } end { ## Command Extension if (${AsPlainText} -and $PSVersionTable.PSVersion -lt [version]'7.0') { return } ## Resume Command try { $steppablePipeline.End() } catch { throw } } <# .ForwardHelpTargetName Microsoft.PowerShell.Security\ConvertFrom-SecureString .ForwardHelpCategory Cmdlet #> } #endregion #region ConvertFrom-SecureStringAsPlainText.ps1 <# .SYNOPSIS Convert/Decrypt SecureString to Plain Text String. .EXAMPLE PS >ConvertFrom-SecureStringAsPlainText (ConvertTo-SecureString 'SuperSecretString' -AsPlainText -Force) -Force Convert plain text to SecureString and then convert it back. .INPUTS System.Security.SecureString .LINK https://github.com/jasoth/Utility.PS #> function ConvertFrom-SecureStringAsPlainText { [CmdletBinding()] [OutputType([string])] param ( # Secure String Value [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [securestring] $SecureString, # Confirms that you understand the implications of using the AsPlainText parameter and still want to use it. [Parameter(Mandatory = $false)] [switch] $Force ) begin { if ($PSVersionTable.PSVersion -ge [version]'7.0') { Write-Warning 'PowerShell 7 introduced an AsPlainText parameter to the ConvertFrom-SecureString cmdlet.' } if (!${Force}) { ## Terminating Error $Exception = New-Object ArgumentException -ArgumentList 'The system cannot protect plain text output. To suppress this warning and convert a SecureString to plain text, reissue the command specifying the Force parameter.' Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::InvalidArgument) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'ConvertSecureStringFailureForceRequired' -TargetObject ${SecureString} -ErrorAction Stop } } process { try { [IntPtr] $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) Write-Output ([System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($BSTR)) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) } } } #endregion #region ConvertFrom-UrlString.ps1 <# .SYNOPSIS Convert URL encoded string to string. .EXAMPLE PS >ConvertFrom-UrlString 'A+string+with+url+encoding' Convert URL encoded string to string. .INPUTS System.String .LINK https://github.com/jasoth/Utility.PS #> function ConvertFrom-UrlString { [CmdletBinding()] [OutputType([string])] param ( # Value to convert [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string[]] $InputStrings ) process { foreach ($InputString in $InputStrings) { Write-Output ([System.Net.WebUtility]::UrlDecode($InputString)) } } } #endregion #region ConvertTo-Base64String.ps1 <# .SYNOPSIS Convert Byte Array or Plain Text String to Base64 String. .EXAMPLE PS >ConvertTo-Base64String "A string with base64 encoding" Convert String with Default Encoding to Base64 String. .EXAMPLE PS >"ASCII string with base64url encoding" | ConvertTo-Base64String -Base64Url -Encoding Ascii Convert String with Ascii Encoding to Base64Url String. .EXAMPLE PS >ConvertTo-Base64String ([guid]::NewGuid()) Convert GUID to Base64 String. .INPUTS System.Object .LINK https://github.com/jasoth/Utility.PS #> function ConvertTo-Base64String { [CmdletBinding()] [OutputType([string])] param ( # Value to convert [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [object] $InputObjects, # Use base64url variant [Parameter (Mandatory = $false)] [switch] $Base64Url, # Output encoding to use for text strings [Parameter (Mandatory = $false)] [ValidateSet('Ascii', 'UTF32', 'UTF7', 'UTF8', 'BigEndianUnicode', 'Unicode')] [string] $Encoding = 'Default' ) begin { function Transform ([byte[]]$InputBytes) { [string] $outBase64String = [System.Convert]::ToBase64String($InputBytes) if ($Base64Url) { $outBase64String = $outBase64String.Replace('+', '-').Replace('/', '_').Replace('=', '') } return $outBase64String } ## Create list to capture byte stream from piped input. [System.Collections.Generic.List[byte]] $listBytes = New-Object System.Collections.Generic.List[byte] } process { if ($InputObjects -is [byte[]]) { Write-Output (Transform $InputObjects) } else { foreach ($InputObject in $InputObjects) { [byte[]] $InputBytes = $null if ($InputObject -is [byte]) { ## Populate list with byte stream from piped input. if ($listBytes.Count -eq 0) { Write-Verbose 'Creating byte array from byte stream.' Write-Warning ('For better performance when piping a single byte array, use "Write-Output $byteArray -NoEnumerate | {0}".' -f $MyInvocation.MyCommand) } $listBytes.Add($InputObject) } elseif ($InputObject -is [byte[]]) { $InputBytes = $InputObject } elseif ($InputObject -is [string]) { $InputBytes = [Text.Encoding]::$Encoding.GetBytes($InputObject) } elseif ($InputObject -is [bool] -or $InputObject -is [char] -or $InputObject -is [single] -or $InputObject -is [double] -or $InputObject -is [int16] -or $InputObject -is [int32] -or $InputObject -is [int64] -or $InputObject -is [uint16] -or $InputObject -is [uint32] -or $InputObject -is [uint64]) { $InputBytes = [System.BitConverter]::GetBytes($InputObject) } elseif ($InputObject -is [guid]) { $InputBytes = $InputObject.ToByteArray() } elseif ($InputObject -is [System.IO.FileSystemInfo]) { if ($PSVersionTable.PSVersion -ge [version]'6.0') { $InputBytes = Get-Content $InputObject.FullName -Raw -AsByteStream } else { $InputBytes = Get-Content $InputObject.FullName -Raw -Encoding Byte } } else { ## Non-Terminating Error $Exception = New-Object ArgumentException -ArgumentList ('Cannot convert input of type {0} to Base64 string.' -f $InputObject.GetType()) Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ParserError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'ConvertBase64StringFailureTypeNotSupported' -TargetObject $InputObject } if ($null -ne $InputBytes -and $InputBytes.Count -gt 0) { Write-Output (Transform $InputBytes) } } } } end { ## Output captured byte stream from piped input. if ($listBytes.Count -gt 0) { Write-Output (Transform $listBytes.ToArray()) } } } #endregion #region ConvertTo-ClixmlString.ps1 <# .SYNOPSIS Convert string to Clixml serialized string. .EXAMPLE PS >ConvertTo-ClixmlString 'A clixml serialized string' Convert string to Clixml serialized string. .INPUTS System.Object .LINK https://github.com/jasoth/Utility.PS #> function ConvertTo-ClixmlString { [CmdletBinding()] [OutputType([string])] param ( # Value to convert [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [object] $InputObject, # Omits white space and indented formatting in the output string. [Parameter(Mandatory = $false)] [switch] $Compress, # Specifies how many levels of nested objects are included. [Parameter(Mandatory = $false)] [int] $Depth ) process { #foreach ($_InputObject in $InputObject) { if ($Depth) { $OutputString = [System.Management.Automation.PSSerializer]::Serialize($InputObject, $Depth) } else { $OutputString = [System.Management.Automation.PSSerializer]::Serialize($InputObject) } if ($Compress) { $OutputString = $OutputString -replace '\r?\n\s*', '' } return $OutputString #} } } #endregion #region ConvertTo-Csv.ps1 <# .SYNOPSIS Converts .NET objects into a series of character-separated value (CSV) strings. .DESCRIPTION The `ConvertTo-CSV` cmdlet returns a series of character-separated value (CSV) strings that represent the objects that you submit. You can then use the `ConvertFrom-Csv` cmdlet to recreate objects from the CSV strings. The objects converted from CSV are string values of the original objects that contain property values and no methods. You can use the `Export-Csv` cmdlet to convert objects to CSV strings. `Export-CSV` is similar to `ConvertTo-CSV`, except that it saves the CSV strings to a file. The `ConvertTo-CSV` cmdlet has parameters to specify a delimiter other than a comma or use the current culture as the delimiter. .PARAMETER InputObject Specifies the objects that are converted to CSV strings. Enter a variable that contains the objects or type a command or expression that gets the objects. You can also pipe objects to `ConvertTo-CSV`. .PARAMETER Delimiter Specifies the delimiter to separate the property values in CSV strings. The default is a comma (`,`). Enter a character, such as a colon (`:`). To specify a semicolon (`;`) enclose it in single quotation marks. If you specify a character other than the actual string delimiter in the file, `ConvertFrom-Csv` can't create the objects from the CSV strings and returns the CSV strings. .PARAMETER UseCulture Uses the list separator for the current culture as the item delimiter. To find the list separator for a culture, use the following command: `(Get-Culture).TextInfo.ListSeparator`. .PARAMETER IncludeTypeInformation When this parameter is used the first line of the output contains #TYPE followed by the fully qualified name of the object type. For example, #TYPE System.Diagnostics.Process. This parameter was introduced in PowerShell 6.0. .PARAMETER NoTypeInformation Removes the #TYPE information header from the output. This parameter became the default in PowerShell 6.0 and is included for backwards compatibility. .PARAMETER ArrayDelimiter Specifies the delimiter that separates the items in arrays. .EXAMPLE PS >Get-Process -Name pwsh | ConvertTo-Csv -NoTypeInformation "Name","SI","Handles","VM","WS","PM","NPM","Path","Parent","Company","CPU","FileVersion", ... "pwsh","8","950","2204001161216","100925440","59686912","67104", ... The `Get-Process` cmdlet gets the Process object and uses the Name parameter to specify the PowerShell process. The process object is sent down the pipeline to the `ConvertTo-CSV` cmdlet. The `ConvertTo-CSV` cmdlet converts the object to CSV strings. The NoTypeInformation parameter removes the #TYPE information header from the CSV output and is not required in PowerShell 6. .EXAMPLE PS >$Date = Get-Date PS >ConvertTo-Csv -InputObject $Date -Delimiter ';' -NoTypeInformation "DisplayHint";"DateTime";"Date";"Day";"DayOfWeek";"DayOfYear";"Hour";"Kind";"Millisecond";"Minute";"Month";"Second";"Ticks";"TimeOfDay";"Year" "DateTime";"Friday, January 4, 2019 14:40:51";"1/4/2019 00:00:00";"4";"Friday";"4";"14";"Local";"711";"40";"1";"51";"636822096517114991";"14:40:51.7114991";"2019"</dev:code> The `Get-Date` cmdlet gets the DateTime object and saves it in the `$Date` variable. The `ConvertTo-Csv` cmdlet converts the DateTime object to strings. The InputObject parameter uses the DateTime object stored in the `$Date` variable. The Delimiter parameter specifies a semicolon to separate the string values. The NoTypeInformation parameter removes the #TYPE information header from the CSV output and is not required in PowerShell 6. .EXAMPLE PS >(Get-Culture).TextInfo.ListSeparator PS >Get-WinEvent -LogName 'PowerShellCore/Operational' | ConvertTo-Csv -UseCulture -NoTypeInformation , "Message","Id","Version","Qualifiers","Level","Task","Opcode","Keywords","RecordId", ... "Error Message = System error""4100","1",,"3","106","19","0","31716","PowerShellCore", ... The `Get-Culture` cmdlet uses the nested properties TextInfo and ListSeparator and displays the current culture's default list separator. The `Get-WinEvent` cmdlet gets the event log objects and uses the LogName parameter to specify the log file name. The event log objects are sent down the pipeline to the `ConvertTo-Csv` cmdlet. The `ConvertTo-Csv` cmdlet converts the event log objects to a series of CSV strings. The UseCulture parameter uses the current culture's default list separator as the delimiter. The NoTypeInformation parameter removes the #TYPE information header from the CSV output and is not required in PowerShell 6. .EXAMPLE PS >Get-Date | ConvertTo-Csv -QuoteFields "DateTime","Date" DisplayHint,"DateTime","Date",Day,DayOfWeek,DayOfYear,Hour,Kind,Millisecond,Minute,Month,Second,Ticks,TimeOfDay,Year DateTime,"Thursday, August 22, 2019 11:27:34 AM","8/22/2019 12:00:00 AM",22,Thursday,234,11,Local,569,27,8,34,637020700545699784,11:27:34.5699784,2019 Convert to CSV with quotes around two columns .EXAMPLE PS >Get-Date | ConvertTo-Csv -UseQuotes AsNeeded DisplayHint,DateTime,Date,Day,DayOfWeek,DayOfYear,Hour,Kind,Millisecond,Minute,Month,Second,Ticks,TimeOfDay,Year DateTime,"Thursday, August 22, 2019 11:31:00 AM",8/22/2019 12:00:00 AM,22,Thursday,234,11,Local,713,31,8,0,637020702607132640,11:31:00.7132640,2019 Convert to CSV with quotes only when needed .EXAMPLE PS >$person1 = @{ Name = 'John Smith' Number = 1 } PS >$person2 = @{ Name = 'Jane Smith' Number = 2 } PS >$allPeople = $person1, $person2 PS >$allPeople | ConvertTo-Csv "Name","Number" "John Smith","1" "Jane Smith","2" Convert hashtables to CSV .EXAMPLE PS >$allPeople | Add-Member -Name ExtraProp -Value 42 PS >$allPeople | ConvertTo-Csv "Name","Number","ExtraProp" "John Smith","1","42" "Jane Smith","2","42" Each hashtable has a property named `ExtraProp` added by `Add-Member` and then converted to CSV. You can see `ExtraProp` is now a header in the output. If an added property has the same name as a key from the hashtable, the key takes precedence and only the key is converted to CSV. .INPUTS System.Security.SecureString You can pipe a SecureString object to this cmdlet. .OUTPUTS System.String This cmdlet returns the created plain text string. .NOTES To create a secure string from characters that are typed at the command prompt, use the AsSecureString parameter of the Read-Host cmdlet. When you use the Key or SecureKey parameters to specify a key, the key length must be correct. For example, a key of 128 bits can be specified as a byte array of 16 decimal numerals. Similarly, 192-bit and 256-bit keys correspond to byte arrays of 24 and 32 decimal numerals, respectively. Some characters, such as emoticons, correspond to several code points in the string that contains them. Avoid using these characters because they may cause problems and misunderstandings when used in a password. .LINK https://go.microsoft.com/fwlink/?LinkID=135203 .LINK ConvertFrom-Csv .LINK Export-Csv .LINK Import-Csv .LINK https://github.com/jasoth/Utility.PS #> function ConvertTo-Csv { [CmdletBinding(DefaultParameterSetName = 'DelimiterPath', HelpUri = 'https://go.microsoft.com/fwlink/?LinkID=135203', RemotingCapability = 'None')] param( [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [psobject] ${InputObject}, [Parameter(ParameterSetName = 'Delimiter', Position = 1)] [ValidateNotNull()] [char] ${Delimiter}, [Parameter(ParameterSetName = 'UseCulture')] [switch] ${UseCulture}, [Alias('ITI')] [switch] ${IncludeTypeInformation}, [Alias('NTI')] [switch] ${NoTypeInformation}, [ValidateNotNull()] [string] ${ArrayDelimiter} = "`r`n" ) begin { function Transform (${InputObject}, ${ArrayDelimiter}) { [bool] $ContainsArray = $false [System.Collections.Generic.List[object]] $SelectProperties = New-Object System.Collections.Generic.List[object] $Properties = ${InputObject} | Select-Object -First 1 | Get-Member -MemberType NoteProperty, Property, ScriptProperty foreach ($Property in $Properties) { if ($Property.Definition -like ("*``[``] {0}*" -f $Property.Name) -or $Property.Definition -like ("*List``[*``] {0}*" -f $Property.Name)) { $SelectProperties.Add(@{ Name = $Property.Name; Expression = [scriptblock]::Create(('$_.{0} -join "{1}"' -f $Property.Name, ${ArrayDelimiter})) }) $ContainsArray = $true } else { $SelectProperties.Add($Property.Name) } } if ($ContainsArray) { return ${InputObject} | Select-Object -Property $SelectProperties.ToArray() } else { return ${InputObject} } } ## Command Extension if ($null -ne ${InputObject}) { $PSBoundParameters['InputObject'] = Transform ${InputObject} ${ArrayDelimiter} } ## Remove extra parameters if ($PSBoundParameters.ContainsKey('ArrayDelimiter')) { [void] $PSBoundParameters.Remove('ArrayDelimiter') } ## Resume Command try { $outBuffer = $null if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) { $PSBoundParameters['OutBuffer'] = 1 } $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\ConvertTo-Csv', [System.Management.Automation.CommandTypes]::Cmdlet) $scriptCmd = { & $wrappedCmd @PSBoundParameters } $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) $steppablePipeline.Begin($PSCmdlet) } catch { throw } } process { ## Command Extension if ($null -ne ${InputObject}) { $_ = Transform ${InputObject} ${ArrayDelimiter} } ## Resume Command try { $steppablePipeline.Process($_) } catch { throw } } end { try { $steppablePipeline.End() } catch { throw } } <# .ForwardHelpTargetName Microsoft.PowerShell.Utility\ConvertTo-Csv .ForwardHelpCategory Cmdlet #> } #endregion #region ConvertTo-Dictionary.ps1 <# .SYNOPSIS Convert hashtable to generic dictionary. .EXAMPLE PS >ConvertTo-Dictionary @{ KeyName = 'StringValue' } -ValueType ([string]) Convert hashtable to generic dictionary. .INPUTS System.Hashtable .LINK https://github.com/jasoth/Utility.PS #> function ConvertTo-Dictionary { [CmdletBinding()] [OutputType([System.Collections.Generic.Dictionary[object, object]])] param ( # Value to convert [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [hashtable[]] $InputObjects, # Data Type of Key [Parameter(Mandatory = $false)] [type] $KeyType = [string], # Data Type of Value [Parameter(Mandatory = $false)] [type] $ValueType = [object] ) process { foreach ($InputObject in $InputObjects) { $OutputObject = New-Object ('System.Collections.Generic.Dictionary[[{0}],[{1}]]' -f $KeyType.FullName, $ValueType.FullName) foreach ($KeyPair in $InputObject.GetEnumerator()) { $OutputObject.Add($KeyPair.Key, $KeyPair.Value) } Write-Output $OutputObject } } } #endregion #region ConvertTo-HexString.ps1 <# .SYNOPSIS Convert to Hex String .EXAMPLE PS >ConvertTo-HexString "What is a hex string?" Convert string to hex byte string seperated by spaces. .EXAMPLE PS >"ASCII string to hex string" | ConvertTo-HexString -Delimiter "" -Encoding Ascii Convert ASCII string to hex byte string with no seperation. .INPUTS System.Object .LINK https://github.com/jasoth/Utility.PS #> function ConvertTo-HexString { [CmdletBinding()] param ( # Value to convert [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [object] $InputObjects, # Delimiter between Hex pairs [Parameter (Mandatory = $false)] [string] $Delimiter = ' ', # Encoding to use for text strings [Parameter (Mandatory = $false)] [ValidateSet('Ascii', 'UTF32', 'UTF7', 'UTF8', 'BigEndianUnicode', 'Unicode')] [string] $Encoding = 'Default' ) begin { function Transform ([byte[]]$InputBytes) { [string[]] $outHexString = New-Object string[] $InputBytes.Count for ($iByte = 0; $iByte -lt $InputBytes.Count; $iByte++) { $outHexString[$iByte] = $InputBytes[$iByte].ToString('X2') } return $outHexString -join $Delimiter } ## Create list to capture byte stream from piped input. [System.Collections.Generic.List[byte]] $listBytes = New-Object System.Collections.Generic.List[byte] } process { if ($InputObjects -is [byte[]]) { Write-Output (Transform $InputObjects) } else { foreach ($InputObject in $InputObjects) { [byte[]] $InputBytes = $null if ($InputObject -is [byte]) { ## Populate list with byte stream from piped input. if ($listBytes.Count -eq 0) { Write-Verbose 'Creating byte array from byte stream.' Write-Warning ('For better performance when piping a single byte array, use "Write-Output $byteArray -NoEnumerate | {0}".' -f $MyInvocation.MyCommand) } $listBytes.Add($InputObject) } elseif ($InputObject -is [byte[]]) { $InputBytes = $InputObject } elseif ($InputObject -is [string]) { $InputBytes = [Text.Encoding]::$Encoding.GetBytes($InputObject) } elseif ($InputObject -is [bool] -or $InputObject -is [char] -or $InputObject -is [single] -or $InputObject -is [double] -or $InputObject -is [int16] -or $InputObject -is [int32] -or $InputObject -is [int64] -or $InputObject -is [uint16] -or $InputObject -is [uint32] -or $InputObject -is [uint64]) { $InputBytes = [System.BitConverter]::GetBytes($InputObject) } elseif ($InputObject -is [guid]) { $InputBytes = $InputObject.ToByteArray() } elseif ($InputObject -is [System.IO.FileSystemInfo]) { if ($PSVersionTable.PSVersion -ge [version]'6.0') { $InputBytes = Get-Content $InputObject.FullName -Raw -AsByteStream } else { $InputBytes = Get-Content $InputObject.FullName -Raw -Encoding Byte } } else { ## Non-Terminating Error $Exception = New-Object ArgumentException -ArgumentList ('Cannot convert input of type {0} to Hex string.' -f $InputObject.GetType()) Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ParserError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'ConvertHexFailureTypeNotSupported' -TargetObject $InputObject } if ($null -ne $InputBytes -and $InputBytes.Count -gt 0) { Write-Output (Transform $InputBytes) } } } } end { ## Output captured byte stream from piped input. if ($listBytes.Count -gt 0) { Write-Output (Transform $listBytes.ToArray()) } } } #endregion #region ConvertTo-HtmlString.ps1 <# .SYNOPSIS Convert string to HTML encoded string. .EXAMPLE PS >ConvertTo-HtmlString 'A string with <html> encoding' Convert string to HTML encoded string. .INPUTS System.String .LINK https://github.com/jasoth/Utility.PS #> function ConvertTo-HtmlString { [CmdletBinding()] [OutputType([string])] param ( # Value to convert [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string[]] $InputStrings ) process { foreach ($InputString in $InputStrings) { Write-Output ([System.Net.WebUtility]::HtmlEncode($InputString)) } } } #endregion #region ConvertTo-MarkdownTable.ps1 <# .SYNOPSIS Converts an object to a markdown table. .EXAMPLE PS >ConvertTo-MarkdownTable $PsVersionTable Converts the PsVersionTable variable object to markdown table. .EXAMPLE PS >Get-PSHostProcessInfo | ConvertTo-MarkdownTable -Compact Converts PSHostProcessInfo objects to markdown table. .INPUTS System.Object .LINK https://github.com/jasoth/Utility.PS #> function ConvertTo-MarkdownTable { [CmdletBinding()] [OutputType([string])] param ( # Objects to convert [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [object[]] $InputObject, # Property names to include in the output. [Parameter(Mandatory = $false, Position = 1)] [string[]] $Property, # Output one row per input object or one keypair list table per input object [Parameter(Mandatory = $false)] [ValidateSet('Table', 'List')] [string] $As, # Do not include whitespace padding in table [Parameter(Mandatory = $false)] [switch] $Compact, # String to use as delimiter for array values [Parameter(Mandatory = $false)] [string] $ArrayDelimiter, # Format second level depth objects with the specified format [Parameter(Mandatory = $false)] [ValidateSet('ToString', 'PsFormat', 'Html')] [string] $ObjectFormat = 'PsFormat' ) begin { ## Initalize variables $NewLineReplacement = '<br>' function FormatMarkdownTableHeaderRow ($ColumnWidths) { if ($ColumnWidths.Count -gt 0) { $InitialColumn = $true [string]$TableRow = '| ' [string]$DelimiterRow = '| ' foreach ($PropertyName in $ColumnWidths.Keys) { if (!$InitialColumn) { $TableRow += ' | ' } $TableRow += $PropertyName.PadRight($ColumnWidths[$PropertyName], ' ') if (!$InitialColumn) { $DelimiterRow += ' | ' } if ($ColumnWidths[$PropertyName] -gt 0) { $DelimiterRow += '---'.PadRight($ColumnWidths[$PropertyName], '-') } else { $DelimiterRow += '---' #.PadRight($PropertyName.Length, '-') } $InitialColumn = $false } $TableRow += ' |' $DelimiterRow += ' |' $TableRow $DelimiterRow } } function FormatMarkdownTableRow ($ColumnWidths, $InputObject) { $InitialColumn = $true [string]$TableRow = '| ' foreach ($PropertyName in $ColumnWidths.Keys) { if (!$InitialColumn) { $TableRow += ' | ' } if ($InputObject) { $StringValue = '' $PropertyValue = Get-PropertyValue $InputObject $PropertyName $StringValue = Transform $PropertyValue $TableRow += $StringValue.PadRight($ColumnWidths[$PropertyName], ' ') } $InitialColumn = $false } $TableRow += ' |' return $TableRow } function FormatMarkdownKeyPairRows ($ColumnWidths, $InputObject) { foreach ($Property in $InputObject.PSObject.Properties) { $InitialColumn = $true [string]$TableRow = '| ' foreach ($PropertyName in $ColumnWidths.Keys) { if (!$InitialColumn) { $TableRow += ' | ' } if ($InputObject) { if ($PropertyName -eq 'Name') { $TableRow += $Property.Name.PadRight($ColumnWidths['Name'], ' ') } else { $StringValue = '' $PropertyValue = $Property.Value $StringValue = Transform $PropertyValue $TableRow += $StringValue.PadRight($ColumnWidths['Value'], ' ') } } $InitialColumn = $false } $TableRow += ' |' Write-Output $TableRow } } function Transform ($PropertyValue) { $StringValue = '' if ($null -ne $PropertyValue) { if ($ArrayDelimiter -ne '' -and $PropertyValue -is [System.Collections.IList]) { [array]$ArrayObject = New-Object -TypeName object[] -ArgumentList $PropertyValue.Count # ConstrainedLanguage safe for ($i = 0; $i -lt $PropertyValue.Count; $i++) { $ArrayObject[$i] = $PropertyValue[$i].ToString() if (!$ArrayObject[$i]) { $ArrayObject[$i] = $PropertyValue[$i].psobject.TypeNames[0] } } $StringValue = ($ArrayObject -join $ArrayDelimiter) } elseif ($PropertyValue -is [System.Collections.IDictionary] -or $PropertyValue -is [psobject]) { if ($PropertyValue -is [System.Collections.IDictionary]) { $PropertyValue = New-Object -TypeName PSObject -Property $PropertyValue # ConstrainedLanguage safe } if ($ObjectFormat -eq 'PsFormat') { $FormattedObject = $PropertyValue | Format-List | Out-String -Width 2147483647 $StringValue = $FormattedObject.Trim("`r", "`n") } elseif ($ObjectFormat -eq 'Html') { $HtmlTable = $PropertyValue | ConvertTo-Html -Fragment -As List $StringValue = $HtmlTable -join '' } else { $StringValue = $PropertyValue.ToString() if (!$StringValue) { $StringValue = $PropertyValue.psobject.TypeNames[0] } } } else { $StringValue = $PropertyValue.ToString() } } $StringValue = $StringValue.Replace('\', '\\').Replace('|', '\|') # Escape backslash and pipe characters $StringValue = $StringValue -replace '(?<=[>])[\r\n]+(?=[<])', '' # Remove newlines between html tags $StringValue = $StringValue -replace '[\r\n]+', $NewLineReplacement # Replace newlines return $StringValue } $TableObjects = @() } process { foreach ($_InputObject in $InputObject) { ## Convert dictionaries if ($_InputObject -is [System.Collections.IDictionary]) { $_InputObject = New-Object -TypeName PSObject -Property $_InputObject # ConstrainedLanguage safe } if ($Property) { $OutputObject = Select-Object -InputObject $_InputObject -Property $Property } else { $OutputObject = Select-Object -InputObject $_InputObject -Property "*" } $TableObjects += $OutputObject } } end { if (!$As) { if ($TableObjects.Count -gt 1) { $As = 'Table' } else { $As = 'List' } } if ($As -eq 'List') { foreach ($ObjectTable in $TableObjects) { ## Get column names and widths $KeyPairWidths = [ordered]@{ Name = 0; Value = 0 } foreach ($objProperty in $ObjectTable.PSObject.Properties) { if (!$Compact -and $KeyPairWidths['Name'] -lt $objProperty.Name.Length) { $KeyPairWidths['Name'] = $objProperty.Name.Length } $PropertyValue = Transform $objProperty.Value if (!$Compact -and $null -ne $PropertyValue) { if ($KeyPairWidths['Value'] -lt $PropertyValue.Length) { $KeyPairWidths['Value'] = $PropertyValue.Length } } } ## Output Header and Separator Rows FormatMarkdownTableHeaderRow $KeyPairWidths ## Output Object Rows FormatMarkdownKeyPairRows $KeyPairWidths $ObjectTable '' } } else { ## Get column names and widths $ColumnWidths = [ordered]@{} foreach ($ObjectRow in $TableObjects) { foreach ($objProperty in $ObjectRow.PSObject.Properties) { if ($Compact) { $ColumnWidths[$objProperty.Name] = 0 } elseif ($null -eq $ColumnWidths[$objProperty.Name]) { $ColumnWidths[$objProperty.Name] = $objProperty.Name.Length } $PropertyValue = Transform $objProperty.Value if (!$Compact -and $null -ne $PropertyValue) { if ($ColumnWidths[$objProperty.Name] -lt $PropertyValue.Length) { $ColumnWidths[$objProperty.Name] = $PropertyValue.Length } } } } ## Output Header and Separator Rows FormatMarkdownTableHeaderRow $ColumnWidths ## Output Object Rows foreach ($ObjectRow in $TableObjects) { FormatMarkdownTableRow $ColumnWidths $ObjectRow } } } } #endregion #region ConvertTo-PsParameterString.ps1 <# .SYNOPSIS Convert splatable PowerShell paramters to PowerShell parameter string syntax. .EXAMPLE PS >ConvertTo-PsParameterString @{ key1='value1'; key2='value2' } Convert hashtable to PowerShell parameters string. .INPUTS System.String .LINK https://github.com/jasoth/Utility.PS #> function ConvertTo-PsParameterString { [CmdletBinding()] [OutputType([string])] param ( # Specifies the parameter object to convert to PowerShell string. [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] [AllowNull()] [object] $InputObjects, # Abbrivate types where possible [Parameter(Mandatory = $false)] [switch] $Compact, # Remove types [Parameter(Mandatory = $false, Position = 1)] [type[]] $RemoveTypes = ([string], [bool], [int], [long]), # Do not enumerate output objects [Parameter(Mandatory = $false)] [switch] $NoEnumerate ) begin { function GetPsParameterString ($InputObject) { $OutputString = New-Object System.Text.StringBuilder ## Add Value switch ($InputObject.GetType()) { { $_.Equals([Hashtable]) -or $_.Equals([System.Collections.Specialized.OrderedDictionary]) -or $_.FullName.StartsWith('System.Collections.Generic.Dictionary') -or ($_.BaseType -and $_.BaseType.FullName.StartsWith('System.Collections.Generic.Dictionary')) } { foreach ($Parameter in $InputObject.GetEnumerator()) { [string] $ParameterValue = (ConvertTo-PsString $Parameter.Value -Compact:$Compact -NoEnumerate) if ($ParameterValue.StartsWith('[')) { $ParameterValue = '({0})' -f $ParameterValue } [void]$OutputString.AppendFormat(' -{0} {1}', $Parameter.Key, $ParameterValue) } break } { $_.BaseType.Equals([Array]) -or $_.Equals([System.Collections.ArrayList]) -or $_.FullName.StartsWith('System.Collections.Generic.List') } { foreach ($Parameter in $InputObject) { [string] $ParameterValue = (ConvertTo-PsString $Parameter -Compact:$Compact -NoEnumerate) if ($ParameterValue.StartsWith('[')) { $ParameterValue = '({0})' -f $ParameterValue } [void]$OutputString.AppendFormat(' {0}', $ParameterValue) } break } Default { $Exception = New-Object ArgumentException -ArgumentList ('Cannot convert input of type {0} to PowerShell parameter string. Use -NoEnumerate if providing a single splatable array.' -f $InputObject.GetType()) Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ParserError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'ConvertPowerShellParameterStringFailureTypeNotSupported' -TargetObject $InputObject -ErrorAction Stop } } if ($NoEnumerate) { $listOutputString.Add($OutputString.ToString()) } else { Write-Output $OutputString.ToString() } } if ($NoEnumerate) { $listOutputString = New-Object System.Collections.Generic.List[string] } } process { if ($PSCmdlet.MyInvocation.ExpectingInput -or $NoEnumerate) { GetPsParameterString $InputObjects } else { foreach ($InputObject in $InputObjects) { GetPsParameterString $InputObject } } } end { if ($NoEnumerate) { $OutputArray = New-Object System.Text.StringBuilder if ($PSVersionTable.PSVersion -ge [version]'6.0') { [void]$OutputArray.AppendJoin('', $listOutputString) } else { [void]$OutputArray.Append(($listOutputString -join '')) } Write-Output $OutputArray.ToString() } } } #endregion #region ConvertTo-PsString.ps1 <# .SYNOPSIS Convert PowerShell data types to PowerShell string syntax. .EXAMPLE PS >ConvertTo-PsString @{ key1='value1'; key2='value2' } Convert hashtable to PowerShell string. .INPUTS System.String .LINK https://github.com/jasoth/Utility.PS #> function ConvertTo-PsString { [CmdletBinding()] [OutputType([string])] param ( # Specifies the object to convert to PowerShell string. [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] [AllowNull()] [object] $InputObjects, # Abbrivate types where possible [Parameter(Mandatory = $false)] [switch] $Compact, # Remove types [Parameter(Mandatory = $false, Position = 1)] [type[]] $RemoveTypes = ([string], [bool], [int], [long]), # Do not enumerate output objects [Parameter(Mandatory = $false)] [switch] $NoEnumerate ) begin { if ($Compact) { [System.Collections.Generic.Dictionary[string, type]] $TypeAccelerators = [psobject].Assembly.GetType('System.Management.Automation.TypeAccelerators')::get [System.Collections.Generic.Dictionary[type, string]] $TypeAcceleratorsLookup = New-Object 'System.Collections.Generic.Dictionary[type,string]' foreach ($TypeAcceleratorKey in $TypeAccelerators.Keys) { if (!$TypeAcceleratorsLookup.ContainsKey($TypeAccelerators[$TypeAcceleratorKey])) { $TypeAcceleratorsLookup.Add($TypeAccelerators[$TypeAcceleratorKey], $TypeAcceleratorKey) } } } function Resolve-Type { param ( # [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] [type] $ObjectType, # [Parameter(Mandatory = $false, Position = 1)] [switch] $Compact, # [Parameter(Mandatory = $false, Position = 1)] [type[]] $RemoveTypes ) [string] $OutputString = '' if ($ObjectType.IsGenericType -or ($ObjectType.BaseType -and $ObjectType.BaseType.IsGenericType)) { if (!$ObjectType.IsGenericType) { $ObjectType = $ObjectType.BaseType } if ($ObjectType.FullName.StartsWith('System.Collections.Generic.Dictionary')) { #$OutputString += '[hashtable]' if ($Compact) { $OutputString += '(Invoke-Command { $D = New-Object ''Collections.Generic.Dictionary[' } else { $OutputString += '(Invoke-Command { $D = New-Object ''System.Collections.Generic.Dictionary[' } $iInput = 0 foreach ($GenericTypeArgument in $ObjectType.GenericTypeArguments) { if ($iInput -gt 0) { $OutputString += ',' } $OutputString += Resolve-Type $GenericTypeArgument -Compact:$Compact -RemoveTypes @() $iInput++ } $OutputString += ']''' } elseif ($InputObject.GetType().FullName -match '^(System.(Collections.Generic.[a-zA-Z]+))`[0-9]\[(?:\[(.+?), .+?, Version=.+?, Culture=.+?, PublicKeyToken=.+?\],?)+?\]$') { if ($Compact) { $OutputString += '[{0}[' -f $Matches[2] } else { $OutputString += '[{0}[' -f $Matches[1] } $iInput = 0 foreach ($GenericTypeArgument in $ObjectType.GenericTypeArguments) { if ($iInput -gt 0) { $OutputString += ',' } $OutputString += Resolve-Type $GenericTypeArgument -Compact:$Compact -RemoveTypes @() $iInput++ } $OutputString += ']]' } } elseif ($ObjectType -eq [System.Collections.Specialized.OrderedDictionary]) { $OutputString += '[ordered]' # Explicit cast does not work with full name. Only [ordered] works. } elseif ($ObjectType -eq [System.Management.Automation.PSCustomObject]) { $OutputString += '[pscustomobject]' # Explicit cast does not work with full name. Only [pscustomobject] works. } elseif ($Compact) { if ($ObjectType -notin $RemoveTypes) { if ($TypeAcceleratorsLookup.ContainsKey($ObjectType)) { $OutputString += '[{0}]' -f $TypeAcceleratorsLookup[$ObjectType] } elseif ($ObjectType.FullName.StartsWith('System.')) { $OutputString += '[{0}]' -f $ObjectType.FullName.Substring(7) } else { $OutputString += '[{0}]' -f $ObjectType.FullName } } } else { $OutputString += '[{0}]' -f $ObjectType.FullName } return $OutputString } function GetPsString ($InputObject) { $OutputString = New-Object System.Text.StringBuilder if ($null -eq $InputObject) { [void]$OutputString.Append('$null') } else { ## Add Casting [void]$OutputString.Append((Resolve-Type $InputObject.GetType() -Compact:$Compact -RemoveTypes $RemoveTypes)) ## Add Value switch ($InputObject.GetType()) { { $_.Equals([String]) } { [void]$OutputString.AppendFormat("'{0}'", $InputObject.Replace("'", "''")) #.Replace('"','`"') break } { $_.Equals([Char]) } { [void]$OutputString.AppendFormat("'{0}'", ([string]$InputObject).Replace("'", "''")) break } { $_.Equals([Boolean]) -or $_.Equals([switch]) } { [void]$OutputString.AppendFormat('${0}', $InputObject) break } { $_.Equals([DateTime]) } { [void]$OutputString.AppendFormat("'{0}'", $InputObject.ToString('O')) break } { $_.Equals([guid]) -or $_.Equals([version]) } { [void]$OutputString.AppendFormat("'{0}'", $InputObject) break } { $PSVersionTable.PSVersion -ge [version]'6.0' -and $_.Equals([semver]) } { [void]$OutputString.AppendFormat("'{0}'", $InputObject) break } { $_.BaseType -and $_.BaseType.Equals([Enum]) } { [void]$OutputString.AppendFormat('::{0}', $InputObject) break } { $_.BaseType -and $_.BaseType.Equals([ValueType]) } { [void]$OutputString.AppendFormat('{0}', $InputObject) break } { $_.BaseType.Equals([System.IO.FileSystemInfo]) -or $_.Equals([System.Uri]) } { [void]$OutputString.AppendFormat("'{0}'", $InputObject.ToString().Replace("'", "''")) #.Replace('"','`"') break } { $_.Equals([System.Xml.XmlDocument]) } { [void]$OutputString.AppendFormat("'{0}'", $InputObject.OuterXml.Replace("'", "''")) #.Replace('"','""') break } { $_.Equals([Hashtable]) -or $_.Equals([System.Collections.Specialized.OrderedDictionary]) } { [void]$OutputString.Append('@{') $iInput = 0 foreach ($enumHashtable in $InputObject.GetEnumerator()) { if ($iInput -gt 0) { [void]$OutputString.Append(';') } [void]$OutputString.AppendFormat('{0}={1}', (ConvertTo-PsString $enumHashtable.Key -Compact:$Compact -NoEnumerate), (ConvertTo-PsString $enumHashtable.Value -Compact:$Compact -NoEnumerate)) $iInput++ } [void]$OutputString.Append('}') break } { $_.FullName.StartsWith('System.Collections.Generic.Dictionary') -or ($_.BaseType -and $_.BaseType.FullName.StartsWith('System.Collections.Generic.Dictionary')) } { $iInput = 0 foreach ($enumHashtable in $InputObject.GetEnumerator()) { [void]$OutputString.AppendFormat('; $D.Add({0},{1})', (ConvertTo-PsString $enumHashtable.Key -Compact:$Compact -NoEnumerate), (ConvertTo-PsString $enumHashtable.Value -Compact:$Compact -NoEnumerate)) $iInput++ } [void]$OutputString.Append('; $D })') break } { $_.BaseType -and $_.BaseType.Equals([Array]) } { [void]$OutputString.Append('(Write-Output @(') $iInput = 0 for ($iInput = 0; $iInput -lt $InputObject.Count; $iInput++) { if ($iInput -gt 0) { [void]$OutputString.Append(',') } [void]$OutputString.Append((ConvertTo-PsString $InputObject[$iInput] -Compact:$Compact -RemoveTypes $InputObject.GetType().DeclaredMembers.Where( { $_.Name -eq 'Set' })[0].GetParameters()[1].ParameterType -NoEnumerate)) } [void]$OutputString.Append(') -NoEnumerate)') break } { $_.Equals([System.Collections.ArrayList]) } { [void]$OutputString.Append('@(') $iInput = 0 for ($iInput = 0; $iInput -lt $InputObject.Count; $iInput++) { if ($iInput -gt 0) { [void]$OutputString.Append(',') } [void]$OutputString.Append((ConvertTo-PsString $InputObject[$iInput] -Compact:$Compact -NoEnumerate)) } [void]$OutputString.Append(')') break } { $_.FullName.StartsWith('System.Collections.Generic.List') } { [void]$OutputString.Append('@(') $iInput = 0 for ($iInput = 0; $iInput -lt $InputObject.Count; $iInput++) { if ($iInput -gt 0) { [void]$OutputString.Append(',') } [void]$OutputString.Append((ConvertTo-PsString $InputObject[$iInput] -Compact:$Compact -RemoveTypes $_.GenericTypeArguments -NoEnumerate)) } [void]$OutputString.Append(')') break } ## Convert objects with object initializers { $_ -is [object] -and ($_.GetConstructors() | ForEach-Object { if ($_.IsPublic -and !$_.GetParameters()) { $true } }) } { [void]$OutputString.Append('@{') $iInput = 0 foreach ($Item in ($InputObject | Get-Member -MemberType Property, NoteProperty)) { if ($iInput -gt 0) { [void]$OutputString.Append(';') } $PropertyName = $Item.Name [void]$OutputString.AppendFormat('{0}={1}', (ConvertTo-PsString $PropertyName -Compact:$Compact -NoEnumerate), (ConvertTo-PsString $InputObject.$PropertyName -Compact:$Compact -NoEnumerate)) $iInput++ } [void]$OutputString.Append('}') break } { $_.Equals([System.Management.Automation.PSCustomObject]) } { [void]$OutputString.Append('@{') $iInput = 0 foreach ($Item in ($InputObject | Get-Member -MemberType Property, NoteProperty)) { if ($iInput -gt 0) { [void]$OutputString.Append(';') } $PropertyName = $Item.Name [void]$OutputString.AppendFormat('{0}={1}', (ConvertTo-PsString $PropertyName -Compact:$Compact -NoEnumerate), (ConvertTo-PsString $InputObject.$PropertyName -Compact:$Compact -NoEnumerate)) $iInput++ } [void]$OutputString.Append('}') break } Default { $Exception = New-Object ArgumentException -ArgumentList ('Cannot convert input of type {0} to PowerShell string.' -f $InputObject.GetType()) Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ParserError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'ConvertPowerShellStringFailureTypeNotSupported' -TargetObject $InputObject } } } if ($NoEnumerate) { $listOutputString.Add($OutputString.ToString()) } else { Write-Output $OutputString.ToString() } } if ($NoEnumerate) { $listOutputString = New-Object System.Collections.Generic.List[string] } } process { if ($PSCmdlet.MyInvocation.ExpectingInput -or $NoEnumerate -or $null -eq $InputObjects) { GetPsString $InputObjects } else { foreach ($InputObject in $InputObjects) { GetPsString $InputObject } } } end { if ($NoEnumerate) { if (($null -eq $InputObjects -and $listOutputString.Count -eq 0) -or $listOutputString.Count -gt 1) { Write-Warning ('To avoid losing strong type on outermost enumerable type when piping, use "Write-Output $Array -NoEnumerate | {0}".' -f $MyInvocation.MyCommand) $OutputArray = New-Object System.Text.StringBuilder [void]$OutputArray.Append('(Write-Output @(') if ($PSVersionTable.PSVersion -ge [version]'6.0') { [void]$OutputArray.AppendJoin(',', $listOutputString) } else { [void]$OutputArray.Append(($listOutputString -join ',')) } [void]$OutputArray.Append(') -NoEnumerate)') Write-Output $OutputArray.ToString() } else { Write-Output $listOutputString[0] } } } } #endregion #region ConvertTo-QueryString.ps1 <# .SYNOPSIS Convert Hashtable to Query String. .EXAMPLE PS >ConvertTo-QueryString @{ name = 'path/file.json'; index = 10 } Convert hashtable to query string. .EXAMPLE PS >[ordered]@{ title = 'convert&prosper'; id = [guid]'352182e6-9ab0-4115-807b-c36c88029fa4' } | ConvertTo-QueryString Convert ordered dictionary to query string. .INPUTS System.Collections.Hashtable .LINK https://github.com/jasoth/Utility.PS #> function ConvertTo-QueryString { [CmdletBinding()] [OutputType([string])] param ( # Value to convert [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [object] $InputObjects, # URL encode parameter names [Parameter(Mandatory = $false)] [switch] $EncodeParameterNames ) process { foreach ($InputObject in $InputObjects) { $QueryString = New-Object System.Text.StringBuilder if ($InputObject -is [System.Collections.IDictionary]) { foreach ($Item in $InputObject.GetEnumerator()) { if ($QueryString.Length -gt 0) { [void]$QueryString.Append('&') } [string] $ParameterName = $Item.Key if ($EncodeParameterNames) { $ParameterName = [System.Net.WebUtility]::UrlEncode($ParameterName) } [void]$QueryString.AppendFormat('{0}={1}', $ParameterName, [System.Net.WebUtility]::UrlEncode($Item.Value)) } } elseif ($InputObject -is [object] -and $InputObject -isnot [ValueType]) { foreach ($Item in ($InputObject | Get-Member -MemberType Property, NoteProperty)) { if ($QueryString.Length -gt 0) { [void]$QueryString.Append('&') } [string] $ParameterName = $Item.Name if ($EncodeParameterNames) { $ParameterName = [System.Net.WebUtility]::UrlEncode($ParameterName) } [void]$QueryString.AppendFormat('{0}={1}', $ParameterName, [System.Net.WebUtility]::UrlEncode($InputObject.($Item.Name))) } } else { ## Non-Terminating Error $Exception = New-Object ArgumentException -ArgumentList ('Cannot convert input of type {0} to query string.' -f $InputObject.GetType()) Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ParserError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'ConvertQueryStringFailureTypeNotSupported' -TargetObject $InputObject continue } Write-Output $QueryString.ToString() } } } #endregion #region ConvertTo-UrlString.ps1 <# .SYNOPSIS Convert string to URL encoded string. .EXAMPLE PS >ConvertTo-UrlString 'A string with url encoding' Convert string to URL encoded string. .INPUTS System.String .LINK https://github.com/jasoth/Utility.PS #> function ConvertTo-UrlString { [CmdletBinding()] [OutputType([string])] param ( # Value to convert [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [string[]] $InputStrings ) process { foreach ($InputString in $InputStrings) { Write-Output ([System.Net.WebUtility]::UrlEncode($InputString)) } } } #endregion #region Expand-Data.ps1 <# .SYNOPSIS Decompress data using DEFLATE (RFC 1951) or GZIP file format (RFC 1952). .EXAMPLE PS >[byte[]] $byteArray = @(115,84,40,46,41,202,204,75,87,72,203,47,82,72,206,207,45,40,74,45,46,206,204,207,3,0) PS >Expand-Data $byteArray Decompress string using Deflate. .INPUTS System.String .LINK https://github.com/jasoth/Utility.PS #> function Expand-Data { [CmdletBinding()] [Alias('Decompress-Data')] [Alias('Inflate-Data')] [OutputType([string], [byte[]])] param ( # Value to convert [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [object] $InputObjects, # Input is gzip file format [Parameter(Mandatory = $false)] [switch] $GZip, # Output raw byte array [Parameter (Mandatory = $false)] [switch] $RawBytes, # Encoding to use for text strings [Parameter (Mandatory = $false)] [ValidateSet('Ascii', 'UTF32', 'UTF7', 'UTF8', 'BigEndianUnicode', 'Unicode')] [string] $Encoding = 'Default' ) begin { function Expand ([byte[]]$InputBytes) { try { $streamOutput = New-Object System.IO.MemoryStream try { $streamInput = New-Object System.IO.MemoryStream -ArgumentList @($InputBytes, $false) try { if ($GZip) { $streamCompression = New-Object System.IO.Compression.GZipStream -ArgumentList $streamInput, ([System.IO.Compression.CompressionMode]::Decompress) } else { $streamCompression = New-Object System.IO.Compression.DeflateStream -ArgumentList $streamInput, ([System.IO.Compression.CompressionMode]::Decompress) } $streamCompression.CopyTo($streamOutput) } catch { Write-Error -Exception $_.Exception.InnerException -Category ([System.Management.Automation.ErrorCategory]::InvalidData) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'ExpandDataFailureInvalidData' -TargetObject $InputBytes -ErrorAction Stop } finally { $streamCompression.Dispose() } [byte[]] $OutputBytes = $streamOutput.ToArray() } finally { $streamInput.Dispose() } } finally { $streamOutput.Dispose() } Write-Output $OutputBytes } ## Create list to capture byte stream from piped input. [System.Collections.Generic.List[byte]] $listBytes = New-Object System.Collections.Generic.List[byte] } process { if ($InputObjects -is [byte[]]) { [byte[]] $outBytes = Expand $InputObjects if ($RawBytes) { Write-Output $outBytes -NoEnumerate } else { [string] $outString = ([Text.Encoding]::$Encoding.GetString($outBytes)) Write-Output $outString } } else { foreach ($InputObject in $InputObjects) { [byte[]] $InputBytes = $null if ($InputObject -is [byte]) { ## Populate list with byte stream from piped input. if ($listBytes.Count -eq 0) { Write-Verbose 'Creating byte array from byte stream.' Write-Warning ('For better performance when piping a single byte array, use "Write-Output $byteArray -NoEnumerate | {0}".' -f $MyInvocation.MyCommand) } $listBytes.Add($InputObject) } elseif ($InputObject -is [byte[]]) { $InputBytes = $InputObject } elseif ($InputObject -is [bool] -or $InputObject -is [char] -or $InputObject -is [single] -or $InputObject -is [double] -or $InputObject -is [int16] -or $InputObject -is [int32] -or $InputObject -is [int64] -or $InputObject -is [uint16] -or $InputObject -is [uint32] -or $InputObject -is [uint64]) { $InputBytes = [System.BitConverter]::GetBytes($InputObject) } elseif ($InputObject -is [System.IO.FileSystemInfo]) { if ($PSVersionTable.PSVersion -ge [version]'6.0') { $InputBytes = Get-Content $InputObject.FullName -Raw -AsByteStream } else { $InputBytes = Get-Content $InputObject.FullName -Raw -Encoding Byte } } else { ## Non-Terminating Error $Exception = New-Object ArgumentException -ArgumentList ('Cannot compress input of type {0}.' -f $InputObject.GetType()) Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ParserError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'CompressDataFailureTypeNotSupported' -TargetObject $InputObject } if ($null -ne $InputBytes -and $InputBytes.Count -gt 0) { [byte[]] $outBytes = Expand $InputBytes if ($RawBytes) { Write-Output $outBytes -NoEnumerate } else { [string] $outString = ([Text.Encoding]::$Encoding.GetString($outBytes)) Write-Output $outString } } } } } end { ## Output captured byte stream from piped input. if ($listBytes.Count -gt 0) { [byte[]] $outBytes = Expand $listBytes.ToArray() if ($RawBytes) { Write-Output $outBytes -NoEnumerate } else { [string] $outString = ([Text.Encoding]::$Encoding.GetString($outBytes)) Write-Output $outString } } } } #endregion #region Format-DataSize.ps1 <# .SYNOPSIS Format data size in bytes to human readable format. .EXAMPLE PS >Format-DataSize 123 Format 123 bytes to "123.0 Bytes". .EXAMPLE PS >Format-DataSize 1234567890 Format 1234567890 bytes to "1.150 GB". .INPUTS System.Int64 .LINK https://github.com/jasoth/Utility.PS #> function Format-DataSize { [CmdletBinding()] [Alias('Format-FileSize')] [OutputType([string])] param ( # Specifies the number of bytes to auto scale and format. [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] [long] $Bytes ) begin { ## Adapted From: ## https://github.com/PowerShell/PowerShell/blob/80b5df4b7f6e749e34a2363e1ef6cc09f2761c89/src/System.Management.Automation/engine/Utils.cs#L1489 function DisplayHumanReadableFileSize([long] $bytes) { switch ($bytes) { { $_ -lt 1024 -and $_ -ge 0 } { return "{0:0.0} Bytes" -f $bytes } { $_ -lt 1048576 -and $_ -ge 1024 } { return "{0:0.0} KB" -f ($bytes / 1024) } { $_ -lt 1073741824 -and $_ -ge 1048576 } { return "{0:0.0} MB" -f ($bytes / 1048576) } { $_ -lt 1099511627776 -and $_ -ge 1073741824 } { return "{0:0.000} GB" -f ($bytes / 1073741824) } { $_ -lt 1125899906842624 -and $_ -ge 1099511627776 } { return "{0:0.00000} TB" -f ($bytes / 1099511627776) } { $_ -lt 1152921504606847000 -and $_ -ge 1125899906842624 } { return "{0:0.0000000} PB" -f ($bytes / 1125899906842624) } { $_ -ge 1152921504606847000 } { return "{0:0.000000000} EB" -f ($bytes / 1152921504606847000 ) } Default { return "0 Bytes" } } } } process { foreach ($Byte in $Bytes) { DisplayHumanReadableFileSize $Byte } } } #endregion #region Format-NumberWithMetricUnit.ps1 <# .SYNOPSIS Format number in different metric unit of measure. .EXAMPLE PS >Format-NumberWithMetricUnit 1234 -Unit 'byte(s)' Format number in kilobytes. .EXAMPLE PS >Format-NumberWithMetricUnit 12345678 -Unit 'B' Format number in megabytes. .EXAMPLE PS >Format-NumberWithMetricUnit 1234 'kilobyte(s)' Format number in megabytes. .EXAMPLE PS >Format-NumberWithMetricUnit 12345678 'KB' -TargetUnit 'MB' Format number in megabytes. .EXAMPLE PS >Format-NumberWithMetricUnit 1234 'bit(s)' Format number in kilobits. .EXAMPLE PS >Format-NumberWithMetricUnit 1234 'm' Format number in kilometers. .INPUTS System.Double .LINK https://github.com/jasoth/Utility.PS #> function Format-NumberWithMetricUnit { [CmdletBinding()] [OutputType([string])] param ( # Number to scale [Parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true)] [double] $Number, # Unit of Number [Parameter(Mandatory = $true, Position = 2)] [string] $Unit, # Target Unit of Number [Parameter(Mandatory = $false)] [string] $TargetUnit ) begin { $mapMetricSymbol = @{ -8 = 'y' -7 = 'z' -6 = 'a' -5 = 'f' -4 = 'p' -3 = 'n' -2 = 'µ' -1 = 'm' 0 = '' 1 = 'k' 2 = 'M' 3 = 'G' 4 = 'T' 5 = 'P' 6 = 'E' 7 = 'Z' 8 = 'Y' } $mapMetricPrefix = New-Object hashtable @{ -8 = 'yocto' -7 = 'zepto' -6 = 'atto' -5 = 'femto' -4 = 'pico' -3 = 'nano' -2 = 'micro' -1 = 'milli' 0 = '' 1 = 'kilo' 2 = 'mega' 3 = 'giga' 4 = 'tera' 5 = 'peta' 6 = 'exa' 7 = 'zetta' 8 = 'yotta' } # $mapMetricToExponent = @{ # #'y' = -8 # #'z' = -7 # 'a' = -6 # 'f' = -5 # #'p' = -4 # 'n' = -3 # 'µ' = -2 # #'m' = -1 # 'yocto' = -8 # 'zepto' = -7 # 'atto' = -6 # 'femto' = -5 # 'pico' = -4 # 'nano' = -3 # 'micro' = -2 # 'milli' = -1 # '' = 0 # 'kilo' = 1 # 'mega' = 2 # 'giga' = 3 # 'tera' = 4 # 'peta' = 5 # 'exa' = 6 # 'zetta' = 7 # 'yotta' = 8 # 'k' = 1 # 'M' = 2 # 'G' = 3 # 'T' = 4 # 'P' = 5 # 'E' = 6 # 'Z' = 7 # 'Y' = 8 # } # This method of adding hashtable method uses case-sensitive lookups $mapMetricToExponent = New-Object hashtable $mapMetricToExponent.Add('y', -8) $mapMetricToExponent.Add('z', -7) $mapMetricToExponent.Add('a', -6) $mapMetricToExponent.Add('f', -5) $mapMetricToExponent.Add('p', -4) $mapMetricToExponent.Add('n', -3) $mapMetricToExponent.Add('µ', -2) $mapMetricToExponent.Add('m', -1) $mapMetricToExponent.Add('yocto', -8) $mapMetricToExponent.Add('zepto', -7) $mapMetricToExponent.Add('atto', -6) $mapMetricToExponent.Add('femto', -5) $mapMetricToExponent.Add('pico', -4) $mapMetricToExponent.Add('nano', -3) $mapMetricToExponent.Add('micro', -2) $mapMetricToExponent.Add('milli', -1) $mapMetricToExponent.Add('', 0) $mapMetricToExponent.Add('kilo', 1) $mapMetricToExponent.Add('mega', 2) $mapMetricToExponent.Add('giga', 3) $mapMetricToExponent.Add('tera', 4) $mapMetricToExponent.Add('peta', 5) $mapMetricToExponent.Add('exa', 6) $mapMetricToExponent.Add('zetta', 7) $mapMetricToExponent.Add('yotta', 8) $mapMetricToExponent.Add('k', 1) $mapMetricToExponent.Add('M', 2) $mapMetricToExponent.Add('G', 3) $mapMetricToExponent.Add('T', 4) $mapMetricToExponent.Add('P', 5) $mapMetricToExponent.Add('E', 6) $mapMetricToExponent.Add('Z', 7) $mapMetricToExponent.Add('Y', 8) } process { if ($Unit -match '^(yocto|zepto|atto|femto|pico|nano|micro|milli|centi|deci|deca|hecto|kilo|mega|giga|tera|peta|exa|zetta|yotta|[yzafpnµmcdhkMGTPEZY](?=[A-Z]$|(?-i:[A-Z])))?(.*)$') { $UnitPrefix = if ($Matches[1] -in 'M', 'P', 'Z', 'Y') { $Matches[1] } elseif ($Matches[1] -in 'G', 'T', 'E') { $Matches[1].ToUpper() } elseif ($Matches[1]) { $Matches[1].ToLower() } else { '' } $UnitName = $Matches[2] } if ($UnitName.StartsWith('Byte', [System.StringComparison]::OrdinalIgnoreCase) -or $UnitName -ceq 'B') { $Base = 1024 } else { $Base = 1000 } [int] $SourceExponent = $mapMetricToExponent[$UnitPrefix] [int] $AutoExponent = 0 if ($TargetUnit) { if ($TargetUnit -match '^(yocto|zepto|atto|femto|pico|nano|micro|milli|centi|deci|deca|hecto|kilo|mega|giga|tera|peta|exa|zetta|yotta|[yzafpnµmcdhkMGTPEZY](?=[A-Z]$|(?-i:[A-Z])))?(.*)$') { [string] $TargetUnitPrefix = $null $TargetUnitPrefix = if ($Matches[1] -in 'M', 'P', 'Z', 'Y') { $Matches[1] } elseif ($Matches[1] -in 'G', 'T', 'E') { $Matches[1].ToUpper() } elseif ($Matches[1]) { $Matches[1].ToLower() } else { '' } #$TargetUnitName = $Matches[2] $AutoExponent = $mapMetricToExponent[$TargetUnitPrefix] - $SourceExponent } } else { $AutoExponent = [System.Math]::Floor([System.Math]::Log($Number) / [System.Math]::Log($Base)) # Fails when number is negative if ($UnitName.Length -le 2) { $TargetUnit = $mapMetricSymbol[($SourceExponent + $AutoExponent)] + $UnitName } else { $TargetUnit = $mapMetricPrefix[($SourceExponent + $AutoExponent)] + $UnitName } } [double] $ScaledNumber = $Number / [System.Math]::Pow($Base, $AutoExponent) Write-Output ('{0:0.##} {1}' -f $ScaledNumber, $TargetUnit) } } #endregion #region Format-PropertyValue.ps1 <# .SYNOPSIS Format objects as readable strings for CSV output. .EXAMPLE PS > Format-PropertyValue @{ NestedHashtables = @{ lvl1 = @{ lvl2 = @('value1','value2') } }; Array = @('value1','value2') } -SingleLineOutput Format property values to a single line string for CSV output. .INPUTS System.Object .LINK https://github.com/jasoth/Utility.PS #> function Format-PropertyValue { [CmdletBinding()] [OutputType([psobject])] param ( # Specifies the objects that are converted to CSV strings. [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [psobject[]] $InputObject, # Property names to include in the output. [Parameter(Mandatory = $false, Position = 1)] [string[]] $Property, # Specify how objects are represented in the output. [Parameter(Mandatory = $false)] [ValidateSet('PsFormat', 'PsFormatExpression', 'ToString', 'Json', 'Html')] [string] $ObjectFormat, # Determines how many items are enumerated. Set to 0 to enumerate all items. Default is $global:FormatEnumerationLimit. [Parameter(Mandatory = $false)] [int] $EnumerationLimit = $global:FormatEnumerationLimit, # Specifies how many levels of nested objects are included. This does not apply to ToString object representation. [Parameter(Mandatory = $false)] [ValidateRange(0, 100)] [int] $Depth = 1, # Formatted output is a single line. [Parameter(Mandatory = $false)] [switch] $SingleLineOutput, # Format root level object properties in addition to nested properties. [Parameter(Mandatory = $false)] [switch] $FormatRootLevelProperties ) begin { ## Initalize variables $TopLevelTypes = [string], [ValueType], [version] if ($PSVersionTable.PSVersion -ge '6.0') { $TopLevelTypes += [semver] } $PrevFormatEnumerationLimit = $global:FormatEnumerationLimit $NewLineReplacement = '; ' $ArrayDelimiter = "`r`n" $PsFormatWidth = 2147483646 ## Set default values if (!$ObjectFormat) { if ($SingleLineOutput) { $ObjectFormat = 'ToString' } else { $ObjectFormat = 'PsFormat' } } ## ToDo: Find a way to replicate SmartToString function to replace current PsFormatExpression/ToString object representation implementation. # https://github.com/PowerShell/PowerShell/blob/08baf27b80e604d1685c065ea75761508634de12/src/System.Management.Automation/FormatAndOutput/common/Utilities/MshObjectUtil.cs#L213 function IsTopLevelType ($InputObject) { foreach ($TopLevelType in $TopLevelTypes) { if ($InputObject -is $TopLevelType) { return $true } } return $false } function TransformObject ($InputObject, [string]$ObjectFormat) { if ($InputObject -is [System.Collections.IList]) { $Truncated = $false if ($EnumerationLimit -gt 0 -and $InputObject.Count -gt $EnumerationLimit) { $InputObject = $InputObject[0..($EnumerationLimit - 1)] + '...'; $Truncated = $true } # if (IsTopLevelType $InputObject[0]) { # $OutputObject = $InputObject -join $ArrayDelimiter # } } if ($ObjectFormat -eq 'Json') { $JsonDepth = if ($FormatRootLevelProperties -or $Depth -eq 0) { $Depth } else { $Depth - 1 } $OutputObject = $InputObject | ConvertTo-Json -Depth $JsonDepth -Compress:$SingleLineOutput } elseif ($ObjectFormat -eq 'Html') { if ($InputObject -is [System.Collections.IList]) { if (IsTopLevelType $InputObject[0]) { $OutputObject = $InputObject -join $ArrayDelimiter } else { if ($Truncated) { $InputObject = $InputObject[0..($InputObject.Count - 2)] } $OutputObject = $InputObject | ConvertTo-Html -Fragment -As Table if ($Truncated) { $OutputObject[-1] += '...' } } } else { if (IsTopLevelType $InputObject) { $OutputObject = $InputObject } else { $OutputObject = $InputObject | ConvertTo-Html -Fragment -As List } } if ($SingleLineOutput) { $OutputObject = $OutputObject -join "" } else { $OutputObject = $OutputObject -join "`r`n" } ## Decode inner HTML table tags #$OutputObject = [System.Net.WebUtility]::HtmlDecode($OutputObject) # Not ConstrainedLanguage safe $OutputObject = $OutputObject -replace '<(/?(?:table|th|tr|td|colgroup|col)/?)>', '<$1>' # Escape nested table tags $OutputObject = $OutputObject -replace '&(?=[a-zA-Z0-9]+;)', '&' # Decode HTML ampersands due to double encoding } elseif ($ObjectFormat -eq 'PsFormat') { try { if ($EnumerationLimit) { $global:FormatEnumerationLimit = $EnumerationLimit } if ($InputObject -is [System.Collections.IList]) { if (IsTopLevelType $InputObject[0]) { $OutputObject = $InputObject -join $ArrayDelimiter } else { $OutputObject = $InputObject | Format-Table -AutoSize | Out-String -Width $PsFormatWidth } } else { if (IsTopLevelType $InputObject) { $OutputObject = $InputObject } else { $OutputObject = $InputObject | Format-List | Out-String -Width $PsFormatWidth } } } finally { if ($EnumerationLimit) { $global:FormatEnumerationLimit = $PrevFormatEnumerationLimit } } ## Remove trailing new line from Out-String $OutputObject = $OutputObject.Trim("`r", "`n") } elseif ($ObjectFormat -eq 'ToString') { if ($InputObject -is [System.Collections.IList]) { ## Expand array items [array]$ArrayObject = New-Object -TypeName object[] -ArgumentList $InputObject.Count # ConstrainedLanguage safe for ($i = 0; $i -lt $InputObject.Count; $i++) { $ArrayObject[$i] = $InputObject[$i].ToString() if (!$ArrayObject[$i]) { $ArrayObject[$i] = $InputObject[$i].psobject.TypeNames[0] } } $OutputObject = $ArrayObject -join $ArrayDelimiter } else { $OutputObject = $InputObject.ToString() if (!$OutputObject) { $OutputObject = $InputObject.psobject.TypeNames[0] } } } else { $OutputObject = $InputObject } return $OutputObject } function Transform ($InputObject, [int]$CurrentDepth = 0) { if ($InputObject) { if ($InputObject -is [string]) { $OutputObject = $InputObject.ToString() } elseif ($InputObject -is [DateTime]) { $OutputObject = $InputObject.ToString("o") } elseif (IsTopLevelType $InputObject) { $OutputObject = $InputObject.ToString() } elseif ($InputObject -is [System.Collections.IDictionary]) { ## Convert hashtables to PSObjects and shallow copy object $OutputObject = New-Object -TypeName PSObject -Property $InputObject # ConstrainedLanguage safe if ($ObjectFormat -in 'Html', 'PsFormat', 'PsFormatExpression' -and $CurrentDepth -lt $Depth) { foreach ($Key in $InputObject.Keys) { $OutputObject.$Key = Transform $InputObject[$Key] ($CurrentDepth + 1) } } $OutputObject = TransformObject $OutputObject -ObjectFormat $ObjectFormat } elseif ($InputObject -is [System.Collections.ICollection]) { ## Shallow copy array [array]$OutputObject = $InputObject | ForEach-Object { $_ } # ConstrainedLanguage safe if ($ObjectFormat -in 'Html', 'PsFormat', 'PsFormatExpression' -and $CurrentDepth -lt $Depth) { # foreach ($_OutputObject in $OutputObject) { # $_OutputObject = Transform $_OutputObject ($CurrentDepth + 1) # } # for ($i = 0; $i -lt $InputObject.Count; $i++) { # $OutputObject[$i] = Transform $InputObject[$i] ($CurrentDepth + 1) # } for ($i = 0; $i -lt $OutputObject.Count; $i++) { $OutputObject[$i] = Transform $OutputObject[$i] ($CurrentDepth + 1) } } $OutputObject = TransformObject $OutputObject -ObjectFormat $ObjectFormat } elseif ($InputObject -is [psobject]) { ## Shallow copy PSObject $OutputObject = Select-Object -InputObject $InputObject -Property "*" # ConstrainedLanguage safe if ($ObjectFormat -in 'Html', 'PsFormat', 'PsFormatExpression' -and $CurrentDepth -lt $Depth) { foreach ($objProperty in $InputObject.psobject.Properties) { $PropertyName = $objProperty.Name $OutputObject.$PropertyName = Transform $objProperty.Value ($CurrentDepth + 1) } } $OutputObject = TransformObject $OutputObject -ObjectFormat $ObjectFormat } else { $OutputObject = $InputObject.ToString() } if ($SingleLineOutput) { $OutputObject = $OutputObject -replace '[\r\n]+', $NewLineReplacement } return $OutputObject } return $InputObject } } process { foreach ($_InputObject in $InputObject) { if ($_InputObject -is [string]) { $OutputObject = $_InputObject if ($SingleLineOutput) { $OutputObject = $OutputObject -replace '[\r\n]+', $NewLineReplacement } } elseif ($_InputObject -is [System.ValueType]) { $OutputObject = Transform $_InputObject if ($SingleLineOutput) { $OutputObject = $OutputObject -replace '[\r\n]+', $NewLineReplacement } } elseif ($_InputObject -is [System.Collections.IDictionary] -or $_InputObject -is [psobject]) { if ($_InputObject -is [System.Collections.IDictionary]) { ## Convert IDictionary to PSObject $_InputObject = New-Object -TypeName PSObject -Property $_InputObject # ConstrainedLanguage safe } if ($Property) { $OutputObject = Select-Object -InputObject $_InputObject -Property $Property } else { $OutputObject = Select-Object -InputObject $_InputObject -Property "*" } if ($FormatRootLevelProperties) { $OutputObject = Transform $OutputObject -CurrentDepth 0 } else { foreach ($objProperty in $OutputObject.psobject.Properties) { $PropertyName = $objProperty.Name if (!$Property -or $objProperty.Name -in $Property) { $OutputObject.$PropertyName = Transform $objProperty.Value -CurrentDepth 1 } } } } else { $OutputObject = Transform $_InputObject if ($SingleLineOutput) { $OutputObject = $OutputObject -replace '[\r\n]+', $NewLineReplacement } } try { if ($EnumerationLimit) { $global:FormatEnumerationLimit = $EnumerationLimit } Write-Output $OutputObject } finally { if ($EnumerationLimit) { $global:FormatEnumerationLimit = $PrevFormatEnumerationLimit } } } } } #endregion #region Get-ContentEncoding.ps1 <# .SYNOPSIS Get content encoding of byte array for file .EXAMPLE PS >Get-ContentEncoding ([byte[]](0xFE, 0xFF, 0x00, 0x00)) Get content encoding of byte array. .EXAMPLE PS >Get-ContentEncoding 'file.txt' Get content encoding of file. .INPUTS System.Object .LINK https://github.com/jasoth/Utility.PS #> function Get-ContentEncoding { [CmdletBinding()] [OutputType([pscustomobject])] param ( # Content represented as byte array [Parameter(Mandatory = $true, ParameterSetName = 'ByteArray', Position = 0, ValueFromPipeline = $true)] [byte[]] $InputBytes, # Content file path [Parameter(Mandatory = $true, ParameterSetName = 'File', Position = 0)] [string] $Path, # Number of bytes to read from beginning of file [Parameter(Mandatory = $false, ParameterSetName = 'File')] [int] $NumberOfBytesToRead = 8000 ) begin { function New-ContentEncodingOutput ($Encoding) { $Output = [pscustomobject][ordered]@{ CodePage = $null Name = $null DisplayName = $null TextEncoding = $null } if ($Encoding -is [System.Text.Encoding]) { $Output.CodePage = $Encoding.CodePage $Output.Name = $Encoding.WebName $Output.DisplayName = $Encoding.EncodingName $Output.TextEncoding = $Encoding } else { $Output.Name = $Output.DisplayName = $Encoding } return $Output } $CriticalError = $null if ($PSCmdlet.ParameterSetName -eq 'File') { [byte[]] $InputBytes = $null if (Resolve-Path $Path -ErrorVariable CriticalError) { #Get-Content $Path -AsByteStream -ReadCount $NumberOfBytesToRead $FileStream = [System.IO.File]::OpenRead($Path) try { $BinaryReader = New-Object System.IO.BinaryReader -ArgumentList $FileStream $InputBytes = $BinaryReader.ReadBytes($NumberOfBytesToRead) } finally { $FileStream.Close() } } } ## Intialize $EncodingInfo = [System.Text.Encoding]::GetEncodings() [System.Collections.Generic.List[System.Text.Encoding]] $listEncodings = $EncodingInfo.GetEncoding() | Where-Object { $_.GetPreamble() } | Sort-Object { $_.GetPreamble().Count } -Descending [int] $MaxPreambleCount = $listEncodings[0].GetPreamble().Count Set-Variable NullByte -Option Constant -Value ([byte]0x00) [bool] $ContainsNull = $false [int] $Position = 0 } process { if ($CriticalError) { return } foreach ($byte in $InputBytes) { ## Break out of loop if null was found and any potential preambles are complete if ($ContainsNull -eq $true -and $Position -ge $MaxPreambleCount) { return } ## Check for BOM preamble to determine text encoding if ($Position -lt $MaxPreambleCount) { for ($i = 0; $i -lt $listEncodings.Count; $i++) { [byte[]]$Preamble = $listEncodings[$i].GetPreamble() if ($Position -lt $Preamble.Count -and $byte -ne $Preamble[$Position]) { $listEncodings.RemoveAt($i) $i-- } } } ## Check for null byte as it could mean binary data if ($byte.Equals($NullByte)) { $ContainsNull = $true } ## Advance position $Position++ } } end { if ($CriticalError) { return } ## Produce output object if ($listEncodings.Count -gt 0) { New-ContentEncodingOutput $listEncodings[0] } elseif ($ContainsNull) { New-ContentEncodingOutput 'Binary' } else { New-ContentEncodingOutput 'Unknown' } } } #endregion #region Get-PropertyValue.ps1 <# .SYNOPSIS Get object property value in a manner that satifies strict mode. .EXAMPLE PS >$object = New-Object psobject -Property @{ title = 'title value' } PS >$object | Get-PropertyValue -Property 'title' Get value of object property named title. .EXAMPLE PS >$object = New-Object psobject -Property @{ lvl1 = (New-Object psobject -Property @{ nextLevel = 'lvl2 data' }) } PS >Get-PropertyValue $object -Property 'lvl1', 'nextLevel' Get value of nested object property named nextLevel. .INPUTS System.Collections.IDictionary System.Management.Automation.PSObject .LINK https://github.com/jasoth/Utility.PS #> function Get-PropertyValue { [CmdletBinding()] [OutputType([psobject])] param ( # Object containing property values [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [AllowNull()] [psobject] $InputObjects, # Name of property. Specify an array of property names to tranverse nested objects. [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)] [string[]] $Property ) process { foreach ($InputObject in $InputObjects) { for ($iProperty = 0; $iProperty -lt $Property.Count; $iProperty++) { ## Get property value if ($InputObject -is [System.Collections.IDictionary]) { if ($InputObject.Contains($Property[$iProperty])) { $PropertyValue = $InputObject[$Property[$iProperty]] } else { $PropertyValue = $null } } else { $PropertyValue = Select-Object -InputObject $InputObject -ExpandProperty $Property[$iProperty] -ErrorAction Ignore } ## Check for more nested properties if ($iProperty -lt $Property.Count - 1) { $InputObject = $PropertyValue if ($null -eq $InputObject) { break } } else { Write-Output $PropertyValue } } } } } #endregion #region Get-RelativePath.ps1 <# .SYNOPSIS Get path relative to working directory. .EXAMPLE PS >Get-RelativePath 'C:\DirectoryA\File1.txt' Get path relative to current directory. .EXAMPLE PS >Get-RelativePath 'C:\DirectoryA\File1.txt' -WorkingDirectory 'C:\DirectoryB' -CompareCase Get path relative to specified working directory with case-sensitive directory comparison. .INPUTS System.String .LINK https://github.com/jasoth/Utility.PS #> function Get-RelativePath { [CmdletBinding()] [OutputType([string])] param ( # Input paths [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] [string[]] $InputObjects, # Working directory for relative paths. Default is current directory. [Parameter(Mandatory = $false, Position = 2)] [string] $WorkingDirectory = (Get-Location).ProviderPath, # Compare directory names as case-sensitive. [Parameter(Mandatory = $false)] [switch] $CompareCase, # Directory separator used in paths. [Parameter(Mandatory = $false)] [char] $DirectorySeparator = [System.IO.Path]::DirectorySeparatorChar ) begin { ## Adapted From: ## https://github.com/dotnet/runtime/blob/6072e4d3a7a2a1493f514cdf4be75a3d56580e84/src/libraries/System.Private.Uri/src/System/Uri.cs#L5037 function PathDifference([string] $path1, [string] $path2, [bool] $compareCase, [char] $directorySeparator = [System.IO.Path]::DirectorySeparatorChar) { [int] $i = 0 [int] $si = -1 for ($i = 0; ($i -lt $path1.Length) -and ($i -lt $path2.Length); $i++) { if (($path1[$i] -cne $path2[$i]) -and ($compareCase -or ([char]::ToLowerInvariant($path1[$i]) -cne [char]::ToLowerInvariant($path2[$i])))) { break } elseif ($path1[$i] -ceq $directorySeparator) { $si = $i } } if ($i -ceq 0) { return $path2 } if (($i -ceq $path1.Length) -and ($i -ceq $path2.Length)) { return [string]::Empty } [System.Text.StringBuilder] $relPath = New-Object System.Text.StringBuilder ## Walk down several dirs for (; $i -lt $path1.Length; $i++) { if ($path1[$i] -ceq $directorySeparator) { [void] $relPath.Append("..$directorySeparator") } } ## Same path except that path1 ended with a file name and path2 didn't if ($relPath.Length -ceq 0 -and $path2.Length - 1 -ceq $si) { return ".$directorySeparator" ## Truncate the file name } return $relPath.Append($path2.Substring($si + 1)).ToString() } } process { foreach ($InputObject in $InputObjects) { if (!$WorkingDirectory.EndsWith($DirectorySeparator)) { $WorkingDirectory += $DirectorySeparator } [string] $RelativePath = '.{0}{1}' -f $DirectorySeparator, (PathDifference $WorkingDirectory $InputObject $CompareCase $DirectorySeparator) Write-Output $RelativePath } } } #endregion #region Get-StrictModeVersion.ps1 <# .SYNOPSIS Get the strict mode version of the current session scope. .DESCRIPTION Get the strict mode version of the current session scope. 1.0 Prohibits references to uninitialized variables, except for uninitialized variables in strings. 2.0 Prohibits references to uninitialized variables. This includes uninitialized variables in strings. Prohibits references to non-existent properties of an object. Prohibits function calls that use the syntax for calling methods. 3.0 Prohibits references to uninitialized variables. This includes uninitialized variables in strings. Prohibits references to non-existent properties of an object. Prohibits function calls that use the syntax for calling methods. Prohibit out of bounds or unresolvable array indexes. .EXAMPLE PS >Get-StrictModeVersion Get the strict mode version of the current session scope. .INPUTS None .LINK https://github.com/jasoth/Utility.PS #> function Get-StrictModeVersion { [CmdletBinding()] [OutputType([version])] param () try { $null = @()[0] } catch { return [version]'3.0' } try { $null = $null.NonExistentProperty } catch { return [version]'2.0' } try { $null = $UninitializedVariable } catch { return [version]'1.0' } return [version]'0.0' } #endregion #region Get-X509Certificate.ps1 <# .SYNOPSIS Get certificate object for X509 certificate. .EXAMPLE PS >[byte[]] $DERCert = @(48,130,4,18,48,130,2,250,160,3,2,1,2,2,15,0,193,0,139,60,60,136,17,209,62,246,99,236,223,64,48,13,6,9,42,134,72,134,247,13,1,1,4,5,0,48,112,49,43,48,41,6,3,85,4,11,19,34,67,111,112,121,114,105,103,104,116,32,40,99,41,32,49,57,57,55,32,77,105,99,114,111,115,111,102,116,32,67,111,114,112,46,49,30,48,28,6,3,85,4,11,19,21,77,105,99,114,111,115,111,102,116,32,67,111,114,112,111,114,97,116,105,111,110,49,33,48,31,6,3,85,4,3,19,24,77,105,99,114,111,115,111,102,116,32,82,111,111,116,32,65,117,116,104,111,114,105,116,121,48,30,23,13,57,55,48,49,49,48,48,55,48,48,48,48,90,23,13,50,48,49,50,51,49,48,55,48,48,48,48,90,48,112,49,43,48,41,6,3,85,4,11,19,34,67,111,112,121,114,105,103,104,116,32,40,99,41,32,49,57,57,55,32,77,105,99,114,111,115,111,102,116,32,67,111,114,112,46,49,30,48,28,6,3,85,4,11,19,21,77,105,99,114,111,115,111,102,116,32,67,111,114,112,111,114,97,116,105,111,110,49,33,48,31,6,3,85,4,3,19,24,77,105,99,114,111,115,111,102,116,32,82,111,111,116,32,65,117,116,104,111,114,105,116,121,48,130,1,34,48,13,6,9,42,134,72,134,247,13,1,1,1,5,0,3,130,1,15,0,48,130,1,10,2,130,1,1,0,169,2,189,193,112,230,59,242,78,27,40,159,151,120,94,48,234,162,169,141,37,95,248,254,149,76,163,183,254,157,162,32,62,124,81,162,155,162,143,96,50,107,209,66,100,121,238,172,118,201,84,218,242,235,156,134,28,143,159,132,102,179,197,107,122,98,35,214,29,60,222,15,1,146,232,150,196,191,45,102,154,154,104,38,153,208,58,44,191,12,181,88,38,193,70,231,10,62,56,150,44,169,40,57,168,236,73,131,66,227,132,15,187,154,108,85,97,172,130,124,161,96,45,119,76,233,153,180,100,59,154,80,28,49,8,36,20,159,169,231,145,43,24,230,61,152,99,20,96,88,5,101,159,29,55,82,135,247,167,239,148,2,198,27,211,191,85,69,179,137,128,191,58,236,84,148,78,174,253,167,122,109,116,78,175,24,204,150,9,40,33,0,87,144,96,105,55,187,75,18,7,60,86,255,91,251,164,102,10,8,166,210,129,86,87,239,182,59,94,22,129,119,4,218,246,190,174,128,149,254,176,205,127,214,167,26,114,92,60,202,188,240,8,163,34,48,179,6,133,201,179,32,119,19,133,223,2,3,1,0,1,163,129,168,48,129,165,48,129,162,6,3,85,29,1,4,129,154,48,129,151,128,16,91,208,112,239,105,114,158,35,81,126,20,178,77,142,255,203,161,114,48,112,49,43,48,41,6,3,85,4,11,19,34,67,111,112,121,114,105,103,104,116,32,40,99,41,32,49,57,57,55,32,77,105,99,114,111,115,111,102,116,32,67,111,114,112,46,49,30,48,28,6,3,85,4,11,19,21,77,105,99,114,111,115,111,102,116,32,67,111,114,112,111,114,97,116,105,111,110,49,33,48,31,6,3,85,4,3,19,24,77,105,99,114,111,115,111,102,116,32,82,111,111,116,32,65,117,116,104,111,114,105,116,121,130,15,0,193,0,139,60,60,136,17,209,62,246,99,236,223,64,48,13,6,9,42,134,72,134,247,13,1,1,4,5,0,3,130,1,1,0,149,232,11,192,141,243,151,24,53,237,184,1,36,216,119,17,243,92,96,50,159,158,11,203,62,5,145,136,143,201,58,230,33,242,240,87,147,44,181,160,71,200,98,239,252,215,204,59,59,90,169,54,84,105,254,36,109,63,201,204,170,222,5,124,221,49,141,61,159,16,112,106,187,254,18,79,24,105,192,252,208,67,227,17,90,32,79,234,98,123,175,170,25,200,43,55,37,45,190,101,161,18,138,37,15,99,163,247,84,28,249,33,201,214,21,243,82,172,110,67,50,7,253,130,23,248,229,103,108,13,81,246,189,241,82,199,189,231,196,48,252,32,49,9,136,29,149,41,26,77,213,29,2,165,241,128,224,3,180,91,244,177,221,200,87,238,101,73,199,82,84,182,180,3,40,18,255,144,214,240,8,143,126,184,151,197,171,55,44,228,122,228,168,119,227,118,160,0,208,106,63,193,210,54,138,224,65,18,168,53,106,27,106,219,53,225,212,28,4,228,168,69,4,200,90,51,56,110,77,28,13,98,183,10,162,140,211,213,84,63,70,205,28,85,166,112,219,18,58,135,147,117,159,167,210,160) PS >Get-X509Certificate $DERCert -Verbose Get certificate details from binary (DER) encoded X509 certificate. .EXAMPLE PS >[string] $Base64Cert = 'MIIEEjCCAvqgAwIBAgIPAMEAizw8iBHRPvZj7N9AMA0GCSqGSIb3DQEBBAUAMHAxKzApBgNVBAsTIkNvcHlyaWdodCAoYykgMTk5NyBNaWNyb3NvZnQgQ29ycC4xHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEhMB8GA1UEAxMYTWljcm9zb2Z0IFJvb3QgQXV0aG9yaXR5MB4XDTk3MDExMDA3MDAwMFoXDTIwMTIzMTA3MDAwMFowcDErMCkGA1UECxMiQ29weXJpZ2h0IChjKSAxOTk3IE1pY3Jvc29mdCBDb3JwLjEeMBwGA1UECxMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSEwHwYDVQQDExhNaWNyb3NvZnQgUm9vdCBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpAr3BcOY78k4bKJ+XeF4w6qKpjSVf+P6VTKO3/p2iID58UaKboo9gMmvRQmR57qx2yVTa8uuchhyPn4Rms8VremIj1h083g8BkuiWxL8tZpqaaCaZ0Dosvwy1WCbBRucKPjiWLKkoOajsSYNC44QPu5psVWGsgnyhYC13TOmZtGQ7mlAcMQgkFJ+p55ErGOY9mGMUYFgFZZ8dN1KH96fvlALGG9O/VUWziYC/OuxUlE6u/ad6bXROrxjMlgkoIQBXkGBpN7tLEgc8Vv9b+6RmCgim0oFWV++2O14WgXcE2va+roCV/rDNf9anGnJcPMq88AijIjCzBoXJsyB3E4XfAgMBAAGjgagwgaUwgaIGA1UdAQSBmjCBl4AQW9Bw72lyniNRfhSyTY7/y6FyMHAxKzApBgNVBAsTIkNvcHlyaWdodCAoYykgMTk5NyBNaWNyb3NvZnQgQ29ycC4xHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEhMB8GA1UEAxMYTWljcm9zb2Z0IFJvb3QgQXV0aG9yaXR5gg8AwQCLPDyIEdE+9mPs30AwDQYJKoZIhvcNAQEEBQADggEBAJXoC8CN85cYNe24ASTYdxHzXGAyn54Lyz4FkYiPyTrmIfLwV5MstaBHyGLv/NfMOztaqTZUaf4kbT/JzKreBXzdMY09nxBwarv+Ek8YacD80EPjEVogT+pie6+qGcgrNyUtvmWhEoolD2Oj91Qc+SHJ1hXzUqxuQzIH/YIX+OVnbA1R9r3xUse958Qw/CAxCYgdlSkaTdUdAqXxgOADtFv0sd3IV+5lScdSVLa0AygS/5DW8AiPfriXxas3LOR65Kh343agANBqP8HSNorgQRKoNWobats14dQcBOSoRQTIWjM4bk0cDWK3CqKM09VUP0bNHFWmcNsSOoeTdZ+n0qA=' PS >$Base64Cert | Get-X509Certificate -Verbose Get certificate details from Base64 encoded X509 certificate. .EXAMPLE PS >Get-Item "certificateFile.cer" | Get-X509Certificate Get certificate details from .cer file. .INPUTS System.Object .LINK https://github.com/jasoth/Utility.PS #> function Get-X509Certificate { [CmdletBinding()] [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2], [System.Security.Cryptography.X509Certificates.X509Certificate2Collection])] param ( # X.509 certificate that is binary (DER) encoded or Base64-encoded [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] [object] $InputObjects, # Only return the end-entity certificate [Parameter(Mandatory = $false)] [switch] $EndEntityCertificateOnly ) begin { ## Create list to capture byte stream from piped input. [System.Collections.Generic.List[byte]] $listBytes = New-Object System.Collections.Generic.List[byte] function Transform ([byte[]]$InputBytes) { $X509CertificateCollection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection $X509CertificateCollection.Import($InputBytes, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::EphemeralKeySet) Write-Output $X509CertificateCollection -NoEnumerate } } process { if ($InputObjects -is [byte[]]) { $X509CertificateCollection = Transform $InputObjects if ($EndEntityCertificateOnly) { Write-Output $X509CertificateCollection[-1] } else { Write-Output $X509CertificateCollection } } else { foreach ($InputObject in $InputObjects) { [byte[]] $inputBytes = $null if ($InputObject -is [byte]) { ## Populate list with byte stream from piped input. if ($listBytes.Count -eq 0) { Write-Verbose 'Creating byte array from byte stream.' Write-Warning ('For better performance when piping a single byte array, use "Write-Output $byteArray -NoEnumerate | {0}".' -f $MyInvocation.MyCommand) } $listBytes.Add($InputObject) } elseif ($InputObject -is [byte[]]) { $inputBytes = $InputObject } elseif ($InputObject -is [string] -or $InputObject -is [SecureString]) { if ($InputObject -is [SecureString]) { Write-Verbose 'Decrypting SecureString and decoding Base64 string to byte array.' if ($PSVersionTable.PSVersion -ge [version]'7.0') { $inputString = ConvertFrom-SecureString $InputObject -AsPlainText } else { $inputString = ConvertFrom-SecureStringAsPlainText $InputObject -Force } } else { $inputString = $InputObject } switch -Regex ($inputString) { '^[A-Fa-f0-9\r\n]+$' { Write-Verbose 'Decoding hex string to byte array.' $inputBytes = ConvertFrom-HexString $inputString -RawBytes } '^(?:[A-Za-z0-9+/\r\n]{4})*(?:[A-Za-z0-9+/\r\n]{2}==|[A-Za-z0-9+/\r\n]{3}=)?$' { Write-Verbose 'Decoding Base64 string to byte array.' $inputBytes = [System.Convert]::FromBase64String($inputString) } '.*\.(cer)$' { Write-Verbose 'Decoding hex string to byte array.' if ($PSVersionTable.PSVersion -ge [version]'6.0') { $inputBytes = Get-Content $inputString.FullName -Raw -AsByteStream } else { $inputBytes = Get-Content $inputString.FullName -Raw -Encoding Byte } } default { $inputBytes = [Text.Encoding]::Default.GetBytes($inputString) } } } elseif ($InputObject -is [System.IO.FileSystemInfo]) { Write-Verbose 'Decoding file content to byte array.' if ($PSVersionTable.PSVersion -ge [version]'6.0') { $inputBytes = Get-Content $InputObject.FullName -Raw -AsByteStream } else { $inputBytes = Get-Content $InputObject.FullName -Raw -Encoding Byte } } else { # Otherwise, write a terminating error message indicating that input object type is not supported. $errorMessage = 'Cannot convert input of type {0} to X.509 certificate.' -f $InputObject.GetType() Write-Error -Message $errorMessage -Category ([System.Management.Automation.ErrorCategory]::ParserError) -ErrorId 'GetX509CertificateFailureTypeNotSupported' -ErrorAction Stop } ## Only write output if the input is not a byte stream. if ($listBytes.Count -eq 0) { $X509CertificateCollection = Transform $inputBytes if ($EndEntityCertificateOnly) { Write-Output $X509CertificateCollection[-1] } else { Write-Output $X509CertificateCollection } } } } } end { ## Output captured byte stream from piped input. if ($listBytes.Count -gt 0) { $X509CertificateCollection = Transform $listBytes if ($EndEntityCertificateOnly) { Write-Output $X509CertificateCollection[-1] } else { Write-Output $X509CertificateCollection } } } } #endregion #region Get-X509CertificateCrlDistributionPoints.ps1 <# .SYNOPSIS Get X509 certificate extension 2.5.29.31 for CRL Distribution Points. .EXAMPLE PS >Get-X509CertificateCrlDistributionPoints $Certificate Get certificate CRL Distribution Points extension. .INPUTS System.Security.Cryptography.X509Certificates.X509Certificate2 .LINK https://github.com/jasoth/Utility.PS #> function Get-X509CertificateCrlDistributionPoints { [CmdletBinding()] [OutputType([string])] param ( # X.509 Certificate [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)] [System.Security.Cryptography.X509Certificates.X509Certificate2[]] $X509Certificate ) process { foreach ($Certificate in $X509Certificate) { $ExtCrlDistributionPoints = $Certificate.Extensions | Where-Object { $_.Oid.Value -eq '2.5.29.31' } if ($null -eq $ExtCrlDistributionPoints -or $null -eq $ExtCrlDistributionPoints.RawData -or $ExtCrlDistributionPoints.RawData.Length -lt 11) { continue } [int] $prev = -2 [System.Collections.Generic.List[string]] $items = New-Object 'System.Collections.Generic.List[string]' while ($prev -ne -1 -and $ExtCrlDistributionPoints.RawData.Length -gt $prev + 1) { [int] $startIndex = if ($prev -eq -2) { 8 } else { $prev + 1 } [int] $next = [System.Array]::IndexOf($ExtCrlDistributionPoints.RawData, [byte]0x86, $startIndex) if ($next -eq -1) { if ($prev -ge 0) { [string] $item = [System.Text.Encoding]::UTF8.GetString($ExtCrlDistributionPoints.RawData, $prev + 2, $ExtCrlDistributionPoints.RawData.Length - ($prev + 2)) $items.Add($item) } break } if ($prev -ge 0 -and $next -gt $prev) { [string] $item = [System.Text.Encoding]::UTF8.GetString($ExtCrlDistributionPoints.RawData, $prev + 2, $next - ($prev + 2)) $items.Add($item) } $prev = $next } Write-Output $items.ToArray() } } } #endregion #region Invoke-CommandAsSystem.ps1 <# .SYNOPSIS Run PowerShell commands under system context. .EXAMPLE PS >Invoke-CommandAsSystem { [System.Security.Principal.WindowsIdentity]::GetCurrent().Name } Run the ScriptBlock under the system context. .INPUTS System.Management.Automation.ScriptBlock .LINK https://github.com/jasoth/Utility.PS #> function Invoke-CommandAsSystem { [CmdletBinding()] param ( # Specifies the ScriptBlock to run under the system context. [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 1)] [ScriptBlock] $ScriptBlock, # Specifies the arguments to pass to the ScriptBlock. [Parameter(Mandatory = $false, Position = 2)] [string[]] $ArgumentList ) begin { ## Initialize Critical Dependencies $CriticalError = $null try { Import-Module PSScheduledJob, ScheduledTasks -ErrorAction Stop } catch { Write-Error -ErrorRecord $_ -ErrorVariable CriticalError; return } } process { ## Return Immediately On Critical Error if ($CriticalError) { return } ## Process [guid] $GUID = New-Guid try { ## Register ScheduleJob if ($ArgumentList) { $ScheduledJob = Register-ScheduledJob -Name $GUID -ScheduledJobOption (New-ScheduledJobOption -RunElevated) -ScriptBlock $ScriptBlock -ArgumentList $ArgumentList -ErrorAction Stop } else { $ScheduledJob = Register-ScheduledJob -Name $GUID -ScheduledJobOption (New-ScheduledJobOption -RunElevated) -ScriptBlock $ScriptBlock -ErrorAction Stop } try { ## Register ScheduledTask for ScheduledJob $ScheduledTask = Register-ScheduledTask -TaskName $GUID -Action (New-ScheduledTaskAction -Execute $ScheduledJob.PSExecutionPath -Argument $ScheduledJob.PSExecutionArgs) -Principal (New-ScheduledTaskPrincipal -UserId 'NT AUTHORITY\SYSTEM' -LogonType ServiceAccount -RunLevel Highest) -ErrorAction Stop try { ## Execute ScheduledTask Job to Run ScheduledJob Job $ScheduledTask | Start-ScheduledTask -AsJob -ErrorAction Stop | Wait-Job | Remove-Job -Force -Confirm:$False ## Wait for ScheduledTask to finish While (($ScheduledTask | Get-ScheduledTaskInfo).LastTaskResult -eq 267009) { Start-Sleep -Milliseconds 150 } ## Find ScheduledJob and get the result $Job = Get-Job -Name $GUID -ErrorAction SilentlyContinue | Wait-Job $Result = $Job | Receive-Job -Wait -AutoRemoveJob } finally { ## Unregister ScheduledTask for ScheduledJob $ScheduledTask | Unregister-ScheduledTask -Confirm:$false } } finally { ## Unregister ScheduleJob $ScheduledJob | Unregister-ScheduledJob -Force -Confirm:$False } } catch { Write-Error -ErrorRecord $_; return } return $Result } } #endregion #region New-SecureStringKey.ps1 <# .SYNOPSIS Generate random key for securestring encryption. .EXAMPLE PS >New-SecureStringKey Generate random 16 byte (128-bit) key. .EXAMPLE PS >$SecureKey = New-SecureStringKey -Length 32 PS >$SecureString = ConvertTo-SecureString "Super Secret String" -AsPlainText -Force PS >$EncryptedSecureString = ConvertFrom-SecureString $SecureString -SecureKey $SecureKey PS >$DecryptedSecureString = ConvertTo-SecureString $EncryptedSecureString -SecureKey $SecureKey PS >ConvertFrom-SecureStringAsPlainText $DecryptedSecureString Generate random 32 byte (256-bit) key and use it to encrypt another string. .INPUTS System.Int32 .LINK https://github.com/jasoth/Utility.PS #> function New-SecureStringKey { [CmdletBinding()] param ( # Key length [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true)] [ValidateSet(16, 24, 32)] [int] $Length = 16 ) [byte[]] $Key = Get-Random -InputObject ((0..255)*$Length) -Count $Length [securestring] $SecureKey = ConvertTo-SecureString -String ([System.Text.Encoding]::ASCII.GetString($Key)) -AsPlainText -Force return $SecureKey } #endregion #region Remove-Diacritics.ps1 <# .SYNOPSIS Decompose characters to their base character equivilents and remove diacritics. .EXAMPLE PS >Remove-Diacritics 'àáâãäåÀÁÂÃÄÅfi⁵ẛ' Decompose characters to their base character equivilents and remove diacritics. .EXAMPLE PS >Remove-Diacritics 'àáâãäåÀÁÂÃÄÅfi⁵ẛ' -CompatibilityDecomposition Decompose composite characters to their base character equivilents and remove diacritics. .INPUTS System.String .LINK https://github.com/jasoth/Utility.PS #> function Remove-Diacritics { [CmdletBinding()] param ( # String value to transform. [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [string[]] $InputStrings, # Use compatibility decomposition instead of canonical decomposition which further decomposes composite characters and many formatting distinctions are removed. [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [switch] $CompatibilityDecomposition ) process { [System.Text.NormalizationForm] $NormalizationForm = [System.Text.NormalizationForm]::FormD if ($CompatibilityDecomposition) { $NormalizationForm = [System.Text.NormalizationForm]::FormKD } foreach ($InputString in $InputStrings) { $NormalizedString = $InputString.Normalize($NormalizationForm) $OutputString = New-Object System.Text.StringBuilder foreach ($char in $NormalizedString.ToCharArray()) { if ([Globalization.CharUnicodeInfo]::GetUnicodeCategory($char) -ne [Globalization.UnicodeCategory]::NonSpacingMark) { [void] $OutputString.Append($char) } } Write-Output $OutputString.ToString() } } } #endregion #region Remove-InvalidFileNameCharacters.ps1 <# .SYNOPSIS Remove invalid filename characters from string. .EXAMPLE PS >Remove-InvalidFileNameCharacters 'à/1\b?2|ć*3<đ>4 ē' Remove invalid filename characters from string. .EXAMPLE PS >Remove-InvalidFileNameCharacters 'à/1\b?2|ć*3<đ>4 ē' -RemoveDiacritics Remove invalid filename characters and diacritics from string. .INPUTS System.String .LINK https://github.com/jasoth/Utility.PS #> function Remove-InvalidFileNameCharacters { [CmdletBinding()] param ( # String value to transform. [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [string[]] $InputStrings, # Character used as replacement for invalid characters. Use '' to simply remove. [Parameter(Mandatory = $false)] [string] $ReplacementCharacter = '-', # Replace characters with diacritics to their non-diacritic equivilent. [Parameter(Mandatory = $false)] [switch] $RemoveDiacritics ) process { foreach ($InputString in $InputStrings) { [string] $OutputString = $InputString if ($RemoveDiacritics) { $OutputString = Remove-Diacritics $OutputString -CompatibilityDecomposition } $OutputString = [regex]::Replace($OutputString, ('[{0}]' -f [regex]::Escape([System.IO.Path]::GetInvalidFileNameChars() -join '')), $ReplacementCharacter) Write-Output $OutputString } } } #endregion #region Remove-SensitiveData.ps1 <# .SYNOPSIS Remove sensitive data from object or string. .EXAMPLE PS >$MyString = 'My password is: "SuperSecretString"' PS >Remove-SensitiveData ([ref]$MyString) -FilterValues "Super","String" This removes the word "Super" and "String" from the input string with no output. .EXAMPLE PS >Remove-SensitiveData 'My password is: "SuperSecretString"' -FilterValues "Super","String" -PassThru This removes the word "Super" and "String" from the input string and return the result. .INPUTS System.Object .LINK https://github.com/jasoth/Utility.PS #> function Remove-SensitiveData { [CmdletBinding()] [OutputType([object])] param ( # Object from which to remove sensitive data. [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [object] $InputObjects, # Sensitive string values to remove from input object. [Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string[]] $FilterValues, # Replacement value for senstive data. [Parameter(Mandatory = $false)] [string] $ReplacementValue = '********', # Copy the input object rather than remove data directly from input. [Parameter(Mandatory = $false)] [switch] $Clone, # Output object with sensitive data removed. [Parameter(Mandatory = $false)] [switch] $PassThru ) process { if ($InputObjects.GetType().FullName.StartsWith('System.Management.Automation.PSReference')) { if ($Clone) { $OutputObjects = $InputObjects.Value.Clone() } else { $OutputObjects = $InputObjects } } else { if ($Clone) { $OutputObjects = [ref]$InputObjects.Clone() } else { if ($InputObjects -is [System.ValueType] -or $InputObjects -is [string]) { Write-Warning ('The input of type [{0}] was not passed by reference. Senstive data will not be removed from the original input.' -f $InputObjects.GetType()) } $OutputObjects = [ref]$InputObjects } } if ($OutputObjects.Value -is [string]) { foreach ($FilterValue in $FilterValues) { if ($OutputObjects.Value -and $FilterValue) { $OutputObjects.Value = $OutputObjects.Value.Replace($FilterValue, $ReplacementValue) } } } elseif ($OutputObjects.Value -is [System.Collections.IList]) { for ($ii = 0; $ii -lt $OutputObjects.Value.Count; $ii++) { if ($null -ne $OutputObjects.Value[$ii] -and $OutputObjects.Value[$ii] -isnot [ValueType]) { $OutputObjects.Value[$ii] = Remove-SensitiveData ([ref]$OutputObjects.Value[$ii]) -FilterValues $FilterValues -PassThru } } } elseif ($OutputObjects.Value -is [System.Collections.IDictionary]) { [array] $KeyNames = $OutputObjects.Value.Keys for ($ii = 0; $ii -lt $KeyNames.Count; $ii++) { if ($null -ne $OutputObjects.Value[$KeyNames[$ii]] -and $OutputObjects.Value[$KeyNames[$ii]] -isnot [ValueType]) { $OutputObjects.Value[$KeyNames[$ii]] = Remove-SensitiveData ([ref]$OutputObjects.Value[$KeyNames[$ii]]) -FilterValues $FilterValues -PassThru } } } elseif ($OutputObjects.Value -is [object] -and $OutputObjects.Value -isnot [ValueType]) { [array] $PropertyNames = $OutputObjects.Value | Get-Member -MemberType Property, NoteProperty for ($ii = 0; $ii -lt $PropertyNames.Count; $ii++) { $PropertyName = $PropertyNames[$ii].Name if ($null -ne $OutputObjects.Value.$PropertyName -and $OutputObjects.Value.$PropertyName -isnot [ValueType]) { $OutputObjects.Value.$PropertyName = Remove-SensitiveData ([ref]$OutputObjects.Value.$PropertyName) -FilterValues $FilterValues -PassThru } } } else { ## Non-Terminating Error $Exception = New-Object ArgumentException -ArgumentList ('Cannot remove senstive data from input of type {0}.' -f $OutputObjects.Value.GetType()) Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ParserError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'RemoveSensitiveDataFailureTypeNotSupported' -TargetObject $OutputObjects.Value continue } if ($PassThru -or $Clone) { ## Return the object with sensitive data removed. if ($OutputObjects.Value -is [System.Collections.IList]) { Write-Output $OutputObjects.Value -NoEnumerate } else { Write-Output $OutputObjects.Value } } } } #endregion #region Select-PsBoundParameters.ps1 <# .SYNOPSIS Filters a hashtable or PSBoundParameters containing PowerShell command parameters to only those valid for specified command. .EXAMPLE PS >Select-PsBoundParameters @{Name='Valid'; Verbose=$true; NotAParameter='Remove'} -CommandName Get-Process -ExcludeParameters 'Verbose' Filters the parameter hashtable to only include valid parameters for the Get-Process command and exclude the Verbose parameter. .EXAMPLE PS >Select-PsBoundParameters @{Name='Valid'; Verbose=$true; NotAParameter='Remove'} -CommandName Get-Process -CommandParameterSets NameWithUserName Filters the parameter hashtable to only include valid parameters for the Get-Process command in the "NameWithUserName" ParameterSet. .INPUTS System.String .LINK https://github.com/jasoth/Utility.PS #> function Select-PsBoundParameters { [CmdletBinding()] [OutputType([hashtable])] param ( # Specifies the parameter key pairs to be filtered. [Parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true)] [hashtable] $NamedParameters, # Specifies the parameter names to remove from the output. [Parameter(Mandatory = $false)] [ArgumentCompleter( { param ( $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters ) if ($fakeBoundParameters.ContainsKey('NamedParameters')) { [string[]]$fakeBoundParameters.NamedParameters.Keys | Where-Object { $_ -Like "$wordToComplete*" } } })] [string[]] $ExcludeParameters, # Specifies the name of a PowerShell command to further filter valid parameters. [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [ArgumentCompleter( { param ( $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters ) [array] $CommandInfo = Get-Command "$wordToComplete*" if ($CommandInfo) { $CommandInfo.Name #| ForEach-Object {$_} } })] [Alias('Name')] [string] $CommandName, # Specifies parameter sets of the PowerShell command to further filter valid parameters. [Parameter(Mandatory = $false)] [ArgumentCompleter( { param ( $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters ) if ($fakeBoundParameters.ContainsKey('CommandName')) { [array] $CommandInfo = Get-Command $fakeBoundParameters.CommandName if ($CommandInfo) { $CommandInfo[0].ParameterSets.Name | Where-Object { $_ -Like "$wordToComplete*" } } } })] [string[]] $CommandParameterSets ) process { [hashtable] $SelectedParameters = $NamedParameters.Clone() [string[]] $CommandParameters = $null if ($CommandName) { $CommandInfo = Get-Command $CommandName if ($CommandParameterSets) { [System.Collections.Generic.List[string]] $listCommandParameters = New-Object System.Collections.Generic.List[string] foreach ($CommandParameterSet in $CommandParameterSets) { $listCommandParameters.AddRange([string[]]($CommandInfo.ParameterSets | Where-Object Name -eq $CommandParameterSet | Select-Object -ExpandProperty Parameters | Select-Object -ExpandProperty Name)) } $CommandParameters = $listCommandParameters | Select-Object -Unique } else { $CommandParameters = $CommandInfo.Parameters.Keys } } [string[]] $ParameterKeys = $SelectedParameters.Keys foreach ($ParameterKey in $ParameterKeys) { if ($ExcludeParameters -contains $ParameterKey -or ($CommandParameters -and $CommandParameters -notcontains $ParameterKey)) { $SelectedParameters.Remove($ParameterKey) } } return $SelectedParameters } } #endregion #region Skip-NullValue.ps1 <# .SYNOPSIS Output the first non-null value from list of input values. .EXAMPLE PS >Skip-NullValue $null, 'winner', 'loser' Return the first non-null value which is 'winner'. .EXAMPLE PS >Get-Module 'NonExistentModuleName' | Skip-NullValue -DefaultValue @() Return the first non-null value which is 'winner'. .EXAMPLE PS >Skip-NullValue $null, '', ([guid]::Empty), @(), 0, ([int]-1), 'winner', 'loser' -SkipEmpty -SkipZero -SkipNegative Return the first non-null, non-empty, non-zero, and non-negative value which is 'winner'. .INPUTS System.Object .LINK https://github.com/jasoth/Utility.PS #> function Skip-NullValue { [CmdletBinding()] [Alias('Coalesce')] param ( # Values to coalesce [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] [AllowNull()] [object] $InputObject, # Skip over empty values [Parameter(Mandatory = $false)] [switch] $SkipEmpty, # Skip over zero values [Parameter(Mandatory = $false)] [switch] $SkipZero, # Skip over negative values [Parameter(Mandatory = $false)] [switch] $SkipNegative, # Default value when no other values [Parameter(Mandatory = $false)] [object] $DefaultValue = $null, # Enumerate pipeline input rather than treat it as a single input [Parameter(Mandatory = $false)] [switch] $EnumeratePipelineInput ) begin { function HasValue ($Object) { if ($null -ne $Object) { Write-Debug "ObjectType: $($Object.psobject.TypeNames[0]) | Object: $Object" ## Additional Tests (these could leak errors into $Error variable) [bool]$TestEmptyArray = try { $SkipEmpty -and $Object -is [array] -and $Object.Count -eq 0 } catch { $false } [bool]$TestEmpty = try { $SkipEmpty -and $Object -eq ($Object.GetType())::Empty } catch { $false } [bool]$TestZero = try { $SkipZero -and $Object -eq 0 } catch { $false } [bool]$TestNegative = try { $SkipNegative -and $Object -lt 0 } catch { $false } Write-Debug "TestEmptyArray: $TestEmptyArray | TestEmpty: $TestEmpty | TestZero: $TestZero | TestNegative: $TestNegative" if (!($TestEmptyArray -or $TestEmpty -or $TestZero -or $TestNegative)) { return $true } } return $false } ## Initialize $InputObjects = @() $OutputObject = $null [bool]$IsPipelineInput = $false if ($null -eq $InputObject) { $IsPipelineInput = $true } } process { ## Save pipeline input to process at end if not enumerating pipeline input. $InputObjects += $InputObject if ($IsPipelineInput -and !$EnumeratePipelineInput) { return } ## Skip enumerated input if previous input already satisfied condition. if ($null -ne $OutputObject) { return } ## Loop through input objects and return the first value. foreach ($Object in $InputObject) { if (HasValue $Object) { $OutputObject = $Object return } } } end { ## Remove array if count is 1 or less. if ($InputObjects.Count -eq 0) { $InputObjects = $null } elseif ($InputObjects.Count -eq 1) { $InputObjects = $InputObjects[0] } ## If pipeline input was detected, treat the input array as one value like ?? operator. if ($IsPipelineInput -and !$EnumeratePipelineInput) { if (HasValue $InputObjects) { $OutputObject = $InputObjects } } ## If no acceptable values were found, use default value. if ($null -eq $OutputObject) { $OutputObject = $DefaultValue } Write-Output $OutputObject -NoEnumerate } } #endregion #region Test-IpAddressInSubnet.ps1 <# .SYNOPSIS Determine if an IP address exists in the specified subnet. .EXAMPLE PS >Test-IpAddressInSubnet 192.168.1.10 -Subnet '192.168.1.1/32','192.168.1.0/24' Determine if the IPv4 address exists in the specified subnet. .EXAMPLE PS >Test-IpAddressInSubnet 2001:db8:1234::1 -Subnet '2001:db8:a::123/64','2001:db8:1234::/48' Determine if the IPv6 address exists in the specified subnet. .INPUTS System.Net.IPAddress .LINK https://github.com/jasoth/Utility.PS #> function Test-IpAddressInSubnet { [CmdletBinding()] [OutputType([bool], [string[]])] param ( # IP Address to test against provided subnets. [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 1)] [ipaddress[]] $IpAddresses, # List of subnets in CIDR notation. For example, "192.168.1.0/24" or "2001:db8:1234::/48". [Parameter(Mandatory = $true)] [string[]] $Subnets, # Return list of matching subnets rather than a boolean result. [Parameter(Mandatory = $false)] [switch] $ReturnMatchingSubnets ) begin { function ConvertBitArrayToByteArray([System.Collections.BitArray] $BitArray) { [byte[]] $ByteArray = New-Object byte[] ([System.Math]::Ceiling($BitArray.Length / 8)) $BitArray.CopyTo($ByteArray, 0) return $ByteArray } function ConvertBitArrayToBigInt([System.Collections.BitArray] $BitArray) { return [bigint][byte[]](ConvertBitArrayToByteArray $BitArray) } } process { foreach ($IpAddress in $IpAddresses) { if ($IpAddress.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) { [int32] $bitIpAddress = [BitConverter]::ToInt32($IpAddress.GetAddressBytes(), 0) } else { [System.Collections.BitArray] $bitIpAddress = $IpAddress.GetAddressBytes() } [System.Collections.Generic.List[string]] $listSubnets = New-Object System.Collections.Generic.List[string] [bool] $Result = $false foreach ($Subnet in $Subnets) { [string[]] $SubnetComponents = $Subnet.Split('/') [ipaddress] $SubnetAddress = $SubnetComponents[0] [int] $SubnetMaskLength = $SubnetComponents[1] if ($IpAddress.AddressFamily -eq $SubnetAddress.AddressFamily) { if ($IpAddress.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) { ## Supports IPv4 (32 bit) only but more performant than BitArray? #[int32] $bitIpAddress = [BitConverter]::ToInt32($IpAddress.GetAddressBytes(), 0) [int32] $bitSubnetAddress = [BitConverter]::ToInt32($SubnetAddress.GetAddressBytes(), 0) [int32] $bitSubnetMaskHostOrder = 0 if ($SubnetMaskLength -gt 0) { $bitSubnetMaskHostOrder = -1 -shl (32 - $SubnetMaskLength) } [int32] $bitSubnetMask = [ipaddress]::HostToNetworkOrder($bitSubnetMaskHostOrder) ## Check IP if (($bitIpAddress -band $bitSubnetMask) -eq ($bitSubnetAddress -band $bitSubnetMask)) { if ($ReturnMatchingSubnets) { $listSubnets.Add($Subnet) } else { $Result = $true continue } } } else { ## BitArray supports IPv4 (32 bits) and IPv6 (128 bits). Would Int128 type in .NET 7 improve performance? #[System.Collections.BitArray] $bitIpAddress = $IpAddress.GetAddressBytes() [System.Collections.BitArray] $bitSubnetAddress = $SubnetAddress.GetAddressBytes() [System.Collections.BitArray] $bitSubnetMask = New-Object System.Collections.BitArray -ArgumentList ($bitSubnetAddress.Length - $SubnetMaskLength), $true $bitSubnetMask.Length = $bitSubnetAddress.Length [void]$bitSubnetMask.Not() [byte[]] $ByteArray = ConvertBitArrayToByteArray $bitSubnetMask [array]::Reverse($ByteArray) # Convert to Network byte order [System.Collections.BitArray] $bitSubnetMask = $ByteArray ## Check IP if ((ConvertBitArrayToBigInt $bitIpAddress.And($bitSubnetMask)) -eq (ConvertBitArrayToBigInt $bitSubnetAddress.And($bitSubnetMask))) { if ($ReturnMatchingSubnets) { $listSubnets.Add($Subnet) } else { $Result = $true continue } } } } } ## Return list of matches or boolean result if ($ReturnMatchingSubnets) { if ($listSubnets.Count -gt 1) { Write-Output $listSubnets.ToArray() -NoEnumerate } elseif ($listSubnets.Count -eq 1) { Write-Output $listSubnets.ToArray() } else { $Exception = New-Object ArgumentException -ArgumentList ('The IP address {0} does not belong to any of the provided subnets.' -f $IpAddress) Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ObjectNotFound) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'TestIpAddressInSubnetNoMatch' -TargetObject $IpAddress } } else { Write-Output $Result } } } } #endregion #region Test-PsElevation.ps1 <# .SYNOPSIS Test if current PowerShell process is elevated to local administrator privileges. .EXAMPLE PS >Test-PsElevation Test is current PowerShell process is elevated. .INPUTS None .LINK https://github.com/jasoth/Utility.PS #> function Test-PsElevation { [CmdletBinding()] [OutputType([bool])] param() try { $WindowsIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent() $WindowsPrincipal = New-Object 'System.Security.Principal.WindowsPrincipal' $WindowsIdentity $LocalAdministrator = [System.Security.Principal.WindowsBuiltInRole]::Administrator return $WindowsPrincipal.IsInRole($LocalAdministrator) } catch { if ($_.Exception.InnerException) { Write-Error -Exception $_.Exception.InnerException -Category $_.CategoryInfo.Category -CategoryActivity $_.CategoryInfo.Activity -ErrorId $_.FullyQualifiedErrorId -TargetObject $_.TargetObject } else { Write-Error -ErrorRecord $_ } } } #endregion #region Use-Progress.ps1 <# .SYNOPSIS Display progress bar for processing array of objects. .EXAMPLE PS >Use-Progress -InputObjects @(1..10) -Activity "Processing Parent Objects" -ScriptBlock { $Parent = $args[0] Use-Progress -InputObjects @(1..200) -Activity "Processing Child Objects" -ScriptBlock { $Child = $args[0] Write-Host "Child $Child of Parent $Parent." Start-Sleep -Milliseconds 50 } } Display progress bar for processing array of objects. .INPUTS System.Object[] .LINK https://github.com/jasoth/Utility.PS #> function Use-Progress { [CmdletBinding()] param ( # Array of objects to loop through. [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object[]] $InputObjects, # Specifies the first line of text in the heading above the status bar. This text describes the activity whose progress is being reported. [Parameter(Mandatory = $true)] [string] $Activity, # Script block to execute for each object in array. [Parameter(Mandatory = $true)] [scriptblock] $ScriptBlock, # Property name to use for current operation [Parameter(Mandatory = $false)] [string] $Property, # Minimum timespan between each progress update. [Parameter(Mandatory = $false)] [timespan] $MinimumUpdateFrequency = (New-Timespan -Seconds 1) ) begin { [System.Collections.Generic.List[object]] $listObjects = New-Object System.Collections.Generic.List[object] } process { $listObjects.AddRange($InputObjects) } end { if ($listObjects.Count -gt 0) { [object[]] $InputObjects = $listObjects.ToArray() } [int] $Id = 0 if (!(Get-Variable stackProgressId -ErrorAction SilentlyContinue)) { New-Variable -Name stackProgressId -Scope Script -Value (New-Object System.Collections.Generic.Stack[int]) } while ($stackProgressId.Contains($Id)) { $Id += 1 } [hashtable] $paramWriteProgress = @{ Id = $Id Activity = $Activity } if ($stackProgressId.Count -gt 0) { $paramWriteProgress['ParentId'] = $stackProgressId.Peek() } [int] $SecondsRemaining = -1 [int] $total = $InputObjects.Count try { $stackProgressId.Push($Id) [System.Diagnostics.Stopwatch] $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() for ($iObject = 0; $iObject -lt $total; $iObject++) { if ($iObject -eq 0 -or ($stopwatch.Elapsed - $TimeElapsed) -gt $MinimumUpdateFrequency) { [timespan] $TimeElapsed = $stopwatch.Elapsed $PercentComplete = $iObject / $total if ($PercentComplete -gt 0) { $SecondsRemaining = $TimeElapsed.TotalSeconds / $PercentComplete - $TimeElapsed.TotalSeconds } if ($Property) { $CurrentOperation = $InputObjects[$iObject].$Property } else { $CurrentOperation = $InputObjects[$iObject] } Write-Progress -CurrentOperation $CurrentOperation -Status ("{0:P0} Completed ({1} of {2}) in {3:c}" -f $PercentComplete, $iObject, $total, $TimeElapsed.Subtract($TimeElapsed.Ticks % [TimeSpan]::TicksPerSecond)) -PercentComplete ($PercentComplete * 100) -SecondsRemaining $SecondsRemaining @paramWriteProgress } Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $InputObjects[$iObject] } $stopwatch.Stop() Write-Progress -Status ("{0:P0} Completed ({1} of {2}) in {3:c}" -f 1, $total, $total, $TimeElapsed.Subtract($TimeElapsed.Ticks % [TimeSpan]::TicksPerSecond)) -PercentComplete 100 -SecondsRemaining 0 @paramWriteProgress } finally { [void] $stackProgressId.Pop() #Start-Sleep -Seconds 1 Write-Progress -Id $Id -Activity $Activity -Completed } } } #endregion #region Write-HostPrompt.ps1 <# .SYNOPSIS Displays a PowerShell prompt for multiple fields or multiple choices. .EXAMPLE PS >Write-HostPrompt "Prompt Caption" -Fields "Field 1", "Field 2" Display simple prompt for 2 fields. .EXAMPLE PS >$IntegerField = New-Object System.Management.Automation.Host.FieldDescription -ArgumentList "Integer Field" -Property @{ HelpMessage = "Help Message for Integer Field" } PS >$IntegerField.SetParameterType([int[]]) PS >$DateTimeField = New-Object System.Management.Automation.Host.FieldDescription -ArgumentList "DateTime Field" -Property @{ HelpMessage = "Help Message for DateTime Field" } PS >$DateTimeField.SetParameterType([datetime]) PS >Write-HostPrompt "Prompt Caption" "Prompt Message" -Fields $IntegerField, $DateTimeField Display prompt for 2 type-specific fields, with int field being an array. .EXAMPLE PS >Write-HostPrompt "Prompt Caption" -Choices "Choice &1", "Choice &2" Display simple prompt with 2 choices. .EXAMPLE PS >Write-HostPrompt "Prompt Caption" "Prompt Message" -DefaultChoice 2 -Choices @( New-Object System.Management.Automation.Host.ChoiceDescription -ArgumentList "&1`bChoice one" -Property @{ HelpMessage = "Help Message for Choice 1" } New-Object System.Management.Automation.Host.ChoiceDescription -ArgumentList "&2`bChoice two" -Property @{ HelpMessage = "Help Message for Choice 2" } ) Display prompt with 2 choices and help messages that defaults to the second choice. .EXAMPLE PS >Write-HostPrompt "Prompt Caption" "Choose a number" -Choices "Menu Item A", "Menu Item B", "Menu Item C" -HelpMessages "Menu Item A Needs Help", "Menu Item B Needs More Help",, "Menu Item C Needs Crazy Help" -NumberedHotKeys Display prompt with 3 choices and help message that are automatically numbered. .INPUTS System.Management.Automation.Host.FieldDescription System.Management.Automation.Host.ChoiceDescription .OUTPUTS System.Collections.Generic.Dictionary[System.String,System.Management.Automation.PSObject] System.Int32 .LINK https://github.com/jasoth/Utility.PS #> function Write-HostPrompt { [CmdletBinding()] param ( # Caption to preceed or title the prompt. [Parameter(Mandatory = $true, Position = 1)] [string] $Caption, # A message that describes the prompt. [Parameter(Mandatory = $false, Position = 2)] [string] $Message, # The fields in the prompt. [Parameter(Mandatory = $true, ParameterSetName = 'Fields', Position = 3, ValueFromPipeline = $true)] [System.Management.Automation.Host.FieldDescription[]] $Fields, # The choices the shown in the prompt. [Parameter(Mandatory = $true, ParameterSetName = 'Choices', Position = 3, ValueFromPipeline = $true)] [System.Management.Automation.Host.ChoiceDescription[]] $Choices, # Specifies a help message for each field or choice. [Parameter(Mandatory = $false, Position = 4)] [string[]] $HelpMessages = @(), # The index of the label in the choices to make default. [Parameter(Mandatory = $false, ParameterSetName = 'Choices', Position = 5)] [int] $DefaultChoice, # Use numbered hot keys (aka "keyboard accelerator") for each choice. [Parameter(Mandatory = $false, ParameterSetName = 'Choices', Position = 6)] [switch] $NumberedHotKeys ) begin { ## Create list to capture multiple fields or multiple choices. [System.Collections.Generic.List[System.Management.Automation.Host.FieldDescription]] $listFields = New-Object System.Collections.Generic.List[System.Management.Automation.Host.FieldDescription] [System.Collections.Generic.List[System.Management.Automation.Host.ChoiceDescription]] $listChoices = New-Object System.Collections.Generic.List[System.Management.Automation.Host.ChoiceDescription] } process { switch ($PSCmdlet.ParameterSetName) { 'Fields' { for ($iField = 0; $iField -lt $Fields.Count; $iField++) { if ($iField -lt $HelpMessages.Count -and $HelpMessages[$iField]) { $Fields[$iField].HelpMessage = $HelpMessages[$iField] } $listFields.Add($Fields[$iField]) } } 'Choices' { for ($iChoice = 0; $iChoice -lt $Choices.Count; $iChoice++) { if ($NumberedHotKeys) { $Choices[$iChoice] = New-Object System.Management.Automation.Host.ChoiceDescription -ArgumentList "&$($iChoice+1)`b$($Choices[$iChoice].Label)" -Property @{ HelpMessage = $Choices[$iChoice].HelpMessage } } #elseif (!$Choices[$iChoice].Label.Contains('&')) { $Choices[$iChoice] = New-Object System.Management.Automation.Host.ChoiceDescription -ArgumentList "&$($Choices[$iChoice].Label)" -Property @{ HelpMessage = $Choices[$iChoice].HelpMessage } } if ($iChoice -lt $HelpMessages.Count -and $HelpMessages[$iChoice]) { $Choices[$iChoice].HelpMessage = $HelpMessages[$iChoice] } $listChoices.Add($Choices[$iChoice]) } } } } end { try { switch ($PSCmdlet.ParameterSetName) { 'Fields' { return $Host.UI.Prompt($Caption, $Message, $listFields.ToArray()) } 'Choices' { return $Host.UI.PromptForChoice($Caption, $Message, $listChoices.ToArray(), $DefaultChoice - 1) + 1 } } } catch [System.Management.Automation.PSInvalidOperationException] { ## Write Non-Terminating Error When In Non-Interactive Mode. Write-Error -ErrorRecord $_ -CategoryActivity $MyInvocation.MyCommand } } } #endregion #endregion ## Set Strict Mode for Module. https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/set-strictmode Set-StrictMode -Version 3.0 ## PowerShell Desktop 5.1 does not dot-source ScriptsToProcess when a specific version is specified on import. This is a bug. # if ($PSEdition -eq 'Desktop') { # $ModuleManifest = Import-PowershellDataFile (Join-Path $PSScriptRoot $MyInvocation.MyCommand.Name.Replace('.psm1','.psd1')) # if ($ModuleManifest.ContainsKey('ScriptsToProcess')) { # foreach ($Path in $ModuleManifest.ScriptsToProcess) { # . (Join-Path $PSScriptRoot $Path) # } # } # } Export-ModuleMember -Function @('Compress-Data','ConvertFrom-Base64String','ConvertFrom-ClixmlString','ConvertFrom-HexString','ConvertFrom-HtmlString','ConvertFrom-QueryString','ConvertFrom-SecureStringAsPlainText','ConvertFrom-UrlString','ConvertTo-Base64String','ConvertTo-ClixmlString','ConvertTo-Dictionary','ConvertTo-HexString','ConvertTo-HtmlString','ConvertTo-MarkdownTable','ConvertTo-PsParameterString','ConvertTo-PsString','ConvertTo-QueryString','ConvertTo-UrlString','Expand-Data','Format-DataSize','Format-NumberWithMetricUnit','Format-PropertyValue','Get-ContentEncoding','Get-PropertyValue','Get-RelativePath','Get-StrictModeVersion','Get-X509Certificate','Get-X509CertificateCrlDistributionPoints','Invoke-CommandAsSystem','New-SecureStringKey','Remove-Diacritics','Remove-InvalidFileNameCharacters','Remove-SensitiveData','Select-PsBoundParameters','Skip-NullValue','Test-IpAddressInSubnet','Test-PsElevation','Use-Progress','Write-HostPrompt') -Cmdlet @() -Variable @() -Alias @('Deflate-Data','Decompress-Data','Inflate-Data','Format-FileSize') # SIG # Begin signature block # MIIjbAYJKoZIhvcNAQcCoIIjXTCCI1kCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCA8LgGqFXfoBD9w # gBo6zLuRvJxiwTUUcZA0c7z0G+qfpaCCHWUwggUmMIIEDqADAgECAhAKbwamSf02 # TrzqY8wkoMRzMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNV # BAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwHhcN # MjAwMzMxMDAwMDAwWhcNMjMwNDA1MTIwMDAwWjBjMQswCQYDVQQGEwJVUzENMAsG # A1UECBMET2hpbzETMBEGA1UEBxMKQ2luY2lubmF0aTEXMBUGA1UEChMOSmFzb24g # VGhvbXBzb24xFzAVBgNVBAMTDkphc29uIFRob21wc29uMIIBIjANBgkqhkiG9w0B # AQEFAAOCAQ8AMIIBCgKCAQEAxWfKBk7TC+lDc2MakRESqnSv8U3kLRfQafofGuE9 # cDIZloGUSNXR47pvPw0FUXDIexDQEXFPsKsa8ILC96Sbtuohlogl72QVgC85UEMr # 5LTjZ0ZpPxxRLFTpAiSBcvYhkpm7xHwfT7bqt6Ealp2P6idurMWyFpLwLXz/WgW/ # btb/cV47ACRdsTwxum5z2e1H/o9RXhuLDcBhQhNWmzQ+Z9MHV/ToOattZreisdUM # 7XIQv8TWGh7SOlc8AfO+02Usy1mDkt5GsZ2R9qyrxX3heJw1ZTxcXLoPlwWUiDRE # 9xLMwlElvvyd+lAieukMBqC+IMJRVHlnAuy8OTT3qHyQJQIDAQABo4IBxTCCAcEw # HwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0OBBYEFL7nzjkk # +8NZ6eNdqEujhdQJxOcyMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEF # BQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdpY2VydC5jb20v # c2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2NybDQuZGlnaWNl # cnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUwQzA3BglghkgB # hv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQ # UzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcwAYYYaHR0cDov # L29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8vY2FjZXJ0cy5k # aWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNpZ25pbmdDQS5j # cnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEARH2swe77D6omtCaH # pq3oasA9i4eLbO5TTid1FHNNKYdQq/NLUO8RjEunpw7//eAcSoFXVLRhXnxGfmJ0 # yKLt+YA1J87U6DjHvv8KaaenAHxqhIKltHGpwgET6lSbuvskFPjE0QpPcWSBylXK # YThW4ixwGCd6QSaZpV8OiHVebhxD6G+3Jnz7f5s1D857TTxFKTnOaJaJL754Z4HU # Pm/rIuzZscAeV0ooKnwyDfbZWpEHYL1sWVBLFL3sUH+zgniMbGNJKXoyZxgvOTD4 # Kilzn/1zVATMF772tkxoA/Bvp73vu2QW0U4J+J6QRICOS7Y0+qOPzcS0s46WWu/e # vzWhZzCCBTAwggQYoAMCAQICEAQJGBtf1btmdVNDtW+VUAgwDQYJKoZIhvcNAQEL # BQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UE # CxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJ # RCBSb290IENBMB4XDTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowcjELMAkG # A1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRp # Z2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENv # ZGUgU2lnbmluZyBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPjT # sxx/DhGvZ3cH0wsxSRnP0PtFmbE620T1f+Wondsy13Hqdp0FLreP+pJDwKX5idQ3 # Gde2qvCchqXYJawOeSg6funRZ9PG+yknx9N7I5TkkSOWkHeC+aGEI2YSVDNQdLEo # JrskacLCUvIUZ4qJRdQtoaPpiCwgla4cSocI3wz14k1gGL6qxLKucDFmM3E+rHCi # q85/6XzLkqHlOzEcz+ryCuRXu0q16XTmK/5sy350OTYNkO/ktU6kqepqCquE86xn # TrXE94zRICUj6whkPlKWwfIPEvTFjg/BougsUfdzvL2FsWKDc0GCB+Q4i2pzINAP # ZHM8np+mM6n9Gd8lk9ECAwEAAaOCAc0wggHJMBIGA1UdEwEB/wQIMAYBAf8CAQAw # DgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHkGCCsGAQUFBwEB # BG0wazAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsG # AQUFBzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1 # cmVkSURSb290Q0EuY3J0MIGBBgNVHR8EejB4MDqgOKA2hjRodHRwOi8vY3JsNC5k # aWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMDqgOKA2hjRo # dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0Eu # Y3JsME8GA1UdIARIMEYwOAYKYIZIAYb9bAACBDAqMCgGCCsGAQUFBwIBFhxodHRw # czovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAoGCGCGSAGG/WwDMB0GA1UdDgQWBBRa # xLl7KgqjpepxA8Bg+S32ZXUOWDAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd # 823IDzANBgkqhkiG9w0BAQsFAAOCAQEAPuwNWiSz8yLRFcgsfCUpdqgdXRwtOhrE # 7zBh134LYP3DPQ/Er4v97yrfIFU3sOH20ZJ1D1G0bqWOWuJeJIFOEKTuP3GOYw4T # S63XX0R58zYUBor3nEZOXP+QsRsHDpEV+7qvtVHCjSSuJMbHJyqhKSgaOnEoAjwu # kaPAJRHinBRHoXpoaK+bp1wgXNlxsQyPu6j4xRJon89Ay0BEpRPw5mQMJQhCMrI2 # iiQC/i9yfhzXSUWW6Fkd6fp0ZGuy62ZD2rOwjNXpDd32ASDOmTFjPQgaGLOBm0/G # kxAG/AeB+ova+YJJ92JuoVP6EpQYhS6SkepobEQysmah5xikmmRR7zCCBY0wggR1 # oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZIhvcNAQEMBQAwZTELMAkGA1UE # BhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2lj # ZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4X # DTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVowYjELMAkGA1UEBhMCVVMxFTAT # BgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEh # MB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MIICIjANBgkqhkiG9w0B # AQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjwwIjBpM+zCpyUuySE98orYWcLh # Kac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J58soR0uRf1gU8Ug9SH8aeFaV+ # vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMHhOZ0O21x4i0MG+4g1ckgHWMp # Lc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6Zu53yEioZldXn1RYjgwrt0+n # MNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQecN4x7axxLVqGDgDEI3Y1Dek # LgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4bA3VdeGbZOjFEmjNAvwjXWkmk # wuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9WV1CdoeJl2l6SPDgohIbZpp0 # yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCUtNJhbesz2cXfSwQAzH0clcOP # 9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvoZKYz0YkH4b235kOkGLimdwHh # D5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/JvNNBERJb5RBQ6zHFynIWIgnf # fEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCPorF+CiaZ9eRpL5gdLfXZqbId # 5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOzX44LS # cV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgP # MA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0 # dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2Vy # dHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDBFBgNV # HR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRB # c3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgwBgYEVR0gADANBgkqhkiG9w0B # AQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cHvZqsoYcs7IVeqRq7IviHGmlU # Iu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8UgPITtAq3votVs/59PesMHqa # i7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTnf+hZqPC/Lwum6fI0POz3A8eH # qNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxUjG/voVA9/HYJaISfb8rbII01 # YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8jLfR+cWojayL/ErhULSd+2DrZ # 8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDCCBq4wggSWoAMCAQICEAc2N7ckVHzY # R6z9KGYqXlswDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCVVMxFTATBgNVBAoT # DERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UE # AxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MB4XDTIyMDMyMzAwMDAwMFoXDTM3 # MDMyMjIzNTk1OVowYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ # bmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2 # IFRpbWVTdGFtcGluZyBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB # AMaGNQZJs8E9cklRVcclA8TykTepl1Gh1tKD0Z5Mom2gsMyD+Vr2EaFEFUJfpIjz # aPp985yJC3+dH54PMx9QEwsmc5Zt+FeoAn39Q7SE2hHxc7Gz7iuAhIoiGN/r2j3E # F3+rGSs+QtxnjupRPfDWVtTnKC3r07G1decfBmWNlCnT2exp39mQh0YAe9tEQYnc # fGpXevA3eZ9drMvohGS0UvJ2R/dhgxndX7RUCyFobjchu0CsX7LeSn3O9TkSZ+8O # pWNs5KbFHc02DVzV5huowWR0QKfAcsW6Th+xtVhNef7Xj3OTrCw54qVI1vCwMROp # VymWJy71h6aPTnYVVSZwmCZ/oBpHIEPjQ2OAe3VuJyWQmDo4EbP29p7mO1vsgd4i # FNmCKseSv6De4z6ic/rnH1pslPJSlRErWHRAKKtzQ87fSqEcazjFKfPKqpZzQmif # tkaznTqj1QPgv/CiPMpC3BhIfxQ0z9JMq++bPf4OuGQq+nUoJEHtQr8FnGZJUlD0 # UfM2SU2LINIsVzV5K6jzRWC8I41Y99xh3pP+OcD5sjClTNfpmEpYPtMDiP6zj9Ne # S3YSUZPJjAw7W4oiqMEmCPkUEBIDfV8ju2TjY+Cm4T72wnSyPx4JduyrXUZ14mCj # WAkBKAAOhFTuzuldyF4wEr1GnrXTdrnSDmuZDNIztM2xAgMBAAGjggFdMIIBWTAS # BgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBS6FtltTYUvcyl2mi91jGogj57I # bzAfBgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/57qYrhwPTzAOBgNVHQ8BAf8EBAMC # AYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgwdwYIKwYBBQUHAQEEazBpMCQGCCsGAQUF # BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQQYIKwYBBQUHMAKGNWh0dHA6 # Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3J0 # MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdp # Q2VydFRydXN0ZWRSb290RzQuY3JsMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsGCWCG # SAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAfVmOwJO2b5ipRCIBfmbW2CFC4bAY # LhBNE88wU86/GPvHUF3iSyn7cIoNqilp/GnBzx0H6T5gyNgL5Vxb122H+oQgJTQx # Z822EpZvxFBMYh0MCIKoFr2pVs8Vc40BIiXOlWk/R3f7cnQU1/+rT4osequFzUNf # 7WC2qk+RZp4snuCKrOX9jLxkJodskr2dfNBwCnzvqLx1T7pa96kQsl3p/yhUifDV # inF2ZdrM8HKjI/rAJ4JErpknG6skHibBt94q6/aesXmZgaNWhqsKRcnfxI2g55j7 # +6adcq/Ex8HBanHZxhOACcS2n82HhyS7T6NJuXdmkfFynOlLAlKnN36TU6w7HQhJ # D5TNOXrd/yVjmScsPT9rp/Fmw0HNT7ZAmyEhQNC3EyTN3B14OuSereU0cZLXJmvk # OHOrpgFPvT87eK1MrfvElXvtCl8zOYdBeHo46Zzh3SP9HSjTx/no8Zhf+yvYfvJG # nXUsHicsJttvFXseGYs2uJPU5vIXmVnKcPA3v5gA3yAWTyf7YGcWoWa63VXAOimG # sJigK+2VQbc61RWYMbRiCQ8KvYHZE/6/pNHzV9m8BPqC3jLfBInwAM1dwvnQI38A # C+R2AibZ8GV2QqYphwlHK+Z/GqSFD/yYlvZVVCsfgPrA8g4r5db7qS9EFUrnEw4d # 2zc4GqEr9u3WfPwwggbAMIIEqKADAgECAhAMTWlyS5T6PCpKPSkHgD1aMA0GCSqG # SIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5j # LjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBU # aW1lU3RhbXBpbmcgQ0EwHhcNMjIwOTIxMDAwMDAwWhcNMzMxMTIxMjM1OTU5WjBG # MQswCQYDVQQGEwJVUzERMA8GA1UEChMIRGlnaUNlcnQxJDAiBgNVBAMTG0RpZ2lD # ZXJ0IFRpbWVzdGFtcCAyMDIyIC0gMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC # AgoCggIBAM/spSY6xqnya7uNwQ2a26HoFIV0MxomrNAcVR4eNm28klUMYfSdCXc9 # FZYIL2tkpP0GgxbXkZI4HDEClvtysZc6Va8z7GGK6aYo25BjXL2JU+A6LYyHQq4m # pOS7eHi5ehbhVsbAumRTuyoW51BIu4hpDIjG8b7gL307scpTjUCDHufLckkoHkyA # HoVW54Xt8mG8qjoHffarbuVm3eJc9S/tjdRNlYRo44DLannR0hCRRinrPibytIzN # TLlmyLuqUDgN5YyUXRlav/V7QG5vFqianJVHhoV5PgxeZowaCiS+nKrSnLb3T254 # xCg/oxwPUAY3ugjZNaa1Htp4WB056PhMkRCWfk3h3cKtpX74LRsf7CtGGKMZ9jn3 # 9cFPcS6JAxGiS7uYv/pP5Hs27wZE5FX/NurlfDHn88JSxOYWe1p+pSVz28BqmSEt # Y+VZ9U0vkB8nt9KrFOU4ZodRCGv7U0M50GT6Vs/g9ArmFG1keLuY/ZTDcyHzL8Iu # INeBrNPxB9ThvdldS24xlCmL5kGkZZTAWOXlLimQprdhZPrZIGwYUWC6poEPCSVT # 8b876asHDmoHOWIZydaFfxPZjXnPYsXs4Xu5zGcTB5rBeO3GiMiwbjJ5xwtZg43G # 7vUsfHuOy2SJ8bHEuOdTXl9V0n0ZKVkDTvpd6kVzHIR+187i1Dp3AgMBAAGjggGL # MIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAK # BggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwHwYD # VR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0OBBYEFGKK3tBh/I8x # FO2XC809KpQU31KcMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwzLmRpZ2lj # ZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBp # bmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUFBzABhhhodHRwOi8v # b2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6Ly9jYWNlcnRzLmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3Rh # bXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAFWqKhrzRvN4Vzcw/HXjT9aF # I/H8+ZU5myXm93KKmMN31GT8Ffs2wklRLHiIY1UJRjkA/GnUypsp+6M/wMkAmxMd # sJiJ3HjyzXyFzVOdr2LiYWajFCpFh0qYQitQ/Bu1nggwCfrkLdcJiXn5CeaIzn0b # uGqim8FTYAnoo7id160fHLjsmEHw9g6A++T/350Qp+sAul9Kjxo6UrTqvwlJFTU2 # WZoPVNKyG39+XgmtdlSKdG3K0gVnK3br/5iyJpU4GYhEFOUKWaJr5yI+RCHSPxzA # m+18SLLYkgyRTzxmlK9dAlPrnuKe5NMfhgFknADC6Vp0dQ094XmIvxwBl8kZI4DX # NlpflhaxYwzGRkA7zl011Fk+Q5oYrsPJy8P7mxNfarXH4PMFw1nfJ2Ir3kHJU7n/ # NBBn9iYymHv+XEKUgZSCnawKi8ZLFUrTmJBFYDOA4CPe+AOk9kVH5c64A0JH6EE2 # cXet/aLol3ROLtoeHYxayB6a1cLwxiKoT5u92ByaUcQvmvZfpyeXupYuhVfAYOd4 # Vn9q78KVmksRAsiCnMkaBXy6cbVOepls9Oie1FqYyJ+/jbsYXEP10Cro4mLueATb # vdH7WwqocH7wl4R44wgDXUcsY6glOJcB0j862uXl9uab3H4szP8XTE0AotjWAQ64 # i+7m4HJViSwnGWH2dwGMMYIFXTCCBVkCAQEwgYYwcjELMAkGA1UEBhMCVVMxFTAT # BgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEx # MC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2lnbmluZyBD # QQIQCm8Gpkn9Nk686mPMJKDEczANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3 # AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisG # AQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCDIlqRKJM49 # LNKFcGAbLdJkVgNSyXPFDr5KoxVrld+h6TANBgkqhkiG9w0BAQEFAASCAQBUsOzn # STnZxtk16rAMIWKCsvOjBfTGtEjZuN/qQ2QhpSIi0uUO+NEIR7m+4ajOT0naFZGj # XCL61R7qWgwh8LwfMOtoCr99umyidBrQvalc3yl4fG470PUyyEjbSdcMZ1emilwT # FW9cORE+uZlgs3ASC6mMmbqx58/a9JAsWz9aaWSECdE482PkjEXUaZsSaUcHG55o # AjMkTnh3GIf2XEmARMTCzV3bQ0gdyD4tibyuF+l6HYDOUiVrfAUnp44uzgY06wgI # Eb61Op3c3iSWuRMQodJEjMmQdV5x2NHQOk9zyEqRlg6uaG6CEnESO38nWtPCHwyY # 3kPCAJliJuzQ8AupoYIDIDCCAxwGCSqGSIb3DQEJBjGCAw0wggMJAgEBMHcwYzEL # MAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJE # aWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBD # QQIQDE1pckuU+jwqSj0pB4A9WjANBglghkgBZQMEAgEFAKBpMBgGCSqGSIb3DQEJ # AzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIzMDMxOTIyMTExMFowLwYJ # KoZIhvcNAQkEMSIEIOUF3lKakngQ2MMFm2CCJvF+cyvz4ok4NilU9iXcyNxwMA0G # CSqGSIb3DQEBAQUABIICADyvryUuSV3uPTqg47QP4B4VOX1mKUXHFlijgvGW5Xox # o1PLEzHfoPsp00fa4ayGCIFzOzkUgEnn8+kNoUEEOkr3oX7IRiPVEqqtjEfYdodt # m3frIlp+QaFA/Cc8xq3pRzcQOnr0BHDEgXlYrdEpXvydEdiDTr2pVJL+YdXO5jRM # CKJsIB8/YrqrQzV/TXS55rJ96d+xj0ceFUyKqePhmMUMkmRc8pN1en42hoR2kfJP # XhTIELnbLcK7b9ysH2mcbprv7JZINeLUI1x4dq09W5VDbPaRJ9YVczDT1Ugpdkzm # +tChaQi2UAkww5ZHCMT94Rt+IBPgKyjK0u+5wrB4cqAGIyo7LK5ne3H0FDbk5tAq # JHaZk2AZirNW61juvXyQMh4m8reIH5yOcuLik/0Q90s05OS7hul/4pz4Kv0KMnmR # aCnvN/cpe8ZCwH6bA/WmjtO8OMbxTdKdZET+XBdPbhb/9YKDzffrxfiGcf682Lvl # e/9ERZpTh9/IuYrUrDJblIOswrJhX/wYj9nJ4qQ6ySxggso4q882kEimd1tuf5iE # 40KzBqt7cvqAA49FZ4TY0F71cmEYu8ZCXOu3jl4OflB9W+UcCCKiK/7De9o6EY0O # 1H1bgb9nQ8t+lPYRFTl30iCoa4EiPBcYWeCBAaWjE/5Mb5U+TPyQYKDLB/UozFBv # SIG # End signature block |