Omnicit.psm1

class CanonicalName {
    hidden [string]$_FullName
    hidden [string]$_OrganizationalUnit
    hidden [string]$_Domain
    [string]$CanonicalName

    CanonicalName() {
        $this | Add-Member -Name FullName -MemberType ScriptProperty -Value {
            return $this._FullName
        } -SecondValue {
            param($Value)
            $this._FullName = $Value
            try {
                $this.BuildCanonicalName($this._Domain, $this._OrganizationalUnit, $this._FullName)
            }
            catch {
                Write-Verbose -Message 'Uncompleted FullName, OrganizationalUnit or Domain to finish build CanonicalName'
            }
        }
        $this | Add-Member -Name OrganizationalUnit -MemberType ScriptProperty -Value {
            return $this._OrganizationalUnit
        } -SecondValue {
            param($Value)
            $this._OrganizationalUnit = $Value
            try {
                $this.BuildCanonicalName($this._Domain, $this._OrganizationalUnit, $this._FullName)
            }
            catch {
                Write-Verbose -Message 'Uncompleted FullName, OrganizationalUnit or Domain to finish build CanonicalName'
            }
        }
        $this | Add-Member -Name Domain -MemberType ScriptProperty -Value {
            return $this._Domain
        } -SecondValue {
            param($Value)
            $this._Domain = $Value
            try {
                $this.BuildCanonicalName($this._Domain, $this._OrganizationalUnit, $this._FullName)
            }
            catch {
                Write-Verbose -Message 'Uncompleted FullName, OrganizationalUnit or Domain to finish build CanonicalName'
            }
        }
    }
    CanonicalName($CanonicalName) {
        if ($CanonicalName -match '^(?:(?!\/))(?:(?<Domain>[^\/]+)\/)?(?:(?<Path>.+(?=\/)+)\/)?(?:(?<Name>.+))?$') {
            $this._FullName = if ($Matches['Domain'] -eq [string]::Empty) { $null } else { $Matches['Name'] }
            $this._OrganizationalUnit = if ($Matches['Path']) { $Matches['Path'] }
            $this._Domain = if ($Matches['Domain']) { $Matches['Domain'] } else { $Matches['Name'] }
            $this.CanonicalName = $Matches[0]

            $this | Add-Member -Name FullName -MemberType ScriptProperty -Value {
                return $this._FullName
            } -SecondValue {
                param($Value)
                $this._FullName = $Value
                try {
                    $this.BuildCanonicalName($this._Domain, $this._OrganizationalUnit, $this._FullName)
                }
                catch {
                    Write-Verbose -Message 'Uncompleted FullName, OrganizationalUnit or Domain to finish build CanonicalName'
                }
            }
            $this | Add-Member -Name OrganizationalUnit -MemberType ScriptProperty -Value {
                return $this._OrganizationalUnit
            } -SecondValue {
                param($Value)
                $this._OrganizationalUnit = $Value
                try {
                    $this.BuildCanonicalName($this._Domain, $this._OrganizationalUnit, $this._FullName)
                }
                catch {
                    Write-Verbose -Message 'Uncompleted FullName, OrganizationalUnit or Domain to finish build CanonicalName'
                }
            }
            $this | Add-Member -Name Domain -MemberType ScriptProperty -Value {
                return $this._Domain
            } -SecondValue {
                param($Value)
                $this._Domain = $Value
                try {
                    $this.BuildCanonicalName($this._Domain, $this._OrganizationalUnit, $this._FullName)
                }
                catch {
                    Write-Verbose -Message 'Uncompleted FullName, OrganizationalUnit or Domain to finish build CanonicalName'
                }
            }
        }
        else {
            Write-Verbose -Message 'String does not contain a valid CanonicalName.'
            break
        }
    }

    hidden [void]BuildCanonicalName([string]$CN, [string]$OU, [string]$DN) {
        [string]$Slash = '/'
        if ($OU -eq [string]::Empty) {
            $Slash = $null
        }
        $this.CanonicalName = '{0}/{1}{2}{3}' -f $CN, $OU, $Slash, $DN
    }

    [string] ConvertToDistinguishedName () {
        if ($null -eq $this.CanonicalName) {
            Write-Verbose -Message 'No CanonicalName was defined.'
            break
        }
        [Text.StringBuilder]$DistinguishedName = [Text.StringBuilder]::new()
        [string[]]$String = $this.CanonicalName.Split('/')

        if ($this._FullName -ne [string]::Empty) {
            $null = $DistinguishedName.Append('CN={0}' -f ($String[$String.Count - 1]))
        }

        for ($i = $String.Count - 2; $i -ge 1; $i--) {
            $null = $DistinguishedName.Append(',OU={0}' -f ($String[$i]))
        }

        foreach ($Top in ($String[0].Split('.'))) {
            $null = $DistinguishedName.Append(',DC={0}' -f ($Top))
        }

        return $DistinguishedName.ToString().Trim(',')
    }
    [string]ToString() {
        return $this.CanonicalName
    }
}
class CommonName {
    [ValidatePattern('^CN=[a-z1-9]')]
    [ValidateLength(4, 67)]
    [string]$Value

    CommonName([string]$CommonName) {
        $this.Value = $CommonName
    }

    [string]ToString() {
        return $this.Value
    }
}

class OrganizationalUnit {
    [ValidatePattern('^(?:((?:(?:OU)=[^,]+,?)+))')]
    [ValidateLength(4, 67)]
    [string]$Value

    OrganizationalUnit([string]$OrganizationalUnit) {
        $this.Value = $OrganizationalUnit
    }
    [string]ToString() {
        return $this.Value
    }
}

class Domain {
    [ValidatePattern('^((?:DC=[^,]+,?)+)')]
    [ValidateLength(4, 67)]
    [string]$Value

    Domain([string]$Domain) {
        $this.Value = $Domain
    }
    [string]ToString() {
        return $this.Value
    }
}

class DistinguishedName {
    hidden [string]$_FullName
    hidden [string]$_CommonName
    hidden [string]$_OrganizationalUnit
    hidden [string]$_Domain
    [string]$DistinguishedName

    DistinguishedName() {
        $this | Add-Member -Name FullName -MemberType ScriptProperty -Value {
            return [string]$this._FullName
        } -SecondValue {
            param($Value)
            $this._FullName = $Value
            $this._CommonName = 'CN={0}' -f $Value
            try {
                $this.BuildDistinguishedName($this._CommonName, $this._OrganizationalUnit, $this._Domain)
            }
            catch {
                Write-Verbose -Message 'Uncompleted CommonName, OrganizationalUnit or Domain to finish build DistinguishedName'
            }
        }
        $this | Add-Member -Name CommonName -MemberType ScriptProperty -Value {
            return [string]$this._CommonName
        } -SecondValue {
            param([CommonName]$Value)
            $this._FullName = ($Value -replace '^CN=')
            $this._CommonName = $Value
            try {
                $this.BuildDistinguishedName($this._CommonName, $this._OrganizationalUnit, $this._Domain)
            }
            catch {
                Write-Verbose -Message 'Uncompleted CommonName, OrganizationalUnit or Domain to finish build DistinguishedName'
            }
        }
        $this | Add-Member -Name OrganizationalUnit -MemberType ScriptProperty -Value {
            return [string]$this._OrganizationalUnit
        } -SecondValue {
            param([OrganizationalUnit]$Value)
            $this._OrganizationalUnit = $Value
            try {
                $this.BuildDistinguishedName($this._CommonName, $this._OrganizationalUnit, $this._Domain)
            }
            catch {
                Write-Verbose -Message 'Uncompleted CommonName, OrganizationalUnit or Domain to finish build DistinguishedName'
            }
        }
        $this | Add-Member -Name Domain -MemberType ScriptProperty -Value {
            return [string]$this._Domain
        } -SecondValue {
            param([Domain]$Value)
            $this._Domain = $Value
            try {
                $this.BuildDistinguishedName($this._CommonName, $this._OrganizationalUnit, $this._Domain)
            }
            catch {
                Write-Verbose -Message 'Uncompleted CommonName, OrganizationalUnit or Domain to finish build DistinguishedName'
            }
        }
    }

    DistinguishedName([string]$DistinguishedName) {
        if ($DistinguishedName -match '^(?:(?<CN>CN=(?<Name>[^,]*)),)?(?:(?<OU>(?:(?:OU)=[^,]+,?)+),)?(?<Domain>(?:DC=[^,]+,?)+)$') {
            $this._FullName = $Matches['Name']
            $this._CommonName = 'CN={0}' -f $Matches['Name']
            $this._OrganizationalUnit = if ($Matches['OU']) { $Matches['OU'] }
            $this._Domain = $Matches['Domain']
            $this.DistinguishedName = $Matches[0]

            $this | Add-Member -Name FullName -MemberType ScriptProperty -Value {
                return $this._FullName
            } -SecondValue {
                param($Value)
                $this._FullName = $Value
                $this._CommonName = 'CN={0}' -f $Value
                try {
                    $this.BuildDistinguishedName($this._CommonName, $this._OrganizationalUnit, $this._Domain)
                }
                catch {
                    Write-Verbose -Message 'Uncompleted CommonName, OrganizationalUnit or Domain to finish build DistinguishedName'
                }
            }
            $this | Add-Member -Name CommonName -MemberType ScriptProperty -Value {
                return [string]$this._CommonName
            } -SecondValue {
                param([CommonName]$Value)
                $this._FullName = ($Value -replace '^CN=')
                $this._CommonName = $Value
                try {
                    $this.BuildDistinguishedName($this._CommonName, $this._OrganizationalUnit, $this._Domain)
                }
                catch {
                    Write-Verbose -Message 'Uncompleted CommonName, OrganizationalUnit or Domain to finish build DistinguishedName'
                }
            }
            $this | Add-Member -Name OrganizationalUnit -MemberType ScriptProperty -Value {
                return $this._OrganizationalUnit
            } -SecondValue {
                param([OrganizationalUnit]$Value)
                $this._OrganizationalUnit = $Value
                try {
                    $this.BuildDistinguishedName($this._CommonName, $this._OrganizationalUnit, $this._Domain)
                }
                catch {
                    Write-Verbose -Message 'Uncompleted CommonName, OrganizationalUnit or Domain to finish build DistinguishedName'
                }
            }
            $this | Add-Member -Name Domain -MemberType ScriptProperty -Value {
                return $this._Domain
            } -SecondValue {
                param([Domain]$Value)
                $this._Domain = $Value
                try {
                    $this.BuildDistinguishedName($this._CommonName, $this._OrganizationalUnit, $this._Domain)
                }
                catch {
                    Write-Verbose -Message 'Uncompleted CommonName, OrganizationalUnit or Domain to finish build the DistinguishedName string'
                }
            }
        }
        else {
            Write-Verbose -Message 'String does not contain a valid DistinguishedName.'
            break
        }
    }
    DistinguishedName([string]$CommonName, [string]$OrganizationalUnit, [string]$Domain) {
        try {
            $this.BuildDistinguishedName($CommonName, $OrganizationalUnit, $Domain)
        }
        catch {
            Write-Verbose -Message 'Uncompleted CommonName, OrganizationalUnit or Domain to finish build the DistinguishedName string'
            break
        }
        $this._CommonName = $CommonName
        $this._FullName = ($CommonName -replace '^CN=')
        $this._OrganizationalUnit = $OrganizationalUnit
        $this._Domain = $Domain

        $this | Add-Member -Name FullName -MemberType ScriptProperty -Value {
            return [string]$this._FullName
        } -SecondValue {
            param($Value)
            $this._FullName = $Value
            $this._CommonName = 'CN={0}' -f $Value
            try {
                $this.BuildDistinguishedName($this._CommonName, $this._OrganizationalUnit, $this._Domain)
            }
            catch {
                Write-Verbose -Message 'Uncompleted CommonName, OrganizationalUnit or Domain to finish build DistinguishedName'
            }
        }
        $this | Add-Member -Name CommonName -MemberType ScriptProperty -Value {
            return [string]$this._CommonName
        } -SecondValue {
            param([CommonName]$Value)
            $this._FullName = ($Value -replace '^CN=')
            $this._CommonName = $Value
            try {
                $this.BuildDistinguishedName($this._CommonName, $this._OrganizationalUnit, $this._Domain)
            }
            catch {
                Write-Verbose -Message 'Uncompleted CommonName, OrganizationalUnit or Domain to finish build DistinguishedName'
            }
        }
        $this | Add-Member -Name OrganizationalUnit -MemberType ScriptProperty -Value {
            return [string]$this._OrganizationalUnit
        } -SecondValue {
            param([OrganizationalUnit]$Value)
            $this._OrganizationalUnit = $Value
            try {
                $this.BuildDistinguishedName($this._CommonName, $this._OrganizationalUnit, $this._Domain)
            }
            catch {
                Write-Verbose -Message 'Uncompleted CommonName, OrganizationalUnit or Domain to finish build DistinguishedName'
            }
        }
        $this | Add-Member -Name Domain -MemberType ScriptProperty -Value {
            return [string]$this._Domain
        } -SecondValue {
            param([Domain]$Value)
            $this._Domain = $Value
            try {
                $this.BuildDistinguishedName($this._CommonName, $this._OrganizationalUnit, $this._Domain)
            }
            catch {
                Write-Verbose -Message 'Uncompleted CommonName, OrganizationalUnit or Domain to finish build DistinguishedName'
            }
        }
    }
    hidden [void]BuildDistinguishedName([CommonName]$CN, [OrganizationalUnit]$OU, [Domain]$DN) {
        $this.DistinguishedName = '{0},{1},{2}' -f $CN.ToString(), $OU.ToString(), $DN.ToString()
    }

    [string] ConvertToCanonicalName () {
        if ($null -eq $this.DistinguishedName) {
            Write-Verbose -Message 'No DistinguishedName was defined.'
            break
        }
        [string]$CN = $null
        [Collections.ArrayList]$OU = [Collections.ArrayList]::new()
        [Text.StringBuilder]$Canonical = [Text.StringBuilder]::new()
        [Text.StringBuilder]$DC = [Text.StringBuilder]::new()
        [string[]]$String = ($this.DistinguishedName) -split '(?<!\\),(?!\,)'
        foreach ($SubString in $String) {
            $SubString = $SubString.TrimStart()
            # Remove or replace each Distinguished Name indicator with a corresponding trailing char '/' for CanonicalName.
            switch ($SubString.SubString(0, 3)) {
                'CN=' {
                    [string]$CN = '{0}' -f ($SubString -replace 'CN=')
                    continue
                }
                'OU=' {
                    $null = $OU.Add('{0}/' -f ($SubString -replace 'OU='))
                    continue
                }
                'DC=' {
                    $null = $DC.Append('{0}.' -f ($SubString -replace 'DC='))
                    continue
                }
            }
        }

        $OU.Reverse()
        [Text.StringBuilder]$Canonical.Append([string]($DC -replace '\.$', '/'))
        [Text.StringBuilder]$Canonical.Append(-join $OU)
        [Text.StringBuilder]$Canonical.Append($CN)

        return $Canonical.ToString().TrimEnd('/')
    }

    [string]ToString() {
        return $this.DistinguishedName
    }
}
class NormalizeString {
    hidden [Text.Encoding]$Encoding = [Text.Encoding]::GetEncoding('ISO-8859-6')
    hidden [string]$_NormalizedString
    hidden [string]$_String

    NormalizeString() {
        $this | Add-Member -Name String -MemberType ScriptProperty -Value {
            return $this._String
        } -SecondValue {
            param($Value)
            $this._String = $Value
            $this.FormatString()
        }
        $this | Add-Member -Name NormalizedString -MemberType ScriptProperty -Value {
            return $this._NormalizedString
        }
    }
    NormalizeString([string]$Value) {
        $this | Add-Member -Name String -MemberType ScriptProperty -Value {
            return $this._String
        } -SecondValue {
            param($Value)
            $this._String = $Value
            $this.FormatString()
        }
        $this | Add-Member -Name NormalizedString -MemberType ScriptProperty -Value {
            return $this._NormalizedString
        }
        $this.String = $Value
    }
    NormalizeString([string]$Value, [Text.Encoding]$Encoding) {
        $this | Add-Member -Name String -MemberType ScriptProperty -Value {
            return $this._String
        } -SecondValue {
            param($Value)
            $this._String = $Value
            $this.FormatString()
        }
        $this | Add-Member -Name NormalizedString -MemberType ScriptProperty -Value {
            return $this._NormalizedString
        }
        $this.String = $Value
        $this.Encoding = [Text.Encoding]::GetEncoding($Encoding)
    }
    hidden [string]FormatString() {
        try {
            $StringBuilder = [Text.StringBuilder]::new()
            $Bytes = [Text.Encoding]::Convert([Text.Encoding]::Unicode, $this.Encoding, [Text.Encoding]::Unicode.GetBytes($this._String))
            [string]$ConvertedString = $this.Encoding.GetString($Bytes)

            foreach ($Char in $ConvertedString.Normalize([Text.NormalizationForm]::FormD).GetEnumerator()) {
                if ([Globalization.CharUnicodeInfo]::GetUnicodeCategory($Char) -ne [Globalization.UnicodeCategory]::NonSpacingMark) {
                    $null = $StringBuilder.Append($Char)
                }
            }
            $this._NormalizedString = $StringBuilder.ToString()
            return $this._NormalizedString
        }
        catch {
            Write-Verbose -Message ('Unable to convert string - {0}' -f $_.Exception.Message)
            break
        }
    }
    [string] ToString() {
        return $this.NormalizedString
    }
}
enum TypeAccelerators {
    DateTime
    Int32
    Int64
    Boolean
    Char
    Byte
    Decimal
    Double
    Int16
    SByte
    Single
    UInt16
    UInt32
    UInt64
    String
    All
}

class ConvertTypeAccelerator {
    [Object]$Object
    [Object]$ObjectType

    ConvertTypeAccelerator([string]$Object, [TypeAccelerators[]]$ObjectType, [Object]$PSTypeAccelerators) {
        if ($ObjectType -eq [TypeAccelerators]::All) {
            [TypeAccelerators[]]$Enums = ([enum]::GetNames([TypeAccelerators])).Where( { $_ -ne 'All' } )
        }
        else {
            [TypeAccelerators[]]$Enums = $ObjectType
        }
        foreach ($Enum in $Enums) {
            try {
                $this.Object = [Convert]::"To$($Enum)"($Object)
                if ($Enum -eq 'Boolean') {
                    $Enum = 'bool'
                }
                $this.ObjectType = $PSTypeAccelerators[$Enum.ToString()]
                break
            }
            catch {
                continue
            }
        }
        if ($null -eq $this.Object) {
            $this.Object = [string]$Object
            $this.ObjectType = $PSTypeAccelerators['String']
        }
    }
}
function Get-PSTypeAccelerator {
    [CmdletBinding()]
    param()
    try {
        [PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')::Get
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}
function Compare-ObjectProperty {
    <#
    .SYNOPSIS
    The Compare-ObjectProperty function compares two sets of objects and it's properties.
 
    .DESCRIPTION
    The Compare-ObjectProperty function compares two sets of objects and it's properties.
    One set of objects is the "reference set" and the other set is the "difference set."
 
    The result will indicate if each property from the reference set is equal to each property of the difference set.
 
    .EXAMPLE
    $Ref = Get-ADUser -Identity 'Roger Johnsson' -Properties *
    $Dif = Get-ADUser -Identity 'John Rogersson' -Properties *
    Compare-ObjectProperty -ReferenceObject $Ref -DifferenceObject $Dif
 
    This example returns all properties for both the reference object and the difference object with a true respectively false if the properties is equal.
    The original property for both reference object and the difference object is also returned.
 
    .LINK
        https://github.com/Omnicit/Omnicit/blob/master/docs/en-US/Compare-ObjectProperty.md
    #>

    [CmdletBinding(
        PositionalBinding,
        SupportsShouldProcess
    )]
    [Alias('cop')]
    param(
        # Specifies an object used as a reference for comparison.
        [Parameter(
            HelpMessage = 'Enter objects used as a reference for comparison.',
            ValueFromPipeline,
            Mandatory,
            Position = 0
        )]
        [ValidateNotNullOrEmpty()]
        [PSObject]$ReferenceObject,
        # Specifies the object that are compared to the reference objects.
        [Parameter(
            HelpMessage = 'Enter the objects that are compared to the reference objects.',
            Mandatory,
            Position = 1
        )]
        [ValidateNotNullOrEmpty()]
        [PSObject]$DifferenceObject
    )
    $Properties = [Collections.ArrayList]::new()
    $null = $Properties.AddRange([string[]]($ReferenceObject | Get-Member -MemberType Properties).Name)
    $null = $Properties.AddRange([string[]]($DifferenceObject | Get-Member -MemberType Properties).Name)
    $AllProperties = $Properties | Sort-Object -Unique

    foreach ($Property in $AllProperties) {
        if ($PSCmdlet.ShouldProcess($Property, $MyInvocation.MyCommand.Name)) {
            try {
                $DiffProperty = Compare-Object -ReferenceObject $ReferenceObject -DifferenceObject $DifferenceObject -Property $Property -ErrorAction Stop
            }
            catch {
                $DiffProperty = $true
            }
            [PSCustomObject]@{
                PSTypeName      = 'Omnicit.Compare.ObjectProperty'
                PropertyName    = $Property
                PropertyMatch   = (-not [bool]$DiffProperty)
                ReferenceValue  = $ReferenceObject."$Property"
                DifferenceValue = $DifferenceObject."$Property"
            }
        }
    }
}
function ConvertFrom-CanonicalName {
    <#
    .SYNOPSIS
    Convert a CanonicalName string to a DistinguishedName string.
 
    .DESCRIPTION
    The ConvertFrom-CanonicalName converts one or more strings in form of a CanonicalName into DistinguishedName strings.
    Common usage when working in Exchange and comparing objects in Active Directory.
 
    .EXAMPLE
    ConvertFrom-CanonicalName -CanonicalName 'Contoso.com/Department/Users/Roger Johnsson'
    CN=Roger Johnsson,OU=Users,OU=Department,DC=Contoso,DC=com
 
    This example returns the converted CanonicalName in form of a DistinguishedName.
 
    .EXAMPLE
    ConvertFrom-CanonicalName -CanonicalName 'Contoso.com/Department/Users' -OrganizationalUnit
    OU=Users,OU=Department,DC=Contoso,DC=com
 
    This example returns the converted CanonicalName in form of a DistinguishedName. In this case the last object after slash '/' is an OrganizationalUnit.
 
    .EXAMPLE
    ConvertFrom-CanonicalName -CanonicalName 'Contoso.com/Department/Users/Roger Johnsson', 'Contoso.com/Tier1/Users/Bill T Admin'
    CN=Roger Johnsson,OU=Users,OU=Department,DC=Contoso,DC=com
    CN=Bill T Admin,OU=Users,OU=Tier1,DC=Contoso,DC=com
 
    This example returns each CanonicalName converted in form of a DistinguishedName.
 
    .LINK
        https://github.com/Omnicit/Omnicit/blob/master/docs/en-US/ConvertFrom-CanonicalName.md
    #>

    [OutputType([DistinguishedName])]
    [CmdletBinding(
        SupportsShouldProcess
    )]
    param (
        # Specifies the CanonicalName string to be converted to a DistinguishedName string.
        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            HelpMessage = 'Input a valid CanonicalName. Example: "Contoso.com/Department/Users/Roger Johnsson"'
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('CN')]
        [CanonicalName[]]$CanonicalName,

        # Specifies that the object is an OrganizationalUnit (OU=) instead of an Person (CN=).
        # Will automatically be an OrganizationalUnit if the CanonicalName string ends with a slash '/'
        [switch]$OrganizationalUnit
    )
    process {
        foreach ($Name in $CanonicalName) {
            if ($PSCmdlet.ShouldProcess(('{0}' -f $Name, $MyInvocation.MyCommand.Name))) {
                if ($PSBoundParameters.ContainsKey('OrganizationalUnit') -and $Name.CanonicalName -notmatch '\/$') {
                    $Name.OrganizationalUnit = '{0}/{0}' -f $Name.OrganizationalUnit, $Name.FullName
                    $Name.FullName = $null
                }
                [DistinguishedName]$Name.ConvertToDistinguishedName()
            }
        }
    }
}
function ConvertFrom-DistinguishedName {
    <#
    .SYNOPSIS
    Convert a DistinguishedName string to a CanonicalName string.
 
    .DESCRIPTION
    The ConvertFrom-DistinguishedName converts one or more strings in form of a DistinguishedName into CanonicalName strings.
    Common usage when working in Exchange and comparing objects in Active Directory.
 
    .EXAMPLE
    ConvertFrom-DistinguishedName -DistinguishedName 'CN=Roger Johnsson,OU=Users,OU=Department,DC=Contoso,DC=com'
    Contoso.com/Department/Users/Roger Johnsson
 
    This example returns the converted DistinguishedName in form of a CanonicalName.
 
    .EXAMPLE
    ConvertFrom-DistinguishedName -DistinguishedName 'CN=Roger Johnsson,OU=Users,OU=Department,DC=Contoso,DC=com', 'CN=Bill T Admin,OU=Users,OU=Tier1,DC=Contoso,DC=com'
    Contoso.com/Department/Users/Roger Johnsson
    Contoso.com/Tier1/Users/Bill T Admin
 
    This example returns each DistinguishedName converted in form of a CanonicalName.
 
    .LINK
    https://github.com/Omnicit/Omnicit/blob/master/docs/en-US/ConvertFrom-DistinguishedName.md
    #>

    [OutputType([CanonicalName])]
    [CmdletBinding(
        SupportsShouldProcess
    )]
    param (
        # Specifies one or more Distinguished Names to be converted to Canonical Names.
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            HelpMessage = 'Input a valid DistinguishedName. Example: "CN=Roger Johnsson,OU=Users,OU=Department,DC=Contoso,DC=com"'
        )]
        [ValidateNotNullOrEmpty()]
        [Alias('DN')]
        [DistinguishedName[]]$DistinguishedName
    )
    process {
        foreach ($Name in $DistinguishedName) {
            if ($PSCmdlet.ShouldProcess(('{0}' -f $Name, $MyInvocation.MyCommand.Name))) {
                [CanonicalName]$Name.ConvertToCanonicalName()
            }
        }
    }
}
function ConvertTo-MailNormalization {
    <#
    .SYNOPSIS
    Convert a string for Mail Normalization.
 
    .DESCRIPTION
    Converts accented, diacritics and most European chars to the corresponding a-z char.
    Effectively and fast converts a string to a normalized mail string using [Text.NormalizationForm]::FormD and [Text.Encoding]::GetEncoding('ISO-8859-8').
    Function created to support legacy email providers that does not support e-mail address internationalization (EAI).
    Integers will remain intact.
 
    .EXAMPLE
    ConvertTo-MailNormalization -InputObject 'ûüåäöÅÄÖÆÈÉÊËÐÑØçł'
    uuaaoAAOAEEEEDNOcl
 
    This example returns a string with the converted Mail Normalization value.
 
    .NOTES
    Credits to Johan Åkerlund for the Convert-DiacriticCharacters function.
 
    .LINK
        https://github.com/Omnicit/Omnicit/blob/master/docs/en-US/ConvertTo-MailNormalization.md
    #>

    [OutputType([System.String])]
    [CmdletBinding(
        SupportsShouldProcess
    )]
    param (
        # Specifies the string to be converted to Mail Normalization.
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            HelpMessage = 'Specify a string to be converted to Unicode Normalization.'
        )]
        [AllowEmptyString()]
        [Alias('String')]
        [string]$InputObject
    )
    process {
        foreach ($String in $InputObject) {
            if ($PSCmdlet.ShouldProcess(('{0}' -f $String, $MyInvocation.MyCommand.Name))) {
                try {
                    [NormalizeString]::new($String).ToString()
                }
                catch {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
            }
        }
    }
}
function ConvertTo-TitleCase {
    <#
    .SYNOPSIS
    The ConvertTo-TitleCase function converts a specified string to title case.
 
    .DESCRIPTION
    The ConvertTo-TitleCase function converts a specified string to title case using the Method in (Get-Culture).TextInfo
    All input strings will be converted to lowercase, because uppercase are considered to be acronyms.
 
    .EXAMPLE
    ConvertTo-TitleCase -InputObject 'roger johnsson'
    Roger Johnsson
 
    This example returns the string 'Roger Johnsson' which has capitalized the R and J chars.
 
    .EXAMPLE
    'roger johnsson', 'JOHN ROGERSSON' | ConvertTo-TitleCase
    Roger Johnsson
    John Rogersson
 
    This example returns the strings 'Roger Johnsson' and 'John Rogersson' which has capitalized the R and J chars.
 
    .LINK
        https://github.com/Omnicit/Omnicit/blob/master/docs/en-US/ConvertTo-TitleCase.md
    #>

    [Alias('ConvertTo-NameTitle')]
    [CmdletBinding(
        PositionalBinding,
        SupportsShouldProcess
    )]
    param (
        # Specifies one or more objects to be convert to title case strings.
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            Position = 0
        )]
        [AllowEmptyString()]
        [string[]]$InputObject
    )
    begin {
        $TextInfo = (Get-Culture).TextInfo
    }
    process {
        foreach ($String in $InputObject) {
            if ($PSCmdlet.ShouldProcess($String)) {
                    $TextInfo.ToTitleCase($String.ToLower())
                }
            }
        }
    }
function Get-ClipboardArray {
    <#
        .SYNOPSIS
        Paste an array of objects from the clipboard (CTRL+V)
 
        .DESCRIPTION
        Paste an array of objects from the clipboard (CTRL+V) to the variable $ClipboardArray (global scope).
        Using the parameter, AsType, can specify what every object should be tried to be converted to.
        If conversion fails every object as a type string.
        The order for the data type conversion is:
        [DateTime], [Int32], [Int64], [Boolean], [Char], [Byte], [Decimal], [Double], [Int16], [SByte], [Single], [UInt16], [UInt32], [UInt64], [String]
 
        Default an empty string will close the loop from importing any more data, default behavior of Read-Host.
        To change that behavior a specified [System.String] combination can be used with the -BreakString parameter to include empty lines as valid input data.
 
        .EXAMPLE
        Get-ClipboardArray
 
        No.[0]: Some data
        No.[1]: to
        No.[2]: paste
        No.[3]: into
        No.[4]: the function
        No.[5]:
 
        Empty line will close the loop from importing anything more.
        The input to will be saved to the variable ClipboardArray.
        Each item in the array will be of the data type [System.String].
 
        .EXAMPLE
        Get-ClipboardArray -AsType Int32
 
        No.[0]: Some data
        No.[1]: 20170101
        No.[2]: 123
        No.[3]: into
        No.[4]: 876 function
        No.[5]:
 
        Empty line will close the loop from importing anything more.
        The input to will be saved to the variable ClipboardArray.
        This example will try to convert every object in the pasted array to an System.Int32. If its not possible it will default back to System.String.
        Array object [1] and [2] is the only objects that will be converted to a System.Int32 type accelerator.
 
        .EXAMPLE
        Get-ClipboardArray -AsType All
 
        No.[0]: Some data
        No.[1]: 2017-01-01
        No.[2]: 2147483647
        No.[3]: 2147483648
        No.[4]: TRUE
        No.[5]:
 
        Empty line will close the loop from importing anything more.
        The input to will be saved to the variable ClipboardArray.
        This example will try to convert every object in the pasted array to all available data types. If no conversion succeeded it will default back to System.String.
        The order for the data type conversion is [System.DateTime], [System.Int32], [System.Int64], [System.Boolean], [System.String].
        Array object [0] is a String, object [1] is a DateTime, object [2] is an Int32, object [3] is an Int64 and object [4] is a Boolean.
 
        .EXAMPLE
        Get-ClipboardArray -BreakString '?'
 
        No.[0]: Some data
        No.[1]: 2017-01-01
        No.[2]: 2147483647
        No.[3]: 2147483648
        No.[4]: TRUE
        No.[5]:
        No.[6]: ?
 
        The question mark will close the loop from importing anything more.
        The input to will be saved to the variable ClipboardArray.
        This example will not convert any objects in the array, all objects default to System.String.
        The BreakString parameter can be used when the input contains empty rows to not break the loop.
 
        .EXAMPLE
        Get-ClipboardArray -BreakString '?' -AsType All
 
        No.[0]: Some data
        No.[1]: 2017-01-01
        No.[2]: 2147483647
        No.[3]: 2147483648
        No.[4]: TRUE
        No.[5]:
        No.[6]: ?
 
        $ClipboardArray | foreach {$_.Gettype()}
        IsPublic IsSerial Name BaseType
        -------- -------- ---- --------
        True True String System.Object
        True True DateTime System.ValueType
        True True Int32 System.ValueType
        True True Int64 System.ValueType
        True True Boolean System.ValueType
        True True String System.Object
 
        The exclamation mark will close the loop from importing anything more.
        The input to will be saved to the variable ClipboardArray.
        This example will try to convert every object in the array to all available data types. If no conversion succeeded it will default back to System.String.
        The order for the data type conversion is [System.DateTime], [System.Int32], [System.Int64], [System.Boolean], [System.String].
        Array object [0] is a String, object [1] is a DateTime, object [2] is an Int32, object [3] is an Int64, object [4] is a Boolean and object [5] is a String.
        The BreakString parameter can be used when the input contains empty rows to not break the loop.
 
        .LINK
        https://github.com/Omnicit/Omnicit/blob/master/docs/en-US/Get-ClipboardArray.md
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")]
    [CmdletBinding(
        PositionalBinding
    )]
    [Alias('gca')]
    param (
        <#
            Specifies the a single char or a string that will break the input loop.
            Default value is empty string ''
            Quote and some other special chars ("''`´“”‘‘’’„!) is not permitted as input.
        #>

        [Parameter(
            Position = 0
        )]
        [Alias('B')]
        [AllowEmptyString()]
        [ValidatePattern('[^"''`´“”‘‘’’„!]')]
        [string]$BreakString = '',
        <#
            Specifies the TypeAccelerator of the clipboard that each input will be converted to. The acceptable values for this parameter are:
            - DateTime
            - Int32
            - Int64
            - Boolean
            - Char
            - Byte
            - Decimal
            - Double
            - Int16
            - SByte
            - Single
            - UInt16
            - UInt32
            - UInt64
            - String
            - All
        #>

        [Parameter(
            Position = 1
        )]
        [ValidateSet('DateTime', 'Int32', 'Int64', 'Boolean', 'Char', 'Byte', 'Decimal', 'Double',
            'Int16', 'SByte', 'Single', 'UInt16', 'UInt32', 'UInt64', 'String', 'All')]
        [Alias('D')]
        [TypeAccelerators[]]$AsType = 'String'
    )

    try {
        $PSTypeAccelerator = Get-PSTypeAccelerator -ErrorAction Stop
        New-Variable -Name ClipboardArray -Value ([Collections.ArrayList]::new()) -Scope Global -Force -Description 'Variable created from Get-ClipboardArray.' -ErrorAction Stop
        [int32]$n = 0
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }
    do {
        # Read-Host instead of Get-Clipboard for Cross platform compatibility
        $Input = (Read-Host -Prompt "No.[$($n)]")
        $n++
        if ($Input -ne $BreakString) {
            $null = $global:ClipboardArray.Add([ConvertTypeAccelerator]::new($Input, $AsType, $PSTypeAccelerator))
        }
    }
    until ($Input -eq $BreakString)
    Write-Verbose -Message 'Enter $ClipboardArray to access the pasted array objects' -Verbose
}
function Get-FolderSize {
    <#
        .SYNOPSIS
        Get folder size using Robocopy.exe and displays the output in a PSObject table.
 
        .DESCRIPTION
        This function uses Robocopy.exe to calculate the size used for the select path.
        You can specify if you want to display files older than a specific date using the MinFileData parameter.
        If you experience that the function Get-FolderSize (Robocopy) takes time to execute you can increase the threads used by Robocopy with the RobocopyThreadCount parameter.
 
        .EXAMPLE
        Get-FolderSize
 
        Path TotalBytes TotalMBytes TotalGBytes FilesCount DirCount TimeElapsed
        ---- ---------- ----------- ----------- ---------- -------- -----------
        C:\Windows 18512797867 17655,18 17,24 144492 34036 00:00:12.2813478
 
        This example returns a PSObject with the total folder size for the current location, which in this case in C:\Windows.
        .EXAMPLE
        Get-FolderSize -Path \\contoso.com\NETLOGON
 
        Path TotalBytes TotalMBytes TotalGBytes FilesCount DirCount TimeElapsed
        ---- ---------- ----------- ----------- ---------- -------- -----------
        \\contoso.com\NETLOGON 1019904 0,97 0,00 1 2 00:00:00.0157300
 
        This example returns a PSObject with the total folder size for the specified location path \\contoso.com\NETLOGON.
        .EXAMPLE
        Get-FolderSize -Path \\contoso.net\NETLOGON -BytePrecision 4
 
        Path TotalBytes TotalMBytes TotalGBytes FilesCount DirCount TimeElapsed
        ---- ---------- ----------- ----------- ---------- -------- -----------
        \\contoso.com\NETLOGON 1019904 0,9727 0,0009 1 2 00:00:00.0155598
 
        This example returns a PSObject with the total folder size, with for decimals, for the specified location path \\contoso.com\NETLOGON.
        .EXAMPLE
        Get-FolderSize -Path C:\Windows -MinFileAgeDate 2019-06-01
 
        Path TotalBytes TotalMBytes TotalGBytes FilesCount DirCount TimeElapsed
        ---- ---------- ----------- ----------- ---------- -------- -----------
        C:\Windows 11448807458 10918,43 10,66 101354 34036 00:00:24.1594950
 
        This example returns a PSObject with the total folder size for the specified location path C:\Windows and excludes files newer than 2019-06-01.
 
        .EXAMPLE
        Get-FolderSize -Path C:\Windows -MaxFileAgeDate 2019-06-01
 
        Path TotalBytes TotalMBytes TotalGBytes FilesCount DirCount TimeElapsed
        ---- ---------- ----------- ----------- ---------- -------- -----------
        C:\Windows 7063990409 6736,75 6,58 43138 34036 00:00:10.2498128
 
        This example returns a PSObject with the total folder size for the specified location path C:\Windows and excludes files older than 2019-06-01.
 
        .EXAMPLE
        Get-FolderSize -Path C:\Windows -MaxFileAgeDate 2019-06-01 | Select-Object -Property *
 
        Path : C:\Windows
        TotalBytes : 7065038985
        TotalMBytes : 6737,75
        TotalGBytes : 6,58
        FilesCount : 43138
        DirCount : 34036
        BytesFailed : 0
        DirFailed : 0
        FileFailed : 0
        TimeElapsed : 00:00:10.0465934
        StartedTime : 2019-09-05 22:28:33
        EndedTime : 2019-09-05 22:28:43
        DateFilter : Maximum file age "2019-06-01".
        TotalBytesNoDate : 18474790494
        TotalMBytesNoDate : 17618,93
        TotalGBytesNoDate : 17,21
        DirCountNoDate : 34057
        FileCountNoDate : 144414
 
        This example returns a PSObject with all available properties extracted from the specified location path C:\Windows and excludes files older than 2019-06-01.
 
        .LINK
        https://github.com/Omnicit/Omnicit/blob/master/docs/en-US/Get-FolderSize.md
    #>


    [CmdletBinding(
        SupportsShouldProcess,
        DefaultParameterSetName = 'Default',
        PositionalBinding
    )]
    param (
        # Specifies the path to the source directory to measure.
        [Parameter(
            ValueFromPipeline,
            ValueFromPipelineByPropertyName,
            Position = 0
        )]
        [PSDefaultValue(Help = 'Current path')]
        [string[]]$Path = ($PWD.Path),

        # Specifies the minimum file age (exclude files newer than N date).
        [Parameter(
            ParameterSetName = 'MinFile',
            Mandatory,
            Position = 1
        )]
        [Alias('MinDate')]
        [datetime]$MinFileAgeDate,

        # Specifies the maximum file age (to exclude files older than N date).
        [Parameter(
            ParameterSetName = 'MaxFile',
            Mandatory,
            Position = 2
        )]
        [Alias('MaxDate')]
        [datetime]$MaxFileAgeDate,

        # Specifies number of fractional digits, and rounds midpoint values to the nearest even number when converting bytes.
        [Parameter(
            Position = 3
        )]
        [int]$BytePrecision = 2,

        # Number of threads used for Robocopy.
        [Parameter(
            Position = 4
        )]
        [ValidateRange(1, 128)]
        [int]$RobocopyThreadCount = 16
    )
    begin {
        try {
            $PreviousErrorActionPreference = $ErrorActionPreference
            $ErrorActionPreference = 'Continue'

            if (-not $IsWindows -and $PSVersionTable.PSEdition -eq 'Core') {
                throw [System.NotSupportedException]::New('Get-FolderSize is only supported on Windows operating systems.')
            }
            if (-not (Get-Command -Name "$env:windir\system32\robocopy.exe" -ErrorAction SilentlyContinue)) {
                throw [System.NotSupportedException]::New("Unable to locate Robocopy.exe in $env:windir\system32. Please verify if Robocopy.exe is available in the specified path.")
            }
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
        finally {
            $ErrorActionPreference = $PreviousErrorActionPreference
        }

        [Collections.ArrayList]$RobocopyArgs = @('/BYTES', '/FP', '/L', "/MT:$RobocopyThreadCount", '/NC', '/NDL', '/NJH', '/R:0', '/S', '/TS', '/W:0', '/XJ')

        $DateFilter = $null
        switch ($PSBoundParameters.Keys) {
            MinFileAgeDate {
                $null = $RobocopyArgs.Add("/MINAGE:$($MinFileAgeDate.ToString('yyyyMMdd'))")
                [string]$DateFilter = ('Minimum file age "{0}".' -f $MinFileAgeDate.ToShortDateString())
            }
            MaxFileAgeDate {
                $null = $RobocopyArgs.Add("/MAXAGE:$($MaxFileAgeDate.ToString('yyyyMMdd'))")
                [string]$DateFilter = ('Maximum file age "{0}".' -f $MaxFileAgeDate.ToShortDateString())
            }
        }
        # Thank you 'Joakim Svendsen' for the regular expression examples and inspiration!
        [regex]$HeaderRegex = '\s+Total\s+Copied\s+Skipped\s+Mismatch\s+FAILED\s+Extras'
        [regex]$DirLineRegex = 'Dirs\s+:\s+(?<DirCount>\d+)\s+(?<DirCopiedCount>\d+)(?:\s+\d+){2}\s+(?<DirsFailed>\d+)\s+\d+'
        [regex]$FileLineRegex = 'Files\s+:\s+(?<FileCount>\d+)\s+(?<FilesCopiedCount>\d+)(?:\s+\d+){2}\s+(?<FilesFailed>\d+)\s+\d+'
        [regex]$BytesLineRegex = 'Bytes\s+:\s+(?<ByteCount>\d+)\s+(?<ByteCopiedCount>\d+)(?:\s+\d+){2}\s+(?<BytesFailed>\d+)\s+\d+'
        [regex]$TimeLineRegex = 'Times\s+:\s+(?<TimeElapsed>\d+:\d+:\d+).+'
        [regex]$EndedLineRegex = 'Ended\s+:\s+(?<EndedTime>.+)'
    }
    process {
        foreach ($InlinePath in $Path) {
            if ($PSCmdlet.ShouldProcess($InlinePath)) {
                try {

                    [datetime]$StartTime = [datetime]::Now
                    [string]$Summary = (& "$env:windir\system32\robocopy.exe" $InlinePath NULL $RobocopyArgs)[-8..-1]
                    [datetime]$EndTime = [datetime]::Now

                    if ($Summary -match "$HeaderRegex\s+$DirLineRegex\s+$FileLineRegex\s+$BytesLineRegex\s+$TimeLineRegex\s+$EndedLineRegex") {
                        [PSCustomObject]@{
                            PSTypeName        = 'Omnicit.Get.FolderSize'
                            Path              = [string]$InlinePath
                            TotalBytes        = [decimal]$Matches['ByteCopiedCount']
                            TotalMBytes       = [decimal]([math]::Round(([decimal]$Matches['ByteCopiedCount'] / 1MB), $BytePrecision))
                            TotalGBytes       = [decimal]([math]::Round(([decimal]$Matches['ByteCopiedCount'] / 1GB), $BytePrecision))
                            FilesCount        = [decimal]$Matches['FilesCopiedCount']
                            DirCount          = [decimal]$Matches['DirCopiedCount']
                            BytesFailed       = [decimal]$Matches['BytesFailed']
                            DirFailed         = [decimal]$Matches['DirFailed']
                            FileFailed        = [decimal]$Matches['FileFailed']
                            TimeElapsed       = [TimeSpan]($EndTime - $StartTime)
                            StartedTime       = [datetime]$StartTime
                            EndedTime         = [datetime]$EndTime
                            DateFilter        = [string]$DateFilter
                            TotalBytesNoDate  = [decimal]$Matches['ByteCount']
                            TotalMBytesNoDate = [decimal]([math]::Round(([decimal]$Matches['ByteCount'] / 1MB), $BytePrecision))
                            TotalGBytesNoDate = [decimal]([math]::Round(([decimal]$Matches['ByteCount'] / 1GB), $BytePrecision))
                            DirCountNoDate    = [decimal]$Matches['DirCount']
                            FileCountNoDate   = [decimal]$Matches['FileCount']
                        }
                    }
                    else {
                        Write-Warning -Message "Unexpected format returned from Robocopy.exe for path '$InlinePath'." -WarningAction Continue
                    }
                }
                catch {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
            }
        }
    }
}
function Get-Parameter {
    <#
    .SYNOPSIS
        Enumerates the parameters of one or more commands
    .DESCRIPTION
        Lists all the parameters of a command, by ParameterSet, including their aliases, type, position, mandatory, etc.
 
        By default, formats the output to tables grouped by command and parameter set.
 
    .EXAMPLE
        Get-Parameter -CommandName Select-XML
 
            Command: Microsoft.PowerShell.Utility/Select-Xml
            Set: Xml *
 
        Name Aliases Position Mandatory Pipeline ByName Provider Type
        ---- ------- -------- --------- -------- ------ -------- ----
        Namespace {Na*} Named False False False All Hashtable
        Xml {Node, Xm*} 1 True True True All XmlNode[]
        XPath {XP*} 0 True False False All String
 
 
            Command: Microsoft.PowerShell.Utility/Select-Xml
            Set: Path
 
        Name Aliases Position Mandatory Pipeline ByName Provider Type
        ---- ------- -------- --------- -------- ------ -------- ----
        Namespace {Na*} Named False False False All Hashtable
        Path {Pa*} 1 True False True All String[]
        XPath {XP*} 0 True False False All String
        ...
 
        This example returns a PSObject which displays the parameters sorted by the Parameter Sets for the cmdlet Select-XML.
 
    .EXAMPLE
        Get-Command Select-Xml | Get-Parameter
 
            Command: Microsoft.PowerShell.Utility/Select-Xml
            Set: Xml *
 
        Name Aliases Position Mandatory Pipeline ByName Provider Type
        ---- ------- -------- --------- -------- ------ -------- ----
        Namespace {Na*} Named False False False All Hashtable
        Xml {Node, Xm*} 1 True True True All XmlNode[]
        XPath {XP*} 0 True False False All String
 
 
            Command: Microsoft.PowerShell.Utility/Select-Xml
            Set: Path
 
        Name Aliases Position Mandatory Pipeline ByName Provider Type
        ---- ------- -------- --------- -------- ------ -------- ----
        Namespace {Na*} Named False False False All Hashtable
        Path {Pa*} 1 True False True All String[]
        XPath {XP*} 0 True False False All String
        ...
 
        This example returns a PSObject which displays the parameters sorted by the Parameter Sets for the cmdlet Select-XML.
    .EXAMPLE
        Get-Parameter -CommandName Select-Xml -SetName Path
 
            Command: Microsoft.PowerShell.Utility/Select-Xml
            Set: Path
 
 
        Name Aliases Position Mandatory Pipeline ByName Provider Type
        ---- ------- -------- --------- -------- ------ -------- ----
        Namespace {Na*} Named False False False All Hashtable
        Path {Pa*} 1 True False True All String[]
        XPath {XP*} 0 True False False All String
 
        This example returns a PSObject which displays the parameters filtered by the Parameter Set 'Path' for the cmdlet Select-XML.
 
    .NOTES
        With many thanks to Joel Bennett, Jason Archer, Shay Levy, Hal Rottenberg, Oisin Grehan
    #>


    [CmdletBinding(
        DefaultParameterSetName = 'ParameterName'
    )]
    param(
        # The name of the command to get parameters for.
        [Parameter(
            Position = 1,
            Mandatory,
            ValueFromPipelineByPropertyName
        )]
        [Alias('Name')]
        [string[]]$CommandName,

        # The parameter name to filter by (allows Wilcards).
        [Parameter(
            Position = 2,
            ValueFromPipelineByPropertyName,
            ParameterSetName = 'FilterNames'
        )]
        [string[]]$ParameterName = '*',

        # The ParameterSet name to filter by (allows wildcards).
        [Parameter(
            ValueFromPipelineByPropertyName,
            ParameterSetName = 'FilterSets')]
        [string[]]$SetName = '*',

        # The name of the module which contains the command (this is for scoping).
        [Parameter(
            ValueFromPipelineByPropertyName
        )]
        [string]$ModuleName,

        # Skip testing for Provider parameters (will be much faster)
        [switch]$SkipProviderParameters,

        # Forces including the CommonParameters in the output
        [switch]$Force
    )

    begin {
        function Join-ParameterObject {
            param(
                [PSObject]$ParameterMetadata,

                [object]$ParameterSetData
            )

            # Sort ParameterMetadata and $ParameterSetData objects alphabetical and remove duplicate properties.
            [string[]]$SortedParameterMetadata = ($ParameterMetadata | Get-Member -MemberType Properties | Sort-Object -Property Name).Name
            foreach ($ParameterSetDataProperty in ($ParameterSetData | Get-Member -MemberType Properties | Where-Object { $SortedParameterMetadata -notcontains $_.Name }).Name) {
                $ParameterMetadata | Add-Member -MemberType NoteProperty -Name $ParameterSetDataProperty -Value $ParameterSetData.$ParameterSetDataProperty
            }

            [PSCustomObject]@{
                PSTypeName                      = 'Omnicit.Get.Parameter'
                Name                            = $ParameterMetadata.Name
                Position                        = if ($ParameterMetadata.Position -lt 0) { 'Named' } else { $ParameterMetadata.Position }
                Aliases                         = $ParameterMetadata.Aliases
                Short                           = $ParameterMetadata.Name
                Type                            = $ParameterMetadata.ParameterType.Name
                ParameterSet                    = $ParamSet
                Command                         = $Command
                Mandatory                       = $ParameterMetadata.IsMandatory
                Provider                        = $ParameterMetadata.DynamicProvider
                ValueFromPipeline               = $ParameterMetadata.ValueFromPipeline
                ValueFromPipelineByPropertyName = $ParameterMetadata.ValueFromPipelineByPropertyName
            }
        }

        function Add-Parameter {
            [CmdletBinding()]
            param (
                # Parameter used to provide the Hashtable to the pipeline for the $Parameters variable.
                [Hashtable]$OutputParameter,

                # The actual parameters from the $Command
                [Management.Automation.ParameterMetadata[]]$InputParameter
            )

            foreach ($Parameter in $InputParameter | Where-Object { !$OutputParameter.ContainsKey($_.Name) } ) {
                Write-Debug ('INITIALLY: ' + $Parameter.Name)
                $OutputParameter.($Parameter.Name) = $Parameter | Select-Object -Property '*'
            }

            [Array]$DynamicParameter = $InputParameter | Where-Object { $_.IsDynamic }
            if ($DynamicParameter) {
                foreach ($DynamicParam in $DynamicParameter) {
                    if (Get-Member -InputObject $OutputParameter.($DynamicParam.Name) -Name DynamicProvider) {
                        Write-Debug ('ADD:' + $DynamicParam.Name + ' ' + $Provider.Name)
                        $OutputParameter.($DynamicParam.Name).DynamicProvider += $Provider.Name
                    }
                    else {
                        Write-Debug ('CREATE:' + $DynamicParam.Name + ' ' + $Provider.Name)
                        $OutputParameter.($DynamicParam.Name) = $OutputParameter.($DynamicParam.Name) | Select-Object -Property '*', @{ n = 'DynamicProvider'; e = { @($Provider.Name) } }
                    }
                }
            }
        }
    }
    process {
        foreach ($Cmd in $CommandName) {
            if ($ModuleName) {
                $Cmd = "$ModuleName\$Cmd"
            }
            Write-Verbose "Searching for $Cmd"
            try {
                $Command = @(Get-Command -Name $Cmd -ErrorAction Stop)
            }
            catch {
                $PSCmdlet.ThrowTerminatingError($_)
            }

            # Resolve aliases (an alias can point to another alias) that's why a while is used and not an if set.
            while ($Command.CommandType -eq 'Alias') {
                try {
                    $Command = @(Get-Command -Name ($Command.Definition) -ErrorAction Stop)[0]
                }
                catch {
                    continue
                }
            }

            if ($null -eq $Command) {
                Write-Warning -Message ('No command with name "{0}" found' -f $Command.Name)
                continue
            }

            Write-Verbose -Message ('Get-Parameter(s) for {0}\ {1}' -f $Command.Source, $Command.Name)

            $Parameters = @{ }

            # Detect provider parameters, i.e. Get-ChildItem.
            $NoProviderParameters = -not $SkipProviderParameters
            # Assume only the core commands have dynamic provider parameters.
            if (-not $SkipProviderParameters -and $Command.Source -eq 'Microsoft.PowerShell.Management') {
                # Only validate commands that has a parameter which could accept a string path.
                foreach ($Param in $Command.Parameters.Values) {
                    if (([String[]], [String] -contains $Param.ParameterType) -and ($Param.ParameterSets.Values | Where-Object { $_.Position -ge 0 })) {
                        $NoProviderParameters = $false
                        break
                    }
                }
            }

            if ($NoProviderParameters) {
                if ($Command.Parameters) {
                    Add-Parameter -OutputParameter $Parameters -InputParameter $Command.Parameters.Values
                }
            }
            else {
                foreach ($Provider in @(Get-PSProvider)) {
                    if ($Provider.Drives.Length -gt 0) {
                        $Drive = Get-Location -PSProvider $Provider.Name
                    }
                    else {
                        $Drive = '{0}\ {1}::\' -f $Provider.ModuleName, $Provider.Name
                    }
                    Write-Verbose ("Get-Command $Command -Args $Drive | Select-Object -Expand Parameters")

                    try {
                        $MoreParameters = (Get-Command $Command -Args $Drive -ErrorAction Stop).Parameters.Values
                    }
                    catch {
                        Write-Verbose -Message ('No provider parameters was found for "{0}" and PSProvider "{1}"' -f $Command, $Provider.Name)
                    }

                    if ($MoreParameters.Length -gt 0) {
                        Add-Parameter -OutputParameter $Parameters -InputParameter $MoreParameters
                    }
                }

                # If for some reason none of the drive paths worked, just use the default parameters
                if ($Parameters.Length -eq 0) {
                    if ($Command.Parameters) {
                        Add-Parameter -OutputParameter $Parameters -InputParameter $Command.Parameters.Values
                    }
                }
            }

            # Calculate the shortest distinct parameter name (alias) - Do this BEFORE removing the common parameters or anything else.
            $Aliases = $Parameters.Values | Select-Object -ExpandProperty Aliases  ## Get defined aliases
            $ParameterNames = $Parameters.Keys + $Aliases
            foreach ($ParameterNameKey in $($Parameters.Keys)) {
                $Aliases = @($ParameterNameKey) + @($Parameters.$ParameterNameKey.Aliases) | Sort-Object { $_.Length }
                $Shortest = '^{0}' -f @($Aliases)[0]

                foreach ($Alias in $Aliases) {
                    $Short = '^'
                    foreach ($Char in [char[]]$Alias) {
                        $Short += $Char
                        $MinimumCharCount = ($ParameterNames -match $Short).Count
                        if ($MinimumCharCount -eq 1 ) {
                            if ($Short.Length -lt $Shortest.Length) {
                                $Shortest = $Short
                            }
                            break
                        }
                    }
                }
                if ($Shortest.Length -lt @($Aliases)[0].Length + 1) {
                    # Overwrite the Aliases with this new value
                    $Parameters.$ParameterNameKey = $Parameters.$ParameterNameKey | Add-Member NoteProperty Aliases ($Parameters.$ParameterNameKey.Aliases + @("$($Shortest.SubString(1))*")) -Force -Passthru
                }
            }

            $CommonParameters = [string[]][System.Management.Automation.Cmdlet]::CommonParameters

            foreach ($ParamSet in @($Command.ParameterSets.Name)) {
                $ParamSet = $ParamSet | Add-Member -Name IsDefault -MemberType NoteProperty -Value ($ParamSet -eq $Command.DefaultParameterSet) -PassThru
                foreach ($Parameter in $Parameters.Keys | Sort-Object) {

                    # Write-Verbose "Parameter: $Parameter"
                    if (-not $Force -and ($CommonParameters -contains $Parameter)) {
                        continue
                    }
                    if ($Parameters.$Parameter.ParameterSets.ContainsKey($ParamSet) -or $Parameters.$Parameter.ParameterSets.ContainsKey('__AllParameterSets')) {
                        if ($Parameters.$Parameter.ParameterSets.ContainsKey($ParamSet)) {
                            $Output = Join-ParameterObject -ParameterMetadata $Parameters.$Parameter -ParameterSetData $Parameters.$Parameter.ParameterSets.$ParamSet
                        }
                        else {
                            $Output = Join-ParameterObject -ParameterMetadata $Parameters.$Parameter -ParameterSetData $Parameters.$Parameter.ParameterSets.__AllParameterSets
                        }

                        $Output | Where-Object { $(foreach ($pn in $ParameterName) { $_.Name -like $Pn }) -contains $true } |
                        Where-Object { $(foreach ($sn in $SetName) { $_.ParameterSet -like $sn }) -contains $true }
                    }
                }
            }
        }
    }
}
function Get-WanIPAddress {
    <#
    .SYNOPSIS
    The Get-WanIPAddress function retrieves the current external IP address.
 
    .DESCRIPTION
    The Get-WanIPAddress function retrieves the current external IP address using ifconfig.co as the API provider.
 
    .EXAMPLE
    Get-WanIPAddress
 
    IP Address Country City Hostname ISP
    ---------- ------- ---- -------- ---
    123.45.67.89 Sweden Gothenburg h-123-45.A56.priv.contoso.com Contso.com
 
    .LINK
        https://github.com/Omnicit/Omnicit/blob/master/docs/en-US/Get-WanIPAddress.md
    #>

    [Alias('Get-ExternalIP','gwan')]
    [CmdletBinding(
        SupportsShouldProcess
    )]
    param()

    try {
        if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME)) {
            [Net.ServicePointManager]::SecurityProtocol = 3072, 768, 192 # TLS12, TLS11, TLS
            $Json = Invoke-RestMethod -Method Get -Uri 'https://ifconfig.co/json' -ErrorAction Stop

            [PSCustomObject]@{
                PSTypeName               = 'Omnicit.Get.WanIPAddress'
                IP_address               = $Json.ip
                IP_decimal               = $Json.ip_decimal
                Country                  = $Json.country
                EU_Country               = $Json.country_eu
                ISO_Country              = $Json.country_iso
                City                     = $Json.city
                Hostname                 = $Json.hostname
                Latitude                 = $Json.latitude
                Longitude                = $Json.longitude
                Autonomous_System_Number = $Json.asn
                ISP                      = $Json.asn_org
            }
        }
    }
    catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}
function Invoke-ModuleUpdate {
    <#
    .SYNOPSIS
    Update one, several or all installed modules if an update is available from a repository location.
 
    .DESCRIPTION
    Invoke-ModuleUpdate for installed modules that have a repository location, for example from PowerShell Gallery.
    The function, without the Update parameter, returns the current and the latest version available for each installed module with a repository location.
    If there is any existing installed versions for each module that current version number will be displayed under Multiple versions.
    When the Update parameter is issued the function will update the named modules.
 
    The script is based on the "Check-ModuleUpdate.ps1" from Jeffery Hicks* to check for available updates for installed PowerShell modules.
    *Credit: http://jdhitsolutions.com/blog/powershell/5441/check-for-module-updates/
 
    .PARAMETER Name
    Specifies names or name patterns of modules that this function gets. Wildcard characters are permitted.
 
    .PARAMETER Update
    Switch parameter to invoke the 'Update-Module' cmdlet for the targeted modules. The default behavior without this switch is that the function will only list the current and available versions.
 
    .PARAMETER Force
    Switch parameter forces the update of each specified module, regardless of the current version of the module installed. Using the 'Force' parameter without using 'Update' parameter does not perform anything extra.
 
    .EXAMPLE
    Invoke-ModuleUpdate
 
    Name Current Version Online Version Multiple Versions
    ---- --------------- -------------- -----------------
    SpeculationControl 1.0.0 1.0.8 False
    AzureAD 2.0.0.131 2.0.1.10 {2.0.0.115}
    AzureADPreview 2.0.0.154 2.0.1.11 {2.0.0.137}
    ISESteroids 2.7.1.7 2.7.1.7 {2.6.3.30}
    MicrosoftTeams 0.9.1 0.9.3 False
    NTFSSecurity 4.2.3 4.2.3 False
    Office365Connect 1.5.0 1.5.0 False
    ... ... ... ...
 
    This example returns the current and the latest version available for all installed modules that have a repository location.
 
    .EXAMPLE
    Invoke-ModuleUpdate -Update
 
    Name Current Version Online Version Multiple Versions
    ---- --------------- -------------- -----------------
    SpeculationControl 1.0.8 1.0.8 {1.0.0}
    AzureAD 2.0.1.10 2.0.1.10 {2.0.0.131, 2.0.0.115}
    AzureADPreview 2.0.1.11 2.0.1.11 {2.0.0.137, 2.0.0.154}
    ISESteroids 2.7.1.7 2.7.1.7 {2.6.3.30}
    MicrosoftTeams 0.9.3 0.9.3 {0.9.1}
    NTFSSecurity 4.2.3 4.2.3 False
    Office365Connect 1.5.0 1.5.0 False
    ... ... ... ...
 
    This example installs the latest version available for all installed modules that have a repository location.
 
    .EXAMPLE
    Invoke-ModuleUpdate -Name 'AzureAD', 'PSScriptAnalyzer' -Update -Force
 
    Name Current Version Online Version Multiple Versions
    ---- --------------- -------------- -----------------
    AzureAD 2.0.1.10 2.0.1.10 {2.0.0.131, 2.0.0.115}
    PSScriptAnalyzer 1.17.1 1.17.1 {1.17.0}
 
    This example will force install the latest version available for the AzureAD and PSScriptAnalyzer modules.
 
    .LINK
        https://github.com/Omnicit/Omnicit/blob/master/docs/en-US/Invoke-ModuleUpdate.md
    #>

    [CmdletBinding(
        DefaultParameterSetName = 'NoUpdate',
        SupportsShouldProcess
    )]
    param (
        # Specifies names or name patterns of modules that this cmdlet gets. Wildcard characters are permitted.
        [Parameter(
            ParameterSetName = 'NoUpdate',
            ValueFromPipeline,
            Position = 0
        )]
        [Parameter(
            ParameterSetName = 'Update',
            ValueFromPipeline,
            Position = 0
        )]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [string[]]$Name = '*',

        # Switch parameter to invoke the 'Update-Module' cmdlet for the targeted modules. The default behavior without this switch is that the function will only list the current and available versions for installed modules.
        [Parameter(
            ParameterSetName = 'Update',
            Position = 1
        )]
        [switch]$Update,

        # Switch parameter forces the update of each specified module, regardless of the current version of the module installed. Using the 'Force' parameter without using 'Update' parameter does not perform anything extra.
        [Parameter(
            ParameterSetName = 'Update',
            Position = 2
        )]
        [switch]$Force
    )

    begin {
        if ((-not $PSBoundParameters.ContainsKey('Update')) -and $PSBoundParameters.ContainsKey('Force')) {
            Write-Verbose -Message 'Using the "Force" parameter without using "Update" parameter does not perform anything extra.'
        }

        try {
            [array]$AllModules = (Get-Module -Name $Name -ListAvailable -ErrorAction Stop -Verbose:$false).Where{ $null -ne $_.RepositorySourceLocation }

            # Group all modules to exclude multiple versions.
            [array]$Modules = $AllModules | Group-Object -Property Name
            [int]$TotalCount = $Modules.Count

            switch ($Update) {
                $true {
                    [string]$Status = 'Updating module'
                }
                Default {
                    [string]$Status = 'Looking for the latest version of module'
                }
            }
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }

        try {
            # To speed up the 'Find-Module' cmdlet and not query all available session repositories.
            [array][PSCustomObject]$Repositories = Get-PSRepository -ErrorAction Stop

            if ($PSCmdLet.ParameterSetName -eq 'Update' -and $Repositories.InstallationPolicy -contains 'Untrusted') {
                Write-Verbose -Message 'One or more repositories have the InstallationPolicy set to Untrusted.'
                Write-Verbose -Message 'The function will temporary set all repositories to Trusted to avoid continues prompts of "Set-PSRepository" and revert back after finished updating.'
                $RepositoryChanged = $true
                foreach ($Repository in $Repositories) {
                    Set-PSRepository -Name $Repository.Name -InstallationPolicy Trusted -ErrorAction SilentlyContinue -WarningAction SilentlyContinue -Verbose:$false
                }
            }
            else {
                $RepositoryChanged = $false
            }
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }

    }
    process {
        foreach ($Group in $Modules) {
            [int]$PercentComplete = ' {0:N0}' -f (($Modules.IndexOf($Group) / $TotalCount) * 100)
            Write-Progress -Activity (' {0} {1}' -f $Status, $Group.Group[0].Name) -Status (' {0}% Complete:' -f $PercentComplete) -PercentComplete $PercentComplete

            if ($PSCmdlet.ShouldProcess(('{0}' -f $Group.Group[0].Name), $MyInvocation.MyCommand.Name)) {
                $MultipleVersions = @()
                switch ($Group.Count) {
                    ( { $PSItem -gt 1 }) {
                        [string[]]$MultipleVersions = $Group.Group.Version[1..($Group.Group.Version.Length)]
                        [PSModuleInfo]$Module = (($Group).Group | Sort-Object -Property Version -Descending)[0]
                    }
                    Default {
                        $MultipleVersions = $null
                        [PSModuleInfo]$Module = $Group.Group[0]
                    }
                }
                try {
                    if (($Repository = ($Repositories.Where{ [string]$_.SourceLocation -eq [string]$Module.RepositorySourceLocation }).Name)) {
                        $FindModule = @{
                            Repository  = $Repository
                            ErrorAction = 'Stop'
                        }
                    }
                    else {
                        $FindModule = @{
                            ErrorAction = 'Stop'
                        }
                    }
                    [PSCustomObject]$Online = Find-Module -Name $Module.Name @FindModule
                }
                catch {
                    Write-Warning -Message ('Unable to find module {0}. Error: {1}' -f $Module.Name, $_.Exception.Message)
                    continue
                }

                [version]$CurrentVersion = $Module.Version
                if ($PSBoundParameters.ContainsKey('Update')) {
                    if ([version]$Online.Version -gt [version]$Module.Version) {
                        try {
                            Update-Module -Name $Module.Name -Force:$PSBoundParameters['Force'] -ErrorAction Stop
                            [version]$CurrentVersion = $Online.Version
                            $MultipleVersions += $Module.Version
                        }
                        catch {
                            Write-Warning -Message ('Unable to update module. Error: {0}' -f $_.Exception.Message)
                            [version]$CurrentVersion = $Module.Version
                        }
                    }
                    else {
                        [version]$CurrentVersion = $Online.Version
                    }
                }

                # Output result to pipeline
                [PSCustomObject]@{
                    'PSTypeName'        = 'Omnicit.Invoke.ModuleUpdate'
                    'Name'              = [string]$Module.Name
                    'Current Version'   = $CurrentVersion
                    'Online Version'    = $Online.Version
                    'Multiple Versions' = $MultipleVersions
                }
            }
        }
    }
    end {
        try {
            if ($RepositoryChanged) {
                Write-Verbose -Message 'Reverting back installation policies for repositories.'
                foreach ($Repository in $Repositories) {
                    Set-PSRepository -Name $Repository.Name -InstallationPolicy $Repository.InstallationPolicy -ErrorAction SilentlyContinue -WarningAction SilentlyContinue -Verbose:$false
                }
            }
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}
function Start-Mstsc {
    <#
    .SYNOPSIS
    Start a Mstsc process using predefined arguments from parameters.
 
    .DESCRIPTION
    The Start-Mstsc function starts a mstsc.exe process with arguments defined from the parameters.
    Built in check to validate that the current system is Windows.
    Shadow parameters are not yet implemented.
    Default port is defined as 3389.
 
    .EXAMPLE
    Start-Mstsc -ComputerName Server01
    Starts mstsc.exe with the arguments /v:Server01:3389
 
    Connects to Server01 using the default RDP port 3389.
    .EXAMPLE
    Start-Mstsc -ComputerName Server01 -Admin
    Starts mstsc.exe with the arguments /v:Server01:3389 /admin
 
    Connects to the console on Server01 using the default RDP port 3389.
    .EXAMPLE
    Start-Mstsc -ComputerName Server01 -Admin -Port 9876
    Starts mstsc.exe with the arguments /v:Server01:9876 /admin
 
    Connects to the console on Server01 using port 9876.
    .EXAMPLE
    Start-Mstsc -ComputerName Server01 -Admin -Port 9876 -FullScreen
    Starts mstsc.exe with the arguments /v:Server01:9876 /admin /f
 
    Connects to the console on Server01 using port 9876 with full-screen.
    .EXAMPLE
    Start-Mstsc -ComputerName Server01 -Admin -Port 9876 -FullScreen -Prompt
    Starts mstsc.exe with the arguments /v:Server01:9876 /admin /f /prompt
 
    Connects to the console on Server01 using port 9876 with full-screen and prompt for credentials.
    .EXAMPLE
    Start-Mstsc -ComputerName Server01 -Admin -Port 9876 -FullScreen -Prompt -Public
    Starts mstsc.exe with the arguments /v:Server01:9876 /admin /f /prompt /public
 
    Connects to the console on Server01 using port 9876 with full-screen, prompt for credentials and run RDP in public mode.
    .EXAMPLE
    Start-Mstsc -ComputerName Server01 -RestrictedAdmin
    Starts mstsc.exe with the arguments /v:Server01:3389 /restrictedAdmin
 
    Connects to Server01 using the default RDP port 3389 with Restricted Admin.
    .EXAMPLE
    Start-Mstsc -ComputerName Server01 -RemoteGuard
    Starts mstsc.exe with the arguments /v:Server01:3389 /remoteGuard
 
    Connects to Server01 using the default RDP port 3389 with Remote Guard.
    .LINK
        https://github.com/Omnicit/Omnicit/blob/master/docs/en-US/Start-Mstsc.md
    #>

    [Alias('remote')]
    [CmdletBinding(
        SupportsShouldProcess,
        PositionalBinding,
        DefaultParameterSetName = 'Default'
    )]
    param(
        # Specifies the remote PC or Server to which you want to connect.
        [Parameter(
            Position = 1,
            HelpMessage = 'Specify the remote PC or Server to which you want to connect',
            Mandatory
        )]
        [Alias('Server', 'IPAddress', 'Target', 'Node', 'Client')]
        [string[]]$ComputerName,

        # Connects you to the console session for administering a remote PC.
        [Parameter(
            Position = 2
        )]
        [Alias('Console')]
        [switch]$Admin,

        # Specifies the port of the remote PC to which you want to connect. Default value is 3389.
        [Parameter(
            Position = 3
        )]
        [ValidateRange(1, 65535)]
        [ValidateNotNullOrEmpty()]
        [int]$Port = 3389,

        # Starts Remote Desktop in full-screen mode.
        [Parameter(
            ParameterSetName = 'FullScreen'
        )]
        [switch]$FullScreen,

        # Specifies the width of the Remote Desktop window.
        [Parameter(
            ParameterSetName = 'FixedSize',
            Mandatory
        )]
        [Alias('w')]
        [ValidateRange(1, 10000)]
        [int]$Width,

        # Specifies the height of the Remote Desktop window.
        [Parameter(
            ParameterSetName = 'FixedSize',
            Mandatory
        )]
        [Alias('h')]
        [ValidateRange(1, 10000)]
        [int]$Height,

        <#
        Matches the remote desktop width and height with the local virtual desktop, spanning across multiple monitors, if necessary.
        To span across monitors, the monitors must be arranged to form a rectangle.
        #>

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

        # Configures the Remote Desktop Services session monitor layout to be identical to the current client-side configuration.
        [Parameter(
            ParameterSetName = 'MultiMonitor'
        )]
        [Alias('multimon')]
        [switch]$MultiMonitor,

        # Prompts you for your credentials when you connect to the remote PC.
        [switch]$Prompt,

        # Runs Remote Desktop in public mode.
        [switch]$Public,

        <#
        Connects you to the remote PC in Restricted Administration mode.
        In this mode, credentials won't be sent to the remote PC, which can protect you if you connect to a PC that has been compromised.
        However, connections made from the remote PC might not be authenticated by other PCs, which might impact application functionality and compatibility.
        This parameter implies the admin parameter.
        #>

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

        <#
        Connects your device to a remote device using Remote Guard.
        Remote Guard prevents credentials from being sent to the remote PC, which can help protect your credentials if you connect to a remote PC that has been compromised.
        Unlike Restricted Administration mode, Remote Guard also supports connections made from the remote PC by redirecting all requests back to your device.
        #>

        [Parameter(
            ParameterSetName = 'RemoteGuard'
        )]
        [switch]$RemoteGuard
    )
    begin {
        try {
            $PreviousErrorActionPreference = $ErrorActionPreference
            $ErrorActionPreference = 'Continue'

            if (Get-Variable IsWindows -ErrorAction SilentlyContinue) {
                if (-not $IsWindows) {
                    throw [System.NotSupportedException]::New('Start-Mstsc is only supported on Windows operating systems.')
                }
            }
            else {
                Write-Verbose -Message 'Running Start-Mstsc as Windows PowerShell.'
            }
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
        finally {
            $ErrorActionPreference = $PreviousErrorActionPreference
        }
        $RDTable = @{
            'Admin'           = '/admin'
            'Width'           = ('/w:{0}' -f $Width)
            'Height'          = ('/h:{0}' -f $Height)
            'FullScreen'      = '/f'
            'Span'            = '/span'
            'MultiMonitor'    = '/multimon'
            'Prompt'          = '/prompt'
            'Public'          = '/public'
            'RestrictedAdmin' = '/restrictedAdmin'
            'RemoteGuard'     = '/remoteGuard'
        }
    }
    process {
        foreach ($Computer in $ComputerName) {
            try {
                $RDTable.Add('ComputerName', ('/v:{0}:{1}' -f $Computer, $Port))
                [Text.StringBuilder]$RDString = [Text.StringBuilder]::new()

                foreach ($Key in $PSBoundParameters.Keys) {
                    if ($RDTable[$Key]) {
                        $null = $RDString.Append($RDTable[$Key])
                        $null = $RDString.Append(' ')
                    }
                }
                $RDTable.Remove('ComputerName')
                $RDArgument = $RDString.ToString().Trim()
            }
            catch {
                $PSCmdlet.ThrowTerminatingError($_)
            }
            if ($PSCmdlet.ShouldProcess($RDArgument)) {
                try {
                    Write-Verbose -Message ('Starting mstsc with the following arguments: {0}' -f ($RDArgument))
                    Start-Process -FilePath "$env:windir\system32\mstsc.exe" -ArgumentList ($RDArgument)
                }
                catch {
                    $PSCmdlet.ThrowTerminatingError($_)
                }
            }
        }
    }
}