PSDev.psm1

#Region '.\prefix.ps1' 0
# The content of this file will be prepended to the top of the psm1 module file. This is useful for custom module setup is needed on import.
#EndRegion '.\prefix.ps1' 2
#Region '.\Private\Assert-FolderExist.ps1' 0
function Assert-FolderExist
{
    <#
    .SYNOPSIS
        Verify and create folder
    .DESCRIPTION
        Verifies that a folder path exists, if not it will create it
    .PARAMETER Path
        Defines the path to be validated
    .EXAMPLE
        'C:\Temp' | Assert-FolderExist
 
        This will verify that the path exists and if it does not the folder will be created
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string]
        $Path
    )

    process
    {
        $exists = Test-Path -Path $Path -PathType Container
        if (!$exists)
        {
            $null = New-Item -Path $Path -ItemType Directory
        }
    }
}
#EndRegion '.\Private\Assert-FolderExist.ps1' 31
#Region '.\Private\Invoke-GarbageCollect.ps1' 0
function Invoke-GarbageCollect
{
    <#
    .SYNOPSIS
        Calls system.gc collect method. Purpose is mainly for readability.
    .DESCRIPTION
        Calls system.gc collect method. Purpose is mainly for readability.
    .EXAMPLE
        Invoke-GarbageCollect
    #>

    [system.gc]::Collect()
}
#EndRegion '.\Private\Invoke-GarbageCollect.ps1' 13
#Region '.\Private\pslog.ps1' 0
function pslog
{
    <#
    .SYNOPSIS
        This is simple logging function that automatically log to file. Logging to console is maintained.
    .DESCRIPTION
        This is simple logging function that automatically log to file. Logging to console is maintained.
    .PARAMETER Severity
        Defines the type of log, valid vales are, Success,Info,Warning,Error,Verbose,Debug
    .PARAMETER Message
        Defines the message for the log entry
    .PARAMETER Source
        Defines a source, this is useful to separate log entries in categories for different stages of a process or for each function, defaults to default
    .PARAMETER Throw
        Specifies that when using severity error pslog will throw. This is useful in catch statements so that the terminating error is propagated upwards in the stack.
    .PARAMETER LogDirectoryOverride
        Defines a hardcoded log directory to write the log file to. This defaults to %appdatalocal%\<modulename\logs.
    .PARAMETER DoNotLogToConsole
        Specifies that logs should only be written to the log file and not to the console.
    .EXAMPLE
        pslog Verbose 'Successfully wrote to logfile'
        Explanation of the function or its result. You can include multiple examples with additional .EXAMPLE lines
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Sole purpose of function is logging, including console')]
    [cmdletbinding()]
    param(
        [parameter(Position = 0)]
        [ValidateSet('Success', 'Info', 'Warning', 'Error', 'Verbose', 'Debug')]
        [Alias('Type')]
        [string]
        $Severity,

        [parameter(Mandatory, Position = 1)]
        [string]
        $Message,

        [parameter(position = 2)]
        [string]
        $source = 'default',

        [parameter(Position = 3)]
        [switch]
        $Throw,

        [parameter(Position = 4)]
        [string]
        $LogDirectoryOverride,

        [parameter(Position = 5)]
        [switch]
        $DoNotLogToConsole
    )

    begin
    {
        if (-not $LogDirectoryOverride)
        {
            $localappdatapath = [Environment]::GetFolderPath('localapplicationdata') # ie C:\Users\<username>\AppData\Local
            $modulename = $MyInvocation.MyCommand.Module
            $logdir = "$localappdatapath\$modulename\logs"
        }
        else
        {
            $logdir = $LogDirectoryOverride
        }
        $logdir | Assert-FolderExist -Verbose:$VerbosePreference
        $timestamp = (Get-Date)
        $logfilename = ('{0}.log' -f $timestamp.ToString('yyy-MM-dd'))
        $timestampstring = $timestamp.ToString('yyyy-MM-ddThh:mm:ss.ffffzzz')
    }

    process
    {
        switch ($Severity)
        {
            'Success'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                if (-not $DoNotLogToConsole)
                {
                    Write-Host -Object "SUCCESS: $timestampstring`t$source`t$message" -ForegroundColor Green
                }
            }
            'Info'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                if (-not $DoNotLogToConsole)
                {
                    Write-Information -MessageData "$timestampstring`t$source`t$message"
                }
            }
            'Warning'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                if (-not $DoNotLogToConsole)
                {
                    Write-Warning -Message "$timestampstring`t$source`t$message"
                }
            }
            'Error'
            {
                "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                if (-not $DoNotLogToConsole)
                {
                    Write-Error -Message "$timestampstring`t$source`t$message"
                }
                if ($throw)
                {
                    throw
                }
            }
            'Verbose'
            {
                if ($VerbosePreference -ne 'SilentlyContinue')
                {
                    "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                }
                if (-not $DoNotLogToConsole)
                {
                    Write-Verbose -Message "$timestampstring`t$source`t$message"
                }
            }
            'Debug'
            {
                if ($DebugPreference -ne 'SilentlyContinue')
                {
                    "$timestampstring`t$psitem`t$source`t$message" | Add-Content -Path "$logdir\$logfilename" -Encoding utf8 -WhatIf:$false
                }
                if (-not $DoNotLogToConsole)
                {
                    Write-Debug -Message "$timestampstring`t$source`t$message"
                }
            }
        }
    }
}
#EndRegion '.\Private\pslog.ps1' 137
#Region '.\Private\Write-PSProgress.ps1' 0
function Write-PSProgress
{
    <#
    .SYNOPSIS
        Wrapper for PSProgress
    .DESCRIPTION
        This function will automatically calculate items/sec, eta, time remaining
        as well as set the update frequency in case the there are a lot of items processing fast.
    .PARAMETER Activity
        Defines the activity name for the progressbar
    .PARAMETER Id
        Defines a unique ID for this progressbar, this is used when nesting progressbars
    .PARAMETER Target
        Defines a arbitrary text for the currently processed item
    .PARAMETER ParentId
        Defines the ID of a parent progress bar
    .PARAMETER Completed
        Explicitly tells powershell to set the progress bar as completed removing
        it from view. In some cases the progress bar will linger if this is not done.
    .PARAMETER Counter
        The currently processed items counter
    .PARAMETER Total
        The total number of items to process
    .PARAMETER StartTime
        Sets the start datetime for the progressbar, this is required to calculate items/sec, eta and time remaining
    .PARAMETER DisableDynamicUpdateFrquency
        Disables the dynamic update frequency function and every item will update the status of the progressbar
    .PARAMETER NoTimeStats
        Disables calculation of items/sec, eta and time remaining
    .EXAMPLE
        1..10000 | foreach-object -begin {$StartTime = Get-Date} -process {
            Write-PSProgress -Activity 'Looping' -Target $PSItem -Counter $PSItem -Total 10000 -StartTime $StartTime
        }
        Explanation of the function or its result. You can include multiple examples with additional .EXAMPLE lines
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Standard')]
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = 'Completed')]
        [string]
        $Activity,

        [Parameter(Position = 1, ParameterSetName = 'Standard')]
        [Parameter(Position = 1, ParameterSetName = 'Completed')]
        [ValidateRange(0, 2147483647)]
        [int]
        $Id,

        [Parameter(Position = 2, ParameterSetName = 'Standard')]
        [string]
        $Target,

        [Parameter(Position = 3, ParameterSetName = 'Standard')]
        [Parameter(Position = 3, ParameterSetName = 'Completed')]
        [ValidateRange(-1, 2147483647)]
        [int]
        $ParentId,

        [Parameter(Position = 4, ParameterSetname = 'Completed')]
        [switch]
        $Completed,

        [Parameter(Mandatory = $true, Position = 5, ParameterSetName = 'Standard')]
        [long]
        $Counter,

        [Parameter(Mandatory = $true, Position = 6, ParameterSetName = 'Standard')]
        [long]
        $Total,

        [Parameter(Position = 7, ParameterSetName = 'Standard')]
        [datetime]
        $StartTime,

        [Parameter(Position = 8, ParameterSetName = 'Standard')]
        [switch]
        $DisableDynamicUpdateFrquency,

        [Parameter(Position = 9, ParameterSetName = 'Standard')]
        [switch]
        $NoTimeStats
    )

    # Define current timestamp
    $TimeStamp = (Get-Date)

    # Define a dynamic variable name for the global starttime variable
    $StartTimeVariableName = ('ProgressStartTime_{0}' -f $Activity.Replace(' ', ''))

    # Manage global start time variable
    if ($PSBoundParameters.ContainsKey('Completed') -and (Get-Variable -Name $StartTimeVariableName -Scope Global -ErrorAction SilentlyContinue))
    {
        # Remove the global starttime variable if the Completed switch parameter is users
        try
        {
            Remove-Variable -Name $StartTimeVariableName -ErrorAction Stop -Scope Global
        }
        catch
        {
            throw $_
        }
    }
    elseif (-not (Get-Variable -Name $StartTimeVariableName -Scope Global -ErrorAction SilentlyContinue))
    {
        # Global variable do not exist, create global variable
        if ($null -eq $StartTime)
        {
            # No start time defined with parameter, use current timestamp as starttime
            Set-Variable -Name $StartTimeVariableName -Value $TimeStamp -Scope Global
            $StartTime = $TimeStamp
        }
        else
        {
            # Start time defined with parameter, use that value as starttime
            Set-Variable -Name $StartTimeVariableName -Value $StartTime -Scope Global
        }
    }
    else
    {
        # Global start time variable is defined, collect and use it
        $StartTime = Get-Variable -Name $StartTimeVariableName -Scope Global -ErrorAction Stop -ValueOnly
    }

    # Define frequency threshold
    $Frequency = [Math]::Ceiling($Total / 100)
    switch ($PSCmdlet.ParameterSetName)
    {
        'Standard'
        {
            # Only update progress is any of the following is true
            # - DynamicUpdateFrequency is disabled
            # - Counter matches a mod of defined frequecy
            # - Counter is 0
            # - Counter is equal to Total (completed)
            if (($DisableDynamicUpdateFrquency) -or ($Counter % $Frequency -eq 0) -or ($Counter -eq 1) -or ($Counter -eq $Total))
            {

                # Calculations for both timestats and without
                $Percent = [Math]::Round(($Counter / $Total * 100), 0)

                # Define count progress string status
                $CountProgress = ('{0}/{1}' -f $Counter, $Total)

                # If percent would turn out to be more than 100 due to incorrect total assignment revert back to 100% to avoid that write-progress throws
                if ($Percent -gt 100)
                {
                    $Percent = 100
                }

                # Define write-progress splat hash
                $WriteProgressSplat = @{
                    Activity         = $Activity
                    PercentComplete  = $Percent
                    CurrentOperation = $Target
                }

                # Add ID if specified
                if ($Id)
                {
                    $WriteProgressSplat.Id = $Id
                }

                # Add ParentID if specified
                if ($ParentId)
                {
                    $WriteProgressSplat.ParentId = $ParentId
                }

                # Calculations for either timestats and without
                if ($NoTimeStats)
                {
                    $WriteProgressSplat.Status = ('{0} - {1}%' -f $CountProgress, $Percent)
                }
                else
                {
                    # Total seconds elapsed since start
                    $TotalSeconds = ($TimeStamp - $StartTime).TotalSeconds

                    # Calculate items per sec processed (IpS)
                    $ItemsPerSecond = ([Math]::Round(($Counter / $TotalSeconds), 2))

                    # Calculate seconds spent per processed item (for ETA)
                    $SecondsPerItem = if ($Counter -eq 0)
                    {
                        0
                    }
                    else
                    {
 ($TotalSeconds / $Counter)
                    }

                    # Calculate seconds remainging
                    $SecondsRemaing = ($Total - $Counter) * $SecondsPerItem
                    $WriteProgressSplat.SecondsRemaining = $SecondsRemaing

                    # Calculate ETA
                    $ETA = $(($Timestamp).AddSeconds($SecondsRemaing).ToShortTimeString())

                    # Add findings to write-progress splat hash
                    $WriteProgressSplat.Status = ('{0} - {1}% - ETA: {2} - IpS {3}' -f $CountProgress, $Percent, $ETA, $ItemsPerSecond)
                }

                # Call writeprogress
                Write-Progress @WriteProgressSplat
            }
        }
        'Completed'
        {
            Write-Progress -Activity $Activity -Id $Id -Completed
        }
    }
}
#EndRegion '.\Private\Write-PSProgress.ps1' 214
#Region '.\Public\Add-NumberFormater.ps1' 0
function Add-NumberFormater {
    <#
    .DESCRIPTION
        Adding formater capabilities by overwriting the ToString method of the input double value
    .PARAMETER InputObject
        Defines the input value to process
    .PARAMETER Type
        Defines what type of value it is and what units to use. Available values is Standard and DataSize
    .EXAMPLE
        Add-NumberFormater -InputObject 2138476234 -Type DataSize
        Processes the number 2138476234 and returns the value with the replaced ToString() method. This case would return "1,99 GB"
    #>


    [CmdletBinding()] # Enabled to support verbose
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Parameter use is not correctly identified by PSScriptAnalyzer')]
    param(
        [Parameter(Mandatory, ValueFromPipeline)][Alias('Double', 'Number')][double[]]$InputObject,
        [ValidateSet('DataSize', 'Standard')][string]$Type = 'Standard'
    )

    begin {
        $Configuration = @{
            DataSize = @{
                Base  = 1024
                Units = @('', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')
            }
            Standard = @{
                Base  = 1000
                Units = @('', 'K', 'MN', 'MD', 'BN', 'BD', 'TN', 'TD')
            }
        }
    }

    process {
        $InputObject | foreach-object {
            $CurrentNumber = $_
            $TempCopyOfCurrentNumber = $CurrentNumber

            if ($TempCopyOfCurrentNumber -lt $Configuration.($Type).Base) {
                $DisplayString = "'{0:N}'" -f [double]($TempCopyOfCurrentNumber)
            } else {
                $i = 0
                while ($TempCopyOfCurrentNumber -ge $Configuration.($Type).Base -and $i -lt $Configuration.($Type).Units.Length - 1 ) {
                    $TempCopyOfCurrentNumber /= $Configuration.($Type).Base
                    $i++
                }
                $DisplayString = "'{0:N2} {1}'" -f [double]($TempCopyOfCurrentNumber), ($Configuration.($Type).Units[$i])
            }

            $NewObject = $CurrentNumber | Add-Member -MemberType ScriptMethod -Name ToString -Value ([Scriptblock]::Create($DisplayString)) -Force -PassThru
            return $NewObject
        }
    }
    end { }
}
#EndRegion '.\Public\Add-NumberFormater.ps1' 56
#Region '.\Public\Debug-String.ps1' 0
<#
 
  Prerequisites: PowerShell v5.1 and above (verified; may also work in earlier versions)
  License: MIT
  Author: Michael Klement <mklement0@gmail.com>
 
#>


function Debug-String
{

    <#
        .SYNOPSIS
        Outputs a string in diagnostic form or as source code.
 
        .DESCRIPTION
        Prints a string with control or hidden characters visualized, and optionally
        all non-ASCII-range Unicode characters represented as escape sequences.
 
        With -AsSourceCode, the result is printed in single-line form as a
        double-quoted PowerShell string literal that is reusable as source code,
 
        Common control characters are visualized using PowerShell's own escaping
        notation by default, such as
        "`t" for a tab, "`r" for a CR, but a LF is visualized as itself, as an
        actual newline, unless you specify -SingleLine.
 
        As an alternative, if you want ASCII-range control characters visualized in caret notation
        (see https://en.wikipedia.org/wiki/Caret_notation), similar to cat -A on Linux,
        use -CaretNotation. E.g., ^M then represents a CR; but note that a LF is
        always represented as "$" followed by an actual newline.
 
        Any other control characters as well as otherwise hidden characters or
        format / punctuation characters in the non-ASCII range are represented in
        `u{hex-code-point} notation.
 
        To print space characters as themselves, use -NoSpacesAsDots.
 
        $null inputs are accepted, but a warning is issued.
 
        .PARAMETER InputObject
        Defines the string to analyze
 
        .PARAMETER CaretNotation
        Causes LF to be visualized as "$" and all other ASCII-range control characters
        in caret notation, similar to `cat -A` on Linux.
 
        .PARAMETER Delimiters
        You may optionally specify delimiters that the visualization of each input string is enclosed
        in as a a whole its boundaries. You may specify a single string or a 2-element array.
 
        .PARAMETER NoSpacesAsDots
        By default, space chars. are visualized as "·", the MIDDLE DOT char. (U+00B7)
 
        Use this switch to represent spaces as themselves.
 
        .PARAMETER NoEmphasis
        By default, those characters (other than spaces) that aren't output as themselves,
        i.e. control characters and, if requested with -UnicodeEscapes, non-ASCII-range characters,
        are highlighted by color inversion, using ANSI (VT) escape sequences.
 
        Use this switch to turn off this highlighting.
 
        Note that if $PSStyle.OutputRendering = 'PlainText' is in effect, the highlighting
        isn't *shown* even *without* -NoEmphasis, but the escape sequences are still part
        of the output string. Only -NoEmphasis prevents inclusion of these escape sequences.
 
        .PARAMETER AsSourceCode
        Outputs each input string as a double-quoted PowerShell string
        that is reusable in source code, with embedded double quotes, backticks,
        and "$" signs backtick-escaped.
 
        Use -SingleLine to get a single-line representation.
        Control characters that have no native PS escape sequence are represented
        using `u{<hex-code-point} notation, which will only work in PowerShell *Core*
        (v6+) source code.
 
        .PARAMETER SingleLine
        Requests a single-line representation, where LF characters are represented
        as `n instead of actual line breaks.
 
        .PARAMETER UnicodeEscapes
        Requests that all non-ASCII-range characters - such as accented letters - in
        the input string be represented as Unicode escape sequences in the form
        `u{hex-code-point}.
 
        Whe cominbed with -AsSourceCode, the result is a PowerShell string literal
        composed of ASCII-range characters only, but note that only PowerShell *Core*
        (v6+) understands such Unicode escapes.
 
        By default, only control characters that don't have a native PS escape
        sequence / cannot be represented with caret notation are represented this way.
 
        .EXAMPLE
        PS> "a`ab`t c`0d`r`n" | Debug-String -Delimiters [, ]
        [a`0b`t·c`0d`r`
        ]
 
        .EXAMPLE
        PS> "a`ab`t c`0d`r`n" | Debug-String -CaretNotation
        a^Gb^I c^@d^M$
 
        .EXAMPLE
        PS> "a-ü`u{2028}" | Debug-String -UnicodeEscapes # The dash is an em-dash (U+2014)
        a·`u{2014}·`u{fc}
 
        .EXAMPLE
        PS> "a`ab`t c`0d`r`n" | Debug-String -AsSourceCode -SingleLine # roundtrip
        "a`ab`t c`0d`r`n"
    #>


    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'NoSpacesAsDots', Justification = 'False positive')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'NoEmphasis', Justification = 'False positive')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'SingleLine', Justification = 'False positive')]
    [CmdletBinding(DefaultParameterSetName = 'Standard', PositionalBinding = $false)]
    param(
        [Parameter(ValueFromPipeline, Mandatory, ParameterSetName = 'Standard', Position = 0)]
        [Parameter(ValueFromPipeline, Mandatory, ParameterSetName = 'Caret', Position = 0)]
        [Parameter(ValueFromPipeline, Mandatory, ParameterSetName = 'AsSourceCode', Position = 0)]
        [AllowNull()]
        [object[]] $InputObject,

        [Parameter(ParameterSetName = 'Standard')]
        [Parameter(ParameterSetName = 'Caret')]
        [string[]] $Delimiters, # for enclosing the visualized strings as a whole - probably rarely used.

        [Parameter(ParameterSetName = 'Caret')]
        [switch] $CaretNotation,

        [Parameter(ParameterSetName = 'Standard')]
        [Parameter(ParameterSetName = 'Caret')]
        [switch] $NoSpacesAsDots,
        [Parameter(ParameterSetName = 'Caret')]
        [Parameter(ParameterSetName = 'Standard')]
        [switch] $NoEmphasis,

        [Parameter(ParameterSetName = 'AsSourceCode')]
        [switch] $AsSourceCode,

        [Parameter(ParameterSetName = 'Standard')]
        [Parameter(ParameterSetName = 'AsSourceCode')]
        [switch] $SingleLine,

        [Parameter(ParameterSetName = 'Standard')]
        [Parameter(ParameterSetName = 'Caret')]
        [Parameter(ParameterSetName = 'AsSourceCode')]
        [switch] $UnicodeEscapes

    )

    begin
    {
        $esc = [char] 0x1b
        if ($UnicodeEscapes)
        {
            $re = [regex] '(?s).' # We must look at *all* characters.
        }
        else
        {
            # Only control / separator / punctuation chars.
            # * \p{C} matches any Unicode control / format/ invisible characters, both inside and outside
            # the ASCII range; note that tabs (`t) are control character too, but not spaces; it comprises
            # the following Unicode categories: Control, Format, Private_Use, Surrogate, Unassigned
            # * \p{P} comprises punctuation characters.
            # * \p{Z} comprises separator chars., including spaces, but not other ASCII whitespace, which is in the Control category.
            # Note: For -AsSourceCode we include ` (backticks) too.
            $re = if ($AsSourceCode)
            {
                [regex] '[`\p{C}\p{P}\p{Z}]'
            }
            else
            {
                [regex] '[\p{C}\p{P}\p{Z}]'
            }
        }
        $openingDelim = $closingDelim = ''
        if ($Delimiters)
        {
            $openingDelim = $Delimiters[0]
            $closingDelim = $Delimiters[1]
            if (-not $closingDelim)
            {
                $closingDelim = $openingDelim
            }
        }
    }
    process
    {
        if ($null -eq $InputObject)
        {
            Write-Warning 'Ignoring $null input.'; return
        }
        foreach ($str in $InputObject)
        {
            if ($null -eq $str)
            {
                Write-Warning 'Ignoring $null input.'; continue
            }
            if ($str -isnot [string])
            {
                $str = -join ($str | Out-String -Stream)
            }
            $strViz = $re.Replace($str, {
                    param($match)
                    $char = [char] $match.Value[0]
                    $codePoint = [uint16] $char
                    $sbToUnicodeEscape = { '`u{' + '{0:x}' -f [int] $Args[0] + '}' }
                    # wv -v ('in [{0}]' -f [char] $match.Value)
                    $vizChar =
                    if ($CaretNotation)
                    {
                        if ($codePoint -eq 0xA)
                        {
                            # LF -> $<newline>
                            '$' + $char
                        }
                        elseif ($codePoint -eq 0x20)
                        {
                            # space char.
                            if ($NoSpacesAsDots)
                            {
                                ' '
                            }
                            else
                            {
                                '·'
                            }
                        }
                        elseif ($codePoint -ge 0 -and $codePoint -le 31 -or $codePoint -eq 127)
                        {
                            # If it's a control character in the ASCII range,
                            # use caret notation too (C0 range).
                            # See https://en.wikipedia.org/wiki/Caret_notation
                            '^' + [char] ((64 + $codePoint) -band 0x7f)
                        }
                        elseif ($codePoint -ge 128)
                        {
                            # Non-ASCII (control) character -> `u{<hex-code-point>}
                            & $sbToUnicodeEscape $codePoint
                        }
                        else
                        {
                            $char
                        }
                    }
                    else
                    {
                        # -not $CaretNotation
                        # Translate control chars. that have native PS escape sequences
                        # into these escape sequences.
                        switch ($codePoint)
                        {
                            0
                            {
                                '`0'; break
                            }
                            7
                            {
                                '`a'; break
                            }
                            8
                            {
                                '`b'; break
                            }
                            9
                            {
                                '`t'; break
                            }
                            11
                            {
                                '`v'; break
                            }
                            12
                            {
                                '`f'; break
                            }
                            10
                            {
                                if ($SingleLine)
                                {
                                    '`n'
                                }
                                else
                                {
                                    "`n"
                                }; break
                            }
                            13
                            {
                                '`r'; break
                            }
                            27
                            {
                                '`e'; break
                            }
                            32
                            {
                                if ($AsSourceCode -or $NoSpacesAsDots)
                                {
                                    ' '
                                }
                                else
                                {
                                    '·'
                                }; break
                            } # Spaces are visualized as middle dots by default.
                            default
                            {
                                # Note: 0x7f (DELETE) is technically still in the ASCII range, but it is a control char. that should be visualized as such
                                # (and has no dedicated escape sequence).
                                if ($codePoint -ge 0x7f)
                                {
                                    & $sbToUnicodeEscape $codePoint
                                }
                                elseif ($AsSourceCode -and $codePoint -eq 0x60)
                                {
                                    # ` (backtick)
                                    '``'
                                }
                                else
                                {
                                    $char
                                }
                            }
                        } # switch
                    }
                    # Return the visualized character.
                    if (-not ($NoEmphasis -or $AsSourceCode) -and $char -ne ' ' -and $vizChar -cne $char)
                    {
                        # Highlight a visualized character that isn't visualized as itself (apart from spaces)
                        # by inverting its colors, using VT / ANSI escape sequences
                        "$esc[7m$vizChar$esc[m"
                    }
                    else
                    {
                        $vizChar
                    }
                }) # .Replace

            # Output
            if ($AsSourceCode)
            {
                '"{0}"' -f ($strViz -replace '"', '`"' -replace '\$', '`$')
            }
            else
            {
                if ($CaretNotation)
                {
                    # If a string *ended* in a newline, our visualization now has
                    # a trailing LF, which we remove.
                    $strViz = $strViz -replace '(?s)^(.*\$)\n$', '$1'
                }
                $openingDelim + $strViz + $closingDelim
            }
        }
    } # process

} # function
#EndRegion '.\Public\Debug-String.ps1' 359
#Region '.\Public\Get-Office365IPURL.ps1' 0
function Get-Office365IPURL
{
    <#
      .DESCRIPTION
      Retreive a list of ip and urls required for communication to and from Office 365.
 
      .PARAMETER Services
      Defines which services to retreive IP and URLs for. Valid values are Skype,Exchange,Sharepoint.
      Note that Teams is included in the Skype ruleset and OneDrive is included in the Sharepoint ruleset.
 
      .PARAMETER OnlyRequired
      Defines that only rules that are required are returned. This will exclude optional optimize rules.
 
      .PARAMETER Types
      Defines what type of rules to return. Valid values are URL,IP4,IP6
 
      .PARAMETER OutputFormat
      Defines the output format, defaults to an array of objects. Valid values are Object and JSON as of now. If a specific format is
      needed for a firewall please raise a issue with the instructions for the format and it is possible to create preset for it.
 
      .PARAMETER Office365IPURL
      Defines the URL to the Office 365 IP URL Endpoint. Defaults to 'https://endpoints.office.com/endpoints/worldwide?clientrequestid=b10c5ed1-bad1-445f-b386-b919946339a7'.
      Provided as parameter to allow queries to other environments than worldwide as well as keep agility if Microsoft would change URL.
 
      .EXAMPLE
      Get-Office365IPURL -Services Exchange,Skype -OnlyRequired -Types IP4,URL -Outputformat JSON
 
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Services', Justification = 'False positive')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Types', Justification = 'False positive')]
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('Skype', 'Exchange', 'Sharepoint')]
        [string[]]
        $Services = @('Skype', 'Exchange', 'Sharepoint'),

        [Parameter()]
        [switch]
        $OnlyRequired,

        [Parameter()]
        [ValidateSet('URL', 'IP4', 'IP6')]
        [string[]]
        $Types = @('URL', 'IP4', 'IP6'),

        [Parameter()]
        [ValidateSet('Object', 'JSON')]
        [string]
        $OutputFormat = 'Object',

        [Parameter()]
        [string]
        $Office365IPURL = 'https://endpoints.office.com/endpoints/worldwide?clientrequestid=b10c5ed1-bad1-445f-b386-b919946339a7'
    )

    $ErrorActionPreference = 'Stop'

    # Get latest IP URL info
    $Office365Endpoints = Invoke-RestMethod -Uri $Office365IPURL -Method Get

    # Import net module
    Import-Module indented.net.ip

    # Loop through rules
    $Result = $Office365Endpoints | Where-Object { $Services -contains $_.ServiceArea } | ForEach-Object {
        $CurrentRule = $PSItem

        $ObjectHash = [ordered]@{
            Group    = ''
            Service  = $CurrentRule.ServiceArea
            Type     = ''
            Protocol = ''
            Port     = $null
            Endpoint = ''
            Required = $CurrentRule.Required
        }

        $CurrentRule.URLs | Where-Object { $_ -ne '' -and $_ -ne $null } | ForEach-Object {
            $ObjectHash.Type = 'URL'
            $ObjectHash.Endpoint = $PSItem

            $CurrentRule.TCPPorts -split (',') | Where-Object { $_ -ne '' } | ForEach-Object {
                $ObjectHash.Protocol = 'TCP'
                $ObjectHash.Port = $PSItem
                $ObjectHash.Group = $CurrentRule.ServiceArea + '_' + 'TCP' + '_' + "$PSItem" + '_' + 'URL'
                [pscustomobject]$ObjectHash
            }
            $CurrentRule.UDPPorts -split (',') | Where-Object { $_ -ne '' } | ForEach-Object {
                $ObjectHash.Protocol = 'UDP'
                $ObjectHash.Port = $PSItem
                $ObjectHash.Group = $CurrentRule.ServiceArea + '_' + 'UDP' + '_' + "$PSItem" + '_' + 'URL'
                [pscustomobject]$ObjectHash
            }
        }
        # Process IPs
        $CurrentRule.ips | Where-Object { $_ -ne '' -and $_ -ne $null } | ForEach-Object {
            if ($PSItem -like '*:*')
            {
                $ObjectHash.Type = 'IP6'
            }
            else
            {
                $ObjectHash.Type = 'IP4'
            }
            $ObjectHash.Endpoint = $PSItem

            $CurrentRule.TCPPorts -split (',') | Where-Object { $_ -ne '' } | ForEach-Object {
                $ObjectHash.Protocol = 'TCP'
                $ObjectHash.Port = $PSItem
                $ObjectHash.Group = $CurrentRule.ServiceArea + '_' + 'TCP' + '_' + "$PSItem" + '_' + 'IP'
                [pscustomobject]$ObjectHash
            }
            $CurrentRule.UDPPorts -split (',') | Where-Object { $_ -ne '' } | ForEach-Object {
                $ObjectHash.Protocol = 'UDP'
                $ObjectHash.Port = $PSItem
                $ObjectHash.Group = $CurrentRule.ServiceArea + '_' + 'UDP' + '_' + "$PSItem" + '_' + 'IP'
                [pscustomobject]$ObjectHash
            }
        }
    } | Where-Object { $Types -contains $PSItem.Type }

    switch ($OutputFormat)
    {
        'Object'
        {
            if ($OnlyRequired)
            {
                $Result | Where-Object { $_.required -eq $true } | Sort-Object -Property Group | Format-Table
            }
            else
            {
                $Result | Sort-Object -Property Group | Format-Table
            }
        }
        'JSON'
        {
            $JSONHash = [ordered]@{}
            $Result | Group-Object -Property Protocol | ForEach-Object {
                $CurrentProtocolGroup = $PSItem

                # Create protocol node if it does not exist
                if (-not $JSONHash.Contains($CurrentProtocolGroup.Name))
                {
                    $JSONHash.Add($CurrentProtocolGroup.Name, [ordered]@{})
                }
                $CurrentProtocolGroup.Group | Group-Object -Property Port | ForEach-Object {
                    $CurrentPortGroup = $PSItem

                    # Create port node if it does not exists
                    if (-not $JSONHash.$($CurrentProtocolGroup.Name).Contains($CurrentPortGroup.Name))
                    {
                        $JSONHash.$($CurrentProtocolGroup.Name).Add($CurrentPortGroup.Name, [ordered]@{})
                    }

                    $CurrentPortGroup.Group | Group-Object -Property Type | ForEach-Object {
                        $CurrentTypeGroup = $PSItem
                        $EndpointArray = [string[]]($CurrentTypeGroup.Group.Endpoint)
                        $JSONHash.$($CurrentProtocolGroup.Name).$($CurrentPortGroup.Name).Add($CurrentTypeGroup.Name, $EndpointArray)
                    }
                }
            }
            $JSONHash | ConvertTo-Json -Depth 10
        }
    }
}
#EndRegion '.\Public\Get-Office365IPURL.ps1' 167
#Region '.\Public\Test-Office365IPURL.ps1' 0
function Test-Office365IPURL
{
    <#
      .DESCRIPTION
      Retreive a list of ip and urls required for communication to and from Office 365.
 
      .PARAMETER IP
      Defines the IP to search for with in the scopes of rules returned from Office 365
 
      .PARAMETER Office365IPURL
      Defines the URL to the Office 365 IP URL Endpoint. Defaults to 'https://endpoints.office.com/endpoints/worldwide?clientrequestid=b10c5ed1-bad1-445f-b386-b919946339a7'.
      Provided as parameter to allow queries to other environments than worldwide as well as keep agility if Microsoft would change URL.
 
      .EXAMPLE
      Get-Office365IPURL -Services Exchange,Skype -OnlyRequired -Types IP4,URL -Outputformat JSON
 
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]
        $IP,

        [Parameter()]
        [string]
        $Office365IPURL = 'https://endpoints.office.com/endpoints/worldwide?clientrequestid=b10c5ed1-bad1-445f-b386-b919946339a7'

    )

    $ErrorActionPreference = 'Stop'

    # Get latest IP URL info
    $Office365Endpoints = Invoke-RestMethod -Uri $Office365IPURL -Method Get

    # Import net module
    Import-Module indented.net.ip

    # Foreach service
    foreach ($item in $IP)
    {
        # Foreach rule in service
        foreach ($rule in $Office365Endpoints)
        {
            # Select Ipv4 ips
            $IPv4Ranges = $rule.ips.where({ $_ -notlike '*:*' })

            # Resolve IPs for URLs. There are two shortcomings of this part. First; Only the currently returned IPs are evaluated. In case other
            # records are returned due to GeoDNS, round robin etc those will not be known and therefor not evaluated. Second; URLs with wildcards are
            # not evalutated, there is no way for the script to know which URLs within the wildcard scope that will be called by services.
            $rule.urls | ForEach-Object {
                if ($_)
                {
                    Resolve-DnsName $_ -ErrorAction SilentlyContinue | Where-Object { $_.GetType().Name -eq 'DnsRecord_A' } | ForEach-Object {
                        $IPv4Ranges += $_.IPAddress
                    }
                }
            }

            # Test each entry in the array if the IP is equal or belongs to the returned IP/range
            foreach ($range in $IPv4Ranges)
            {
                [pscustomobject]@{
                    RuleID      = $rule.id
                    ServiceArea = $rule.ServiceArea
                    TCPPort     = $rule.tcpPorts
                    UDPPort     = $rule.udpPorts
                    Required    = $rule.Required
                    Range       = $range
                    Subject     = $item
                    IsMember    = (Test-SubnetMember -SubjectIPAddress $item -ObjectIPAddress $range)
                }
            }
        }
    }
}
#EndRegion '.\Public\Test-Office365IPURL.ps1' 77
#Region '.\Public\Test-PSGalleryNameAvailability.ps1' 0
function Test-PSGalleryNameAvailability
{
    <#
        .DESCRIPTION
        Retreive a list of ip and urls required for communication to and from Office 365.
 
        .PARAMETER PackageName
        Defines the package name to search for
 
        .EXAMPLE
        Test-PSGalleryNameAvailability -PackageName PowershellGet
    #>

    [CmdletBinding()]
    [OutputType([boolean])]
    param(
        [Parameter(Mandatory)]
        [string]
        $PackageName
    )

    $Response = Invoke-WebRequest -Uri "https://www.powershellgallery.com/packages/$PackageName" -SkipHttpErrorCheck
    if ($Response.RawContent -like '*Page not found*')
    {
        return $true
    }
    else
    {
        return $false
    }

}
#EndRegion '.\Public\Test-PSGalleryNameAvailability.ps1' 32
#Region '.\suffix.ps1' 0
# The content of this file will be appended to the top of the psm1 module file. This is useful for custom procesedures after all module functions are loaded.
#EndRegion '.\suffix.ps1' 2