AdminToolkit.psm1

#Region $AdminToolkitADComputernameArgCompleter
$AdminToolkitADComputernameArgCompleter = {
    param ($CommandName, $ParameterName, $StringMatch)
    if ($null -eq $StringMatch) {
        $Filter = "*"
    }
    else {
        $Filter = "*$StringMatch*"
    }
    (Get-ADComputer -filter { Name -like $Filter }).Name
}
Register-ArgumentCompleter -CommandName Push-LocalScheduledTask,Get-RebootLogs,Clear-CCMCache -ParameterName ComputerName -ScriptBlock $AdminToolkitADComputernameArgCompleter
#EndRegion $AdminToolkitADComputernameArgCompleter
#Region $AdminToolkitScheduledTaskNameArgCompleter
$AdminToolkitScheduledTaskNameArgCompleter = {
    param ($CommandName, $ParameterName, $StringMatch)
    $Tasks = (Get-ScheduledTask | Where-Object { $_.TaskName -match "$StringMatch" }).TaskName
    $QuotedTasks = foreach ($Task in $Tasks) {
        $QuotedTask = "`"$Task`""
        $QuotedTask
    }
    return $QuotedTasks
}
Register-ArgumentCompleter -CommandName Push-LocalScheduledTask -ParameterName TaskName -ScriptBlock $AdminToolkitScheduledTaskNameArgCompleter
#EndRegion $AdminToolkitScheduledTaskNameArgCompleter
#Region Test-IsIpAddressInRange

<#
.SYNOPSIS
    Test to see if a given IP is between a given start and end address.
.DESCRIPTION
    Provide the IP Range (Start and End Address) and the IP to check. This function will return True or False.
.PARAMETER IpAddress
    Specify the IP to test if it is in a specific range.
.PARAMETER StartAddress
    Specify the start IP Address for the range you want to test.
.PARAMETER EndAddress
    Specify the end IP Address for the range you want to test.
.EXAMPLE
    Test-IsIpAddressInRange -IpAddress 10.10.0.235 -StartAddress 10.10.0.0 -EndAddress 10.10.120.0

    Description
    -----------
    This will return True, since 10.10.0.235 is within the given Start and End addresses.
.NOTES
    Author: Matthew DeGarmo
    GitHub: https://github.com/matthewjdegarmo
#>

Function Test-IsIpAddressInRange() {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]
        [System.String] $IpAddress,

        [Parameter(Mandatory)]
        [System.String] $StartAddress,

        [Parameter(Mandatory)]
        [System.String] $EndAddress
    )
    
    $ip = [System.Net.IPAddress]::Parse($IpAddress).GetAddressBytes()
    [array]::Reverse($ip)
    $ip = [System.BitConverter]::ToUInt32($ip, 0)

    $Start = [System.Net.IPAddress]::Parse($StartAddress).GetAddressBytes()
    [array]::Reverse($Start)
    $Start = [System.BitConverter]::ToUInt32($Start, 0)

    $End = [System.Net.IPAddress]::Parse($EndAddress).GetAddressBytes()
    [array]::Reverse($End)
    $End = [System.BitConverter]::ToUInt32($End, 0)

    $Start -le $ip -and $ip -le $End
}
#EndRegion Test-IsIpAddressInRange
#Region Aliases
Set-Alias -Name GD -Value Get-Definition
Set-Alias -Name Watch -Value Watch-Command
#EndRegion Aliases
#Region Clear-Arp

<#
.SYNOPSIS
    Use Netsh to clear ArpCache
.DESCRIPTION
    Clears the local arp table
.EXAMPLE
    PS> Clear-Arp

    Description
    -----------
    This will clear the arpcache on the local machine.
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Clear-Arp() {
    [CmdletBinding()]
    param()
    netsh.exe interface ip delete arpcache
}
#EndRegion Clear-Arp
#Region Clear-CCMCache

<#
.SYNOPSIS
    Clear local CCM Cache.
.DESCRIPTION
    This command will clear the local or remote ccm cache.
.PARAMETER ComputerName
    Specify the remote system to connect to and clear.
.EXAMPLE
    PS> Clear-CCMCache

    Description
    -----------
    Clear the CCM Cache on the local system.
.EXAMPLE
    PS> Clear-CCMCache -ComputerName Some-Remote-PC

    Description
    -----------
    This will attempt to connect and clear the CCM Cache from the computer specified.
.EXAMPLE
    PS> Clear-CCMCache -ComputerName pc1,pc2,pc3,pc4,pc5

    Description
    -----------
    This will attempt to connect to each computer listed to clear the local CCM Cache.
.NOTES
    Author: Matthew J. DeGarmo
    
    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Clear-CCMCache() {
    [CmdletBinding()]
    param(
        [Parameter()]
        [System.String[]] $ComputerName = $env:COMPUTERNAME
    )

    begin {}

    process {
        try {
            Invoke-Command -ComputerName $ComputerName -ScriptBlock {
                Write-Output "Clearing CCM Cache on $($env:COMPUTERNAME)"
                ## Initialize the CCM resource manager com object
                [__comobject]$CCMComObject = New-Object -ComObject 'UIResource.UIResourceMgr'
                ## Get the CacheElementIDs to delete
                $CacheInfo = $CCMComObject.GetCacheInfo().GetCacheElements()
                ## Remove cache items
                ForEach ($CacheItem in $CacheInfo) {
                    $null = $CCMComObject.GetCacheInfo().DeleteCacheElement([string]$($CacheItem.CacheElementID))
                }
            }
        } catch {
            Write-Error "$($_.Exception.Message)"
        }
    }

    end {}
}
#EndRegion Clear-CCMCache
#Region Copy-WithProgress

<#
.SYNOPSIS
    This function performs a copy of a specified object recursively to a specified location.
.DESCRIPTION
    This function is a glorified Copy-Item in that it will show progress data. If moving 10,000 files that equal 2GB in size, it will show you what file you are currently on as well as how much data has been moved / what is left using Write-Progress.
.PARAMETER Path
    Source should specify the object to be copied by name. This value must be the FullPath and cannot be shortened. An example would be if you were in the C:\Scripts directory, you could not specify '.\TestFile.ps1' as the source location, you must specify 'C:\Scripts\TestFile.ps1' in this case.
.PARAMETER Destination
    Destination should specify the target location of the specified Source by name. This value must be the FullPath and cannot be shortened. An example would be if you were in the C:\Scripts directory, you could not specify '.\TestFile.ps1' as the Destination location, you must specify 'C:\Scripts\TestFile.ps1' in this case
.PARAMETER IncludeACL
    With this present, this will copy the ACL from each source file and apply it to the destination file.
.INPUTS
    System.String[]
        This function does not accept pipeline data. The values for all parameters must be specified.
.OUTPUTS
    None
        This function does not produce output except for the Write-Progress data.
.EXAMPLE
    PS>Copy-WithProgress -Source "C:\Scripts\TestFile.ps1" -Destination "C:\Temp\TestFile.ps1"

    Description
    -----------
    This will copy the source file to the file specified in Destination. Note that the filename for Destination can be anything and does not have to match the original.
.EXAMPLE
    PS>Copy-WithProgress -Source .\Folder -Destination .\Folder1 -IncludeACL

    Description
    -----------
    This will copy all contents of .\Folder to .\Folder1 and include the Acl / NTFS permissions.
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Copy-WithProgress() {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 0)]
        [Alias('Source')]
        $Path,

        [Parameter(Mandatory = $true,
            ValueFromPipelineByPropertyName = $true,
            Position = 1)]
        $Destination,

        [Parameter()]
        [Switch] $IncludeACL
    )

    Write-Progress -Activity "Gathering data from $Path"
    $Source = (Resolve-Path -Path $Path).Path.Replace('Microsoft.PowerShell.Core\FileSystem::', '').ToLower()
    $Destination = $Destination.Replace('Microsoft.PowerShell.Core\FileSystem::', '').ToLower()
    $Filelist = Get-Childitem $Source -Recurse
    $Total = $Filelist.count
    $Position = 0
    $Size = ($Filelist | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
    
    Write-Progress -Activity "Gathering data from $Source" -Completed
    foreach ($File in $Filelist) {
        switch ($Size) {
            { $_ -ge '1000000000' } { $TotalSize = "{0:N2} GB" -f ($_ / 1GB) }
            { ($_ -lt '1000000000') -and ($_ -ge '10000000') } { $TotalSize = "{0:N2} MB" -f ($_ / 1MB) }
            { $_ -lt '1000000' } { $TotalSize = "{0:N2} KB" -f ($_ / 1KB) }
        }
        $FileSize = ($File | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
        $Filename = $File.Fullname.ToLower().Replace($Source, '').Replace('Microsoft.PowerShell.Core\FileSystem::', '')
        $DestinationFile = Join-Path $Destination $FileName
        $Position++
        $Percent = [int]$(($Position / $Total) * 100)
        Write-Progress -Activity "Copying data from '$Source' to '$Destination'" -Status "Copying File $Position of $total - $TotalSize remaining..." -PercentComplete (($Position / $total) * 100) -CurrentOperation "$Percent% complete"
        #$null = New-Item -Name $File.FullName -Path $DestinationFile -ItemType File -Force
        $null = Copy-Item $File.FullName -Destination $DestinationFile -Force -ErrorAction SilentlyContinue -Container
        If ($IncludeACL.IsPresent) {
            $SourceFileACL = Get-Acl -Path $File.FullName
            Set-Acl -Path $DestinationFile -AclObject $SourceFileACL
        }
        $Size = ($Size - $FileSize)
    }
    Write-Progress -Activity "Moving data from '$Source' to '$Destination'" -Completed 
}
#EndRegion Copy-WithProgress
#Region DateStamp
<#
This is to pass the cmdlet exporting pester tests since this is a filter
Function DateStamp() {
#>


<#
.SYNOPSIS
    This is a filter used to place timestamps on any output messages.
.DESCRIPTION
    The function `TimeStamp` is a colorized version of this command `DateStamp`, but `TimeStamp` output cannot be written to a file. You will want to use `DateStamp` if you are going to output your messages into a log or txt file.
.EXAMPLE
    "ERROR: Something bad happened on this line of the script" | DateStamp

    [08/04/2020 11:34:35]: ERROR: Something bad happened on this line of the script

    Description
    -----------
    This line will place a time stamp at the beginning of the line that can be written to a file.
.NOTES
    Author: Matthew J. DeGarmo
    Site: https://matthewjdegarmo.github.io

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Filter DateStamp() {
    [CmdletBinding()]
    param()
    process {
        Write-Output "[$(Get-Date -Format "MM/dd/yyyy HH:mm:ss")]: $_"
    }
}
#EndRegion DateStamp
#Region Enable-Remoting

<#
.Synopsis
Enable PSRemoting via PSEXEC remotely.

.Description
This Command will enable PowerShell Remoting on a remote PC.

.PARAMETER ComputerName
    Specify a remote computer to run against.

.PARAMETER Username
    Specify a username to use to make the remote connection.

.PARAMETER Password
    Specify the respective password to match the Username provided.
    
.EXAMPLE
PS> Enable-PSRemoting -computer PCName -username domain\username

Description
-----------
This will enable remoting and then prompt for credentials

.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit

    Change Log:
    Version: 1.0 - Function Creation.

    This Function requires psexec. If you do not, download it with the sysinternals suite. Add psexec to one of your enviroment variable paths.
#>

Function Enable-Remoting() {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0, Mandatory)]
        [string] $ComputerName,
        [Parameter(Position = 1, Mandatory)]
        [string] $Username,
        [Parameter(Position = 2)]
        [SecureString] $Password
    )

    #Enabling PSRemoting
    PsExec.exe \\$ComputerName -s winrm.cmd quickconfig -q
    PsExec.exe \\$ComputerName -u $Username -p $Password powershell.exe cmd /c "enable-psremoting -force"

    try {
        Test-WSMan $Computer
    }
    catch {
        Write-Error "Failed to enable PSRemoting via PSEXEC"
    }
}
#EndRegion Enable-Remoting
#Region Get-Applications

<#
.SYNOPSIS
    List locally installed applications

.DESCRIPTION
    Query local registry for installed applications.
.EXAMPLE
    PS> Get-Applications

    Description
    -----------
    This will generate all installed applications on the local system.
.NOTES
    Author: Matthew J. DeGarmo
    Site: https://matthewjdegarmo.github.io

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Get-Applications() {
    [CmdletBinding()]
    param()
    $RegPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
    Get-ChildItem -Path $RegPath | Get-ItemProperty |
    Sort-Object DisplayName
}
#EndRegion Get-Applications

#Region Get-CIDRNotationBySubnetMask

<#
.SYNOPSIS
    Quickly generate the CIDR "slash" notation for a given subnet mask.
.DESCRIPTION
    This will provide the CIDR value for a subnet mask. This function will also error if the subnet mask is not valid.
.PARAMETER SubnetMask
    Specify the subnet mask to generate the CIDR Notation for.
.EXAMPLE
    PS> Get-CIDRNotationBySubnetMask 255.255.255.0

    24
    Description
    -----------
    Providing the SubnetMask, this returns the correct CIDR abreviation. CIDR is used like this: 192.168.1.0/24
.NOTES
    Author: Matthew J. DeGarmo
    Site: https://matthewjdegarmo.github.io

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Get-CIDRNotationBySubnetMask() {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0)]
        [String] $SubnetMask
    )
    $cidr = 0
    $octet = 0 
    $SubnetMask.split(".") | Foreach-Object {
        switch ($_) {
            255 { $cidr += 8 ; $CorrectSubnet += "$_." }
            254 { $cidr += 7 ; $CorrectSubnet += "$_." }
            252 { $cidr += 6 ; $CorrectSubnet += "$_." }
            248 { $cidr += 5 ; $CorrectSubnet += "$_." }
            240 { $cidr += 4 ; $CorrectSubnet += "$_." }
            224 { $cidr += 3 ; $CorrectSubnet += "$_." }
            192 { $cidr += 2 ; $CorrectSubnet += "$_." }
            128 { $cidr += 1 ; $CorrectSubnet += "$_." }
            0 { $cidr += 0 ; $CorrectSubnet += "$_." }
            default { 
                $SplitSubnet = $SubnetMask.Split('.')
                $SplitSubnet[$octet] = "[$($SplitSubnet[$octet])]"
                $ErrorSubnet = $SplitSubnet -join '.'
                Write-Error -Message "Invalid Subnet Mask value: `'$_`' in $ErrorSubnet" `
                    -Category InvalidArgument `
                    -RecommendedAction "Provide a proper SubnetMask" `
                    -ErrorAction Stop
                $BadMask = $true
            }
        }
        $octet++
    }
    if (-Not ($BadMask)) {
        $cidr
    }
}
#EndRegion Get-CIDRNotationBySubnetMask
#Region Get-ContentWithLineNumbers

<#
.Synopsis
    Mimic Unix / Linux tool nl number lines
.Description
    Print file content with numbered lines no original nl options supported
.PARAMETER FileName
    Specify a file to extract and prefix with line numbers.
.PARAMETER InputObject
    Specify an object of text to prefix with line numbers.
.Example
    PS> Get-ContentWithLineNumbers -FileName C:\Foo.txt
    
    Description
    -----------
    This will append line numbers to the begninning of each line in the Foo.txt file.
.NOTES
    Author: Matthew J. DeGarmo
    Site: https://matthewjdegarmo.github.io

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Get-ContentWithLineNumbers() {
    [CmdletBinding()]
    param (
        [parameter(mandatory = $true, Position = 0, ValueFromPipeline, ParameterSetName = 'File')]
        [String]$FileName,

        [Parameter(ParameterSetName = 'Input')]
        $InputObject
    )
           
    process {
        if ($PSBoundParameters.ContainsKey('FileName')) {
            If (Test-Path $FileName) {
                $Data = Get-Content $FileName | ForEach-Object { "{0,5} {1}" -f $_.ReadCount, $_ }
            }
            $Data
        }
        elseif ($PSBoundParameters.ContainsKey('Input')) {
            $inData = New-Object -TypeName System.IO.StringReader -ArgumentList $InputObject
            $Data = While ($Line = $InData.ReadLine()) { $Line }
            $Data | ForEach-Object { "{0,5} {1}" -f $_.ReadCount, $_ }
        }
    }
}
#EndRegion Get-ContentWithLineNumbers
#Region Get-Definition

<#
.SYNOPSIS
    Gets the back-end definition of a function.
.DESCRIPTION
    This function will export a string of the code that defines a function.
.PARAMETER Function
    This parameter takes a function name, or an alias name, to generate the function definition.
.EXAMPLE
    PS> Get-Definition Get-Definition

    Description
    -----------
    This will get the function definitnion for the `Get-Definition` command itself.
.EXAMPLE
    PS> Get-Definition glo | Clip

    Description
    -----------
    This will get the definition for the `glo` aliased command, and pipe it into your clipboard using the clip.exe file.
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo
    
    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Get-Definition() {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Function
    )

    $null = Get-Command -Name $Function -ErrorAction SilentlyContinue
    $Alias = (Get-Alias -Name $Function -ErrorAction SilentlyContinue).ResolvedCommand.Name
    if ($Alias) {
        Write-Warning "'$Function' is an alias for '$Alias'. Running 'Get-Definition -Function $Alias'."
        $Function = $Alias
    }
    $FunctionDefinition = (Get-Command -name $Function | Select-Object -ExpandProperty Definition)
    $returnDefinition = [System.Text.StringBuilder]::New()

    $null = $returnDefinition.Append("function $Function`() {")
    $null = $returnDefinition.Append($FunctionDefinition)
    $null = $returnDefinition.Append('}')

    $returnDefinition.ToString()
}
#EndRegion Get-Definition
#Region Get-FileOwner

<#
.Synopsis
    Display the owner of an item(s)

.Description
    This Function lists file owners within a given path

.PARAMETER Path
    Specify the file / directory path to query.

.PARAMETER Recursive
    Search recursively.

.Example
    PS> Get-FileOwner C:\Users

    Description
    -----------
    This will list file owners recursively for this directory.

.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Get-FileOwner() {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [string] $Path,

        [switch]$Recursive
    )

    $LastWrite = @{
        Name       = 'Last Write Time'
        Expression = { $_.LastWriteTime.ToString('u') }
    }
    $Owner = @{
        Name       = 'File Owner'
        Expression = { (Get-Acl $_.FullName).Owner }
    }
    $HostName = @{
        Name       = 'Host Name'
        Expression = { $env:COMPUTERNAME }
    }

    Get-ChildItem @PSBoundParameters |
    Select-Object $HostName, $Owner, Name, Directory, $LastWrite, Length
}
#EndRegion Get-FileOwner
#Region Get-FolderSize

<#
.SYNOPSIS
    Quickly calculate the size of a directory.

.DESCRIPTION
    This function will calculate the disk space used by a specified directory. This uses the current directory by default.
.PARAMETER Folder
    Specify the folder to query. This defaults to the current directory.
.PARAMETER ComputerName
    Specify a remote computer to get the folder size of.
    NOTE: The `-Folder` path will still need to be the local path for the remote computer
    Example:
    Get-FolderSize -ComputerName Some-Remote-PC -Folder C:\Windows\System
.EXAMPLE
    PS> Get-FolderSize

    Description
    -----------
    This will display the folder size of the current folder location `Get-Location`
.EXAMPLE
    PS> Get-FolderSize -ComputerName Some-Remote-PC -Folder C:\Windows\System

    Description
    -----------
    This will get the folder size of the C:\Windows\System folder on the remote pc specified.
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Get-FolderSize() {
    [CmdletBinding()]
    Param (
        [Parameter(
            Position = 0,
            ValueFromPipeline
        )]
        [System.String] $Folder = (Get-Location),

        [Parameter()]
        [System.String] $ComputerName
    )

    Begin {

    }

    Process {
        If ($ComputerName) {
            $Size = Invoke-Command -ComputerName $ComputerName -ScriptBlock {
                (Get-ChildItem $using:Folder -Recurse | Measure-Object -Property Length -Sum -ErrorAction Stop).Sum
            }
        } Else {
            $Size = (Get-ChildItem $Folder -Recurse | Measure-Object -Property Length -Sum -ErrorAction Stop).Sum
        }
        switch ($Size) {
            { ($_ -lt '1000000000000000') -and ($_ -ge '1000000000000') } { $TotalSize = "{0:N2} TB" -f ($_ / 1TB) }
            { ($_ -lt '1000000000000') -and ($_ -ge '1000000000') } { $TotalSize = "{0:N2} GB" -f ($_ / 1GB) }
            { ($_ -lt '1000000000') -and ($_ -ge '1000000') } { $TotalSize = "{0:N2} MB" -f ($_ / 1MB) }
            { $_ -lt '1000000' } { $TotalSize = "{0:N2} KB" -f ($_ / 1KB) }
        }
        
        $TotalSize
    }
}
#EndRegion Get-FolderSize
#Region Get-IPv4NetworkInfo

<#
.SYNOPSIS
    Gets extended information about an IPv4 network.
.DESCRIPTION
    Gets Network Address, Broadcast Address, Wildcard Mask.
    and usable host range for a network given the
    IP address and Subnet Mask.

.PARAMETER IPAddress
IP Address of any ip within the network
Note: Exclusive from @CIDRAddress

.PARAMETER SubnetMask
Subnet Mask of the network.
Note: Exclusive from @CIDRAddress

.PARAMETER CIDRAddress
CIDR Notation of IP/Subnet Mask (x.x.x.x/y)
Note: Exclusive from @IPAddress and @SubnetMask

.PARAMETER IncludeIPRange
Switch parameter that defines whether or not the script will return an array
of usable host IP addresses within the defined network.
Note: This parameter can cause delays in script completion for larger subnets.

.EXAMPLE
Get-IPv4NetworkInfo -IPAddress 192.168.1.23 -SubnetMask 255.255.255.0

Get network information with IP Address and Subnet Mask

.EXAMPLE
Get-IPv4NetworkInfo -CIDRAddress 192.168.1.23/24

Get network information with CIDR Notation

.NOTES
    File Name : Get-IPv4NetworkInfo.ps1
    Author : Ryan Drane
    Date : 5/10/16
    Requires : PowerShell v3
.LINK
www.ryandrane.com
#>


Function Get-IPv4NetworkInfo() {
    [CmdletBinding()]
    Param(
        [Parameter(ParameterSetName = "IPandMask", Mandatory = $true)]
        [ValidateScript( { $_ -match [ipaddress]$_ })]
        [System.String]$IPAddress,

        [Parameter(ParameterSetName = "IPandMask", Mandatory = $true)]
        [ValidateScript( { $_ -match [ipaddress]$_ })]
        [System.String]$SubnetMask,

        [Parameter(ParameterSetName = "CIDR", Mandatory = $true)]
        [ValidateScript( { $_ -match '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/([0-9]|[0-2][0-9]|3[0-2])$' })]
        [System.String]$CIDRAddress,

        [Switch]$IncludeIPRange
    )

    # If @CIDRAddress is set
    if ($CIDRAddress) {
        # Separate our IP address, from subnet bit count
        $IPAddress, [int32]$MaskBits = $CIDRAddress.Split('/')

        # Create array to hold our output mask
        $CIDRMask = @()

        # For loop to run through each octet,
        for ($j = 0; $j -lt 4; $j++) {
            # If there are 8 or more bits left
            if ($MaskBits -gt 7) {
                # Add 255 to mask array, and subtract 8 bits
                $CIDRMask += [byte]255
                $MaskBits -= 8
            } else {
                # bits are less than 8, calculate octet bits and
                # zero out our $MaskBits variable.
                $CIDRMask += [byte]255 -shl (8 - $MaskBits)
                $MaskBits = 0
            }
        }

        # Assign our newly created mask to the SubnetMask variable
        $SubnetMask = $CIDRMask -join '.'
    }

    # Get Arrays of [Byte] objects, one for each octet in our IP and Mask
    $IPAddressBytes = ([ipaddress]::Parse($IPAddress)).GetAddressBytes()
    $SubnetMaskBytes = ([ipaddress]::Parse($SubnetMask)).GetAddressBytes()

    # Declare empty arrays to hold output
    $NetworkAddressBytes = @()
    $BroadcastAddressBytes = @()
    $WildcardMaskBytes = @()

    # Determine Broadcast / Network Addresses, as well as Wildcard Mask
    for ($i = 0; $i -lt 4; $i++) {
        # Compare each Octet in the host IP to the Mask using bitwise
        # to obtain our Network Address
        $NetworkAddressBytes += $IPAddressBytes[$i] -band $SubnetMaskBytes[$i]

        # Compare each Octet in the subnet mask to 255 to get our wildcard mask
        $WildcardMaskBytes += $SubnetMaskBytes[$i] -bxor 255

        # Compare each octet in network address to wildcard mask to get broadcast.
        $BroadcastAddressBytes += $NetworkAddressBytes[$i] -bxor $WildcardMaskBytes[$i]
    }

    # Create variables to hold our NetworkAddress, WildcardMask, BroadcastAddress
    $NetworkAddress = $NetworkAddressBytes -join '.'
    $BroadcastAddress = $BroadcastAddressBytes -join '.'
    $WildcardMask = $WildcardMaskBytes -join '.'

    # Now that we have our Network, Widcard, and broadcast information,
    # We need to reverse the byte order in our Network and Broadcast addresses
    [array]::Reverse($NetworkAddressBytes)
    [array]::Reverse($BroadcastAddressBytes)

    # We also need to reverse the array of our IP address in order to get its
    # integer representation
    [array]::Reverse($IPAddressBytes)

    # Next we convert them both to 32-bit integers
    $NetworkAddressInt = [System.BitConverter]::ToUInt32($NetworkAddressBytes, 0)
    $BroadcastAddressInt = [System.BitConverter]::ToUInt32($BroadcastAddressBytes, 0)

    #Calculate the number of hosts in our subnet, subtracting one to account for network address.
    $NumberOfHosts = ($BroadcastAddressInt - $NetworkAddressInt) - 1

    # Declare an empty array to hold our range of usable IPs.
    $IPRange = @()

    # If -IncludeIPRange specified, calculate it
    if ($IncludeIPRange) {
        # Now run through our IP range and figure out the IP address for each.
        For ($j = 1; $j -le $NumberOfHosts; $j++) {
            # Increment Network Address by our counter variable, then convert back
            # lto an IP address and extract as string, add to IPRange output array.
            $IPRange += [ipaddress]([convert]::ToDouble($NetworkAddressInt + $j)) | Select-Object -ExpandProperty IPAddressToString
        }
    }

    # Create our output object
    $obj = New-Object -TypeName psobject

    # Add our properties to it
    Add-Member -InputObject $obj -MemberType NoteProperty -Name "IPAddress"           -Value $IPAddress
    Add-Member -InputObject $obj -MemberType NoteProperty -Name "SubnetMask"          -Value $SubnetMask
    Add-Member -InputObject $obj -MemberType NoteProperty -Name "NetworkAddress"      -Value $NetworkAddress
    Add-Member -InputObject $obj -MemberType NoteProperty -Name "BroadcastAddress"    -Value $BroadcastAddress
    Add-Member -InputObject $obj -MemberType NoteProperty -Name "WildcardMask"        -Value $WildcardMask
    Add-Member -InputObject $obj -MemberType NoteProperty -Name "NumberOfHostIPs"     -Value $NumberOfHosts
    Add-Member -InputObject $obj -MemberType NoteProperty -Name "IPRange"             -Value $IPRange

    # Return the object
    return $obj
}
#EndRegion Get-IPv4NetworkInfo

#Region Get-Management

<#
.SYNOPSIS
    Open Computer management

.DESCRIPTION
    Opens Computer management connected for a PC, local or remote. Default is local.

.PARAMETER ComputerName
    Specify a remote computer to run against.

.Example
    PS> Get-Management Test-999999-H

    Description
    -----------
    This will open computer management for this remote PC, if you are an admin on that PC.

.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Get-Management() {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0)]$ComputerName = $env:ComputerName
    )
    compmgmt.msc /computer:$ComputerName
}
#EndRegion Get-Management
#Region Get-PasswordExpired

<#
.SYNOPSIS
    Generates list of ActiveDirectory users who have expired passwords

.DESCRIPTION
    Returns a list of Active Directory Accounts with expired passwords
.EXAMPLE
    PS> Get-PasswordExpired

    Description
    -----------
    This will get all of the current accounts with expired passwords.
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Get-PasswordExpired() {
    [CmdletBinding()]
    param ()

    Search-ADAccount -PasswordExpired
}
#EndRegion Get-PasswordExpired
#Region Get-PCInfo

<#
.Synopsis
    Gather useful information from a remote PC.

.Description
    Returns useful informaion on the local endpoint or another.
.PARAMETER ComputerName
    Specify a remote computer to generate information for.
.EXAMPLE
    PS> Get-PCInfo -ComputerName Computer1

    Description
    -----------
    This will generate information from the remote computer using CIM Instances.
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Get-PCInfo() {
    [CmdletBinding()]
    Param (
        [string]$ComputerName = $env:ComputerName
    )

    try {
        $SystemEnclosure = Get-CimInstance win32_systemenclosure -computername $ComputerName -ErrorAction Stop
        $OS = Get-CimInstance Win32_OperatingSystem -Computername $ComputerName -ErrorAction Stop
    }
    catch {
        Write-Error "$($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)"
        break
    }

    #Creating Hash table from variables
    $PCInfo = @{
        Manufacturer   = $SystemEnclosure.Manufacturer
        PCName         = $OS.CSName
        OS             = $OS.Caption
        Architecture   = $OS.OSArchitecture
        AssetTag       = $systemenclosure.serialnumber;
        OSVersion      = $OS.Version
        InstallDate    = $OS.InstallDate
        LastBootUpTime = $OS.LastBootUpTime
    }

    #Writing to Host
    Write-Host " "
    Write-Host "Computer Info" -Foregroundcolor Cyan
    Write-Host "If not run on a Dell machine AssetTag is the Serial Number" -Foregroundcolor Yellow

    #Display Hash Table
    $PCInfo.getenumerator() | Sort-Object -property name | Format-Table -autosize

    #Writing to Host
    Write-Host "Computer Disk Info" -Foregroundcolor Cyan

    #Display Drives
    Get-CimInstance win32_logicaldisk -filter "drivetype=3" -computer $ComputerName |
    Format-Table -Property DeviceID, Volumename, `
    @{Name = "SizeGB"; Expression = { [math]::Round($_.Size / 1GB) } }, `
    @{Name = "FreeGB"; Expression = { [math]::Round($_.Freespace / 1GB, 2) } }, `
    @{Name = "PercentFree"; Expression = { [math]::Round(($_.Freespace / $_.size) * 100, 2) } }

    #Writing to Host
    Write-Host "Network Information" -Foregroundcolor Cyan

    Get-CimInstance win32_networkadapterconfiguration -computer $ComputerName | Where-Object { $null -ne $_.IPAddress } |
    Select-Object IPAddress, DefaultIPGateway, DNSServerSearchOrder, IPSubnet, MACAddress, Caption, DHCPEnabled, DHCPServer, DNSDomainSuffixSearchOrder |
    Format-List
}
#EndRegion Get-PCInfo
#Region Get-PCUpTime

<#
.SYNOPSIS
    Get the amount of time since the last boot-up sequence for a computer.
.DESCRIPTION
    This cmdlet uses Get-CimInstance to gather the .LastBootUpTime for the local or remote computer.
    PowerShell 7 comes with a `Get-Uptime` cmdlet, so if called from PowerShell, it will simply call or invoke that cmdlet. otherwise when called from Windows Powershell, it will invoke a CimInstance.
.PARAMETER ComputerName
    Specify the remote computer to query using CIM.
.EXAMPLE
    PS> Get-PCUpTime

    Description
    -----------
    This will return the current UpTime value for the local computer.
.EXAMPLE
    PS> Get-PCUpTime Remote-Server

    Description
    -----------
    This will query `Remote-Server` for it's uptime data.
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Get-PCUpTime() {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)]
        $ComputerName
    )

    begin {
        $Version = $PSVersionTable.PSEdition
    }
    process {
        try {
            switch($Version) {
                'Desktop' {
                    if ($null -ne $ComputerName) {
                        $SplatMe = @{
                            ClassName = 'Win32_OperatingSystem'
                            ComputerName = $ComputerName}
                    } else {
                        $SplatMe = @{
                            ClassName = 'Win32_OperatingSystem'
                        }
                    }
                
                    $Now = Get-Date
                    $LastBootUpTime = (Get-CimInstance @SplatMe -ErrorAction Stop).LastBootUpTime
                    $Return = $Now - $LastBootUpTime
                    return $Return
                }
    
                'Core' {
                    if ($null -ne $ComputerName) {
                        $PCFunctionDefinition = Get-Definition Get-PCUpTime
                        $Script = @"
                        $PCFunctionDefinition
                        Get-PCUpTime
"@

                        $ScriptBlock = {
                            param ($Script)
                            . ([ScriptBlock]::Create($Script))
                        }
                        $params = @{
                            ComputerName = $ComputerName
                            ScriptBlock = $ScriptBlock
                            ArgumentList = $Script
                        }
                        Invoke-Command @params 
                    } else {
                        Get-Uptime
                    }
                }
    
                DEFAULT {}
            }
        } catch {
            Write-Error "$($_.Exception.Message)"
        }
    }
}
#EndRegion Get-PCUpTime
#Region Get-Printers

<#
.SYNOPSIS
    Get printers for local or remote PC

.Description
    This function will attempt to gather printer information for a local or remote PC.

.PARAMETER ComputerName
    Specify the remote computer to query.

.EXAMPLE
    PS> Get-Printers
    
    Description
    -----------
    This will generate local printer information.
.EXAMPLE
    PS> Get-Printers -ComputerName Some-Remote-Computer1
    
    Description
    -----------
    This will generate printer information for the remote computer `Some-Remote-Computer1' via a Cim Instance.
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Get-Printers() {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $false)]
        [String] $ComputerName
    )

    $Params = @{
        ClassName = 'CIM_Printer'
    }
    if ($ComputerName) { $Params += @{ComputerName = $ComputerName } }
    Get-CimInstance @Params | Select-Object Name, Drivername, Portname, Status, SystemName, local, shared, CapabilityDescriptions
}
#EndRegion Get-Printers
#Region Get-PrintManagement

<#
.SYNOPSIS
    Quickly launch Print Management MSC Snap-in
.DESCRIPTION
    Opens Print Management for the local PC and one remote PC using -ComputerName
.PARAMETER ComputerName
    Specify the remote computer to open Print Management against.
.EXAMPLE
    PS> Get-PrintManagement -ComputerName Computer1

    Description
    -----------
    This will open PrintManagement on the local machine and connect to the remote `Computer1`
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Get-PrintManagement() {
    [CmdletBinding()]
    param (
        [string[]]$ComputerName = $env:COMPUTERNAME
    )
    printmanagement.msc /computer:$ComputerName
}
#EndRegion Get-PrintManagement
#Region Get-PublicIP

<#
.SYNOPSIS
    Generates your current Public IP Information

.DESCRIPTION
    Returns WhoIS public IP info for your location or any specified public IP. By Default, returns your current public IP info.

.PARAMETER IP
    Specify the IP Address to look up information for. This uses your current public IP by default.

.EXAMPLE
    PS> Get-PublicIP

    Description
    -----------
    Returns local Public IP Info

    Get-PublicIP

.Example
    PS> Get-PublicIP -IP 8.8.8.8
    
    Description
    -----------
    Returns Public IP Info for 8.8.8.8

    Get-PublicIP -IP 8.8.8.8

.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Get-PublicIP() {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0)]$IP
    )

    try {
        if ($IP) {
            $ipinfo = Invoke-RestMethod http://ipinfo.io/$IP/json
        }
        else {
            $ipinfo = Invoke-RestMethod http://ipinfo.io/json
        }
        [PSCustomObject]@{
            IP           = $ipinfo.ip
            City         = $ipinfo.city
            Region       = $ipinfo.region
            Country      = $ipinfo.country
            Coord        = $ipinfo.loc
            Organization = $ipinfo.org
            Postal       = $ipinfo.Postal
            TimeZone     = $ipinfo.timezone
        }
    }
    catch {
        Write-Error "$($_.Exception.Message)"
    }
}
#EndRegion Get-PublicIP
#Region Get-RebootLogs

<#
.SYNOPSIS
    Get the System Event logs for reboot ID 1074.
.DESCRIPTION
    This will pull system event logs for the local or remote computer.
.PARAMETER ComputerName
    Specify a remote computer to pull logs from.
.EXAMPLE
    PS> Get-RebootLogs

    Description
    -----------
    This will generate a list of all System Reboot log events with ID 1074 on the local system.
.EXAMPLE
    PS> Get-RebootLogs -ComputerName Some-Remote-Computer | Select -First 5

    Description
    -----------
    This will get the System Reboot logs from `Some-Remote-Computer` and only show the first 5 results.
.NOTES
    Author: Matthew J. DeGarmo
    Site: https://matthewjdegarmo.com

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

function Get-RebootLogs() {
    [CmdletBinding()]
    param (
        [Parameter()]
        [string] $ComputerName = $env:COMPUTERNAME
    )
    begin {}
    
    process {
        try {
            $params = @{
                LogName      = 'System'
                ComputerName = $ComputerName.ToUpper()
                ErrorAction  = 'SilentlyContinue'
                Verbose      = $Verbose
            }
            Write-Verbose "Gathering $($params.LogName) logs from $($params.ComputerName) with ID 1074."
            Get-WinEvent @params | Where-Object { $_.ID -eq '1074' }
        }
        catch {
            Write-Error "$($_.Exception.Message)"
        }
    }

    end {}
}
#EndRegion Get-RebootLogs
#Region Get-WindowsBuild

<#
.SYNOPSIS
    Gets Windows Build information.
.DESCRIPTION
    This will query the local PC OR an array of remote PC's
.PARAMETER ComputerName
    Specify the remote computer to query.
.INPUTS
    System.String[]
        You must specify the value for Credential. You cannot pipe a value to this function.
.OUTPUTS
    None
        There are no outputs.
.EXAMPLE
    PS> ConnectTeams
    
    Description
    -----------
    This will attempt a connection to portal.office.com. This will prompt you for account information like what password to use.
.EXAMPLE
    PS> ConnectTeams -Credential 'SomeAccount@Email.com'
    Description
    -----------
    This will attempt a connection to portal.office.com. This will prompt you for account information like what password to use.
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

function Get-WindowsBuild() {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false,
            ValueFromPipelineByPropertyName = $true,
            ValueFromPipeline = $true
        )]
        [string[]]$ComputerName = $env:COMPUTERNAME
    )

    begin {
        $Table = New-Object System.Data.DataTable
        $Table.Columns.AddRange(@("ComputerName", "Windows Edition", "Version", "OSBuild"))
    }

    process {
        Foreach ($Computer in $ComputerName) {
            $Code = {
                $ProductName = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name ProductName).ProductName
                try {
                    $Version = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name ReleaseID -ErrorAction Stop).ReleaseID
                }
                catch {
                    $Version = "N/A"
                }
                $CurrentBuild = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name CurrentBuild).CurrentBuild
                $UBR = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Name UBR).UBR
                $OSVersion = $CurrentBuild + "." + $UBR
    
                $TempTable = New-Object System.Data.DataTable
                $TempTable.Columns.AddRange(@("ComputerName", "Windows Edition", "Version", "OSBuild"))
                [void]$TempTable.Rows.Add($env:COMPUTERNAME, $ProductName, $Version, $OSVersion)
            
                return $TempTable
            }
    
            if ($Computer -eq $env:COMPUTERNAME) {
                $Result = Invoke-Command -ScriptBlock $Code
                [void]$Table.Rows.Add($Result.Computername, $Result.'Windows Edition', $Result.Version, $Result.'OSBuild')
            }
            else {
                if (Test-Connection $Computer -count 1 -ErrorAction SilentlyContinue) {
                    try {
                        $Result = Invoke-Command -ComputerName $Computer -ScriptBlock $Code -ErrorAction Stop
                        [void]$Table.Rows.Add($Result.Computername, $Result.'Windows Edition', $Result.Version, $Result.'OSBuild')
                    }
                    catch {
                        $_
                    }
                }
                else {
                    [void]$Table.Rows.Add($Computer, "OFFLINE", "OFFLINE", "OFFLINE")
                }
            }
        }
    }
    end {
        Return $Table
    }
}
#EndRegion Get-WindowsBuild
#Region grep
$DetectedOS = switch($true) {
    $IsWindows {'Windows'}
    $IsLinux   {'Linux'}
    $IsMacOS   {'MacOS'}
    DEFAULT    {'Windows'}
}

If ($DetectedOS -eq 'Windows') {

    <#
    .SYNOPSIS
        Basic version of the linux command `grep` on Windows.
    .DESCRIPTION
        This is a windows version of the linux `grep` command. I still need to figure out how to NOT import this command when on a linux system.
        This is basically a shorter `Select-String`, and does not support other grep flags as on a Linux system.
    .PARAMETER Regex
        Specify the regex pattern to filter for.
    .EXAMPLE
        Get-Process | grep powershell
        
        Description
        -----------
        This will filter the `Get-Process` output with the regex 'powershell'.
    .NOTES
        Author: Matthew J. DeGarmo
        Handle: @matthewjdegarmo

        You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
            or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
            on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
    #>

    Function grep() {
        # [CmdletBinding()] # This is to pass the advanced function pester tests.
        param(
            $regex
        )

        process {
            $_ | Where-Object { $_ -match $regex }
        }
    }
}
#EndRegion grep
#Region Invoke-Speech

<#
.SYNOPSIS
    Translate a string into an audible message.
.DESCRIPTION
    This function calls the SAPI.SPVoice class to invoke audio given a string. This is useful when running long processes, you can audibly be alerted that a task is finished.
.PARAMETER Message
    Specify the message to have voiced.
.EXAMPLE
    PS> Get-SomeDataThatTakesAnHour;Invoke-Speech -Message "Your data is ready, sir."
    
    Description
    -----------
    This will generated audio for the string 'Your data is ready, sir." depending on your volume level.
.NOTES
    Author: Matthew J. DeGarmo
    Site: https://matthewjdegarmo.github.io

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Invoke-Speech() {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true)]
        [string]$Message
    ) 

    begin {
        #Set Variables for the Invoke-Speech Function
        $voice = New-Object -ComObject SAPI.SPVoice
        $voice.Rate = -2
    }

    process {
        $voice.Speak($Message) | out-null;    
    }
}
#EndRegion Invoke-Speech
#Region LL

<#
.SYNOPSIS
    This is a colorized version of Get-ChildItem (dir, ls).
.DESCRIPTION
    This function will change the color of object names using Get-ChildItem based on the object type or extension.
    You can define more extensions and their associated colors if you wish.
.PARAMETER Directory
    Specify the directory to get items for. Default is '.' or current directory.
.PARAMETER All
    Essentially this is a `-Force` switch on Get-ChildItem. By default this is set to $false.
.EXAMPLE
    PS> LL C:\Temp
    
    Description
    -----------
    Display a colorized output for `Get-ChildItem` at C:\Temp.
.EXAMPLE
    PS> ll
    
    Description
    -----------
    Display a colorized output for `Get-ChildItem` at the current working directory.
.NOTES
    Author: Matthew J. DeGarmo
    Site: https://matthewjdegarmo.github.io

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function LL() {
    [CmdletBinding()]
    param (
        [String] $Directory = ".", 
        $All = $false
    ) 
    $originalForeground = $host.ui.rawui.foregroundColor 
    if ( $All ) { 
        $toList = Get-ChildItem -force $Directory 
    }
    else { 
        $toList = Get-ChildItem $Directory 
    }
    foreach ($Item in $toList) { 
        Switch ($Item.Extension) {
            ".Exe" { $host.ui.rawui.foregroundColor = "Yellow" } 
            ".cmd" { $host.ui.rawui.foregroundColor = "Red" } 
            ".lnk" { $host.ui.rawui.foregroundColor = "Red" }
            ".msh" { $host.ui.rawui.foregroundColor = "Red" } 
            ".vbs" { $host.ui.rawui.foregroundColor = "Red" }
            ".ps1" { $host.ui.rawui.foregroundColor = "Cyan" }
            ".psm1" { $host.ui.rawui.foregroundColor = "Cyan" }
            ".psd1" { $host.ui.rawui.foregroundColor = "Cyan" }
            ".ps1xml" { $host.ui.rawui.foregroundColor = "Cyan" }
            ".txt" { $host.ui.rawui.foregroundColor = "DarkCyan" }
            ".xml" { $host.ui.rawui.foregroundColor = "Magenta" }
            ".cvs" { $host.ui.rawui.foregroundColor = "Magenta" }
            ".doc" { $host.ui.rawui.foregroundColor = "Magenta" }
            ".csv" { $host.ui.rawui.foregroundColor = "Magenta" }
            ".bat" { $host.ui.rawui.foregroundColor = "Yellow" }
            ".docx" { $host.ui.rawui.foregroundColor = "Magenta" }
            ".pdf" { $host.ui.rawui.foregroundColor = "Magenta" }
            ".xls" { $host.ui.rawui.foregroundColor = "Magenta" }
            ".xlsx" { $host.ui.rawui.foregroundColor = "Magenta" }
            ".log" { $host.ui.rawui.foregroundColor = "DarkCyan" }
            Default { $host.ui.rawui.foregroundColor = $originalForeground } 
        } 
        if ($item.Mode.StartsWith("d")) { $host.ui.rawui.foregroundColor = "Green" }
        $item 
    }  
    $host.ui.rawui.foregroundColor = $originalForeground 
}
#EndRegion LL
#Region LLM

<#
.SYNOPSIS
    This is a quick way to lock your workstation.
.DESCRIPTION
    LLM is to stand for 'Lock Local Machine'. This will lock the current session on a windows workstation. Will need to add functionality to lock a linux or mac.
.EXAMPLE
    PS> llm
    
    Description
    -----------
    This will quickly lock the current workstation.
.NOTES
    Author: Matthew J. DeGarmo
    Site: https://matthewjdegarmo.github.io

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function llm() {
    [CmdletBinding()]
    param ()
    $signature = @"
    [DllImport("user32.dll", SetLastError = true)]
    public static extern bool LockWorkStation();
"@
  
    Write-Host "Locking local machine: $Env:COMPUTERNAME" -ForegroundColor Yellow
    $LockWorkStation = Add-Type -memberDefinition $signature -name "Win32LockWorkStation" -namespace Win32Functions -passthru  
    $LockWorkStation::LockWorkStation() | Out-Null
}
#EndRegion LLM
#Region Locate

<#
.SYNOPSIS
Quickly search a location for a file, folder, hidden file, etc...
This should return the same object that Get-ChildItem returns.

.DESCRIPTION
This function takes a -Filter and applies dual-wildcards for maximum search results.
By default this will search recursively in the local directory, but you can specify
any custom location.

.PARAMETER Directory [<SwitchParameter>]
Gets directories (folders).

To get only directories, use the Directory parameter and omit the File parameter. To exclude directories, use
the File parameter and omit the Directory parameter.

To get directories, use the Directory parameter.

.PARAMETER File [<SwitchParameter>]
Gets files.

To get only files, use the File parameter and omit the Directory parameter. To exclude files, use the
Directory parameter and omit the File parameter.

To get files, use the File parameter.

.PARAMETER Hidden [<SwitchParameter>]
Gets only hidden files and directories (folders). By default, Get-ChildItem gets only non-hidden items, but
you can use the Force parameter to include hidden items in the results.

To get only hidden items, use the Hidden parameter. To exclude hidden items, omit the Hidden parameter.

.PARAMETER ReadOnly [<SwitchParameter>]
Gets only read-only files and directories (folders).

To get only read-only items, use the ReadOnly parameter, its "ar" alias, or the ReadOnly value of the
Attributes parameter. To exclude read-only items, use the Attributes parameter.

.PARAMETER System [<SwitchParameter>]
Gets only system files and directories (folders).

To get only system files and folders, use the System parameter.

.PARAMETER Force [<SwitchParameter>]
Gets hidden files and folders. By default, hidden files and folder are excluded. You can also get hidden files
and folders by using the Hidden parameter or the Hidden value of the Attributes parameter.

.PARAMETER Exclude <String[]>
Specifies, as a string array, an item or items that this cmdlet excludes in the operation. The value of this
parameter qualifies the Path parameter. Enter a path element or pattern, such as *.txt. Wildcards are
permitted.

.PARAMETER Include <String[]>
Specifies, as a string array, an item or items that this cmdlet includes in the operation. The value of this
parameter qualifies the Path parameter. Enter a path element or pattern, such as *.txt. Wildcards are
permitted.

The Include parameter is effective only when the command includes the Recurse parameter or the path leads to
the contents of a directory, such as C:\Windows\*, where the wildcard character specifies the contents of the
C:\Windows directory.

.PARAMETER Filter <String>
Specifies a filter in the provider's format or language. The value of this parameter qualifies the Path
parameter. The syntax of the filter, including the use of wildcards, depends on the provider. Filters are more
efficient than other parameters, because the provider applies them when retrieving the objects, rather than
having Windows PowerShell filter the objects after they are retrieved.

.PARAMETER Path <String[]>
Specifies a path to one or more locations. Wildcards are permitted. The default location is the current
directory (.).

.PARAMETER Recurse [<SwitchParameter>]
Indicates that this cmdlet gets the items in the specified locations and in all child items of the locations.

In Windows PowerShell 2.0 and earlier versions of Windows PowerShell, the Recurse parameter works only when
the value of the Path parameter is a container that has child items, such as C:\Windows or C:\Windows\ , and
not when it is an item does not have child items, such as C:\Windows\ .exe.

By Default, this is set to True. Use -Recurse:$false to turn off recursive results.

.EXAMPLE
    PS> Locate AdminToolkit.psd1 -Recurse
    
    Description
    -----------
    This will search from the current working directory for files or folders mathing the filter 'AdminToolkit.psd1'
.EXAMPLE
    PS> locate foo.txt C:\temp
    
    Description
    -----------
    This will search for the file foo.txt in the directory C:\temp.
.EXAMPLE
    PS> locate test -Recurse -Exclude *.tests.*

    Directory: C:\Temp\HelpDesk\Functions\Public

    Mode LastWriteTime Length Name
    ---- ------------- ------ ----
    -a--- 8/5/2020 11:51 AM 6985 Test-Administrator.ps1

    Directory: C:\Temp\HelpDesk

    Mode LastWriteTime Length Name
    ---- ------------- ------ ----
    d---- 8/5/2020 2:07 PM Tests
    
    Description
    -----------
    This will search recursively using the filter 'test' and exclude files/folders that match '*.tests.*'
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit

    Change Log:
    Version: 1.0 - Function Creation.
#>
 
Function Locate() {
    [CmdletBinding()]
    param (
        [Parameter(Position = 0)]
        [string]$Filter,
        [Parameter(Position = 1)]
        [string]$Path = (Get-Location),
        [string[]]$Include,
        [string[]]$Exclude,
        [switch]$Force,
        [switch]$Recurse,
        [switch]$Directory,
        [switch]$File,
        [switch]$System,
        [switch]$Hidden,
        [switch]$ReadOnly
    )
    if (-Not($PSBoundParameters.Filter)) {
        $PSBoundParameters.Filter = '*'
    } else {
        $PSBoundParameters.Filter = "*$Filter*"
    }

    Get-ChildItem @PSBoundParameters -ErrorAction SilentlyContinue
}
#EndRegion Locate
#Region Merge-CIDRIpRanges

<#
.SYNOPSIS
    Reduces a list of CIDR IP Ranges to a single list of ranges that do not overlap.
.DESCRIPTION
    Given a list of CIDR IP Ranges, this will remove CIDR IP Ranges that overlap / are nested within other ranges in the list.
    When an overlap is found, the CIDR Range that survives the filter is the larger range.

    EXAMPLE: If the provided list contains 3.134.215.0/24 and 3.132.0.0/14 ranges, the range that will be kept is 3.132.0.0/14 since it is the larger range and contains the other.
.PARAMETER CIDRAddresses
    Provide an array of CIDR IP Ranges (IP.Add.Re.SS/## format).
.EXAMPLE
    Merge-CIDRIpRanges -CIDRAddresses $TotalCIDRIps
.NOTES
    Author: Matthew DeGarmo
    GitHub: https://github.com/matthewjdegarmo
#>

Function Merge-CIDRIpRanges() {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)]
        [System.String[]] $CIDRAddresses
    )

    Begin {
        $CIDRAddresses = $CIDRAddresses | Select-Object -Unique
    }

    Process {
        [System.Collections.ArrayList]$RejectedIPs = @()
        [System.Collections.ArrayList]$SurvivingIPs = @()

        $Index = 0
        $CIDRAddresses | ForEach-Object {
            $Index++
            # If ($Index -eq ($CIDRAddresses.Count - 1)) { continue }
            $RootIP = $_
            $IpInfo = Get-IPv4NetworkInfo -CIDRAddress $_

            $ProgressPercent = ($Index / ($CIDRAddresses.count) * 100)
            $Percent = "$([math]::round($ProgressPercent,2))%"

            $Progress = @{
                Activity = "Filtering Overlapping CIDR IP Ranges"
                Status = "Examining IP: [$RootIP] - $Percent"
                CurrentOperation = "IP: $Index of $($CIDRAddresses.Count)"
                Id = 1
                PercentComplete = $ProgressPercent
            }
            Write-Progress @Progress
            Write-Debug "Beginning compare of: [$RootIP]"
            [System.Collections.ArrayList]$Reject = @()
            [System.Collections.ArrayList]$Survivor = @()
            $NestedIndex = 0
            $CIDRAddresses[$Index..$($CIDRAddresses.Count - 1)] | ForEach-Object {
                $NestedIndex++
                $CompareIP = $_
                Write-Debug "Comparing [$RootIP] to [$CompareIP]"
                $CompareIPInfo = Get-IPv4NetworkInfo -CIDRAddress $_
                $NestedProgressPercent = ($NestedIndex / $($CIDRAddresses.Count - 1) * 100)
                $Progress = @{
                    Activity = "Comparing [$RootIP] to full array of CIDR Ranges"
                    Status = "Comparing: [$RootIP] to [$CompareIP]"
                    CurrentOperation = "IP: $NestedIndex of $($CIDRAddresses.Count - $Index)"
                    ParentId = 1
                    PercentComplete = $NestedProgressPercent
                }
                Write-Progress @Progress

                $RangeCompareRootToNested = @{
                    IPAddress    = $CompareIPInfo.IpAddress
                    StartAddress = $IPInfo.NetworkAddress
                    EndAddress   = $IPInfo.BroadcastAddress
                }

                If (Test-IsIpAddressInRange @RangeCompareRootToNested) {
                    Write-Verbose "$($CompareIP) is inside range $($RootIP)"
                    $null = $Survivor.Add($RootIP)
                    $null = $Reject.Add($CompareIP)

                }

                $RangeCompareNestedToRoot = @{
                    IPAddress    = $IPInfo.IpAddress
                    StartAddress = $CompareIPInfo.NetworkAddress
                    EndAddress   = $CompareIPInfo.BroadcastAddress
                }

                If (Test-IsIpAddressInRange @RangeCompareNestedToRoot) {
                    Write-Verbose "$($RootIP) is inside range $($CompareIP)"
                    $null = $Survivor.Add($CompareIP)
                    $null = $Reject.Add($RootIP)
                }
            }

            If ($Survivor -or $Reject) {
                Foreach ($Ip in @($Survivor | Select-Object -Unique)) {
                    Write-Verbose "Adding Survivor: $Ip"
                    $null = $SurvivingIPs.Add($Ip)
                }
                Foreach ($Ip in @($Reject | Select-Object -Unique)) {
                    Write-Verbose "Adding Reject: $Ip"
                    $null = $RejectedIPs.Add($Ip)
                }
            } Else {
                Write-Verbose "No Conflict: $($RootIP)"
                $null = $SurvivingIPs.Add($RootIP)
            }
        }
        Compare-Object $SurvivingIPs $RejectedIPs -PassThru | Where-Object { $_.SideIndicator -eq '<=' }
    }

    End {}
}
#EndRegion Merge-CIDRRanges

#Region New-Folder

<#
.SYNOPSIS
    Easily create a new folder in the current working directory.
.DESCRIPTION
    This will create a new directory in the current working directory.
.PARAMETER Name
    Spedify the name for the new folder.
.EXAMPLE
    PS> New-Folder Foobar
    
    Description
    -----------
    This will create a new folder 'Foobar' if there currently is not a folder of the same name.
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function New-Folder() {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string] $Name
    )

    process {
        New-Item $Name -ItemType Directory
    }
}
#EndRegion New-Folder

#Region PSRemote

<#
.SYNOPSIS
    Starts an Enter-PSSession with the specified Server.
.DESCRIPTION
    PSRemote will attempt to enter a PSSession with a specified host with a specified account. If no Credential is specified, it will use the currently signed in account to connect.
.PARAMETER ComputerName
    This parameter specifies what host to attempt an Enter-PSSession with.
.PARAMETER Credential
    This parameter is used to change the current account for the PSSession.
.PARAMETER IncludeModule
    This parameter specifies any local installed / imported modules to be defined in the remote scope. Essentially bringing any local modules with you without installing them on the remote machine.
.PARAMETER IncludeProfile
    Specify a local profile to load in the remote session.
.INPUTS
    System.String[]
        You must specify the value for Computername. You cannot pipe a value to this function.
.OUTPUTS
    None
        There are no outputs except for Write-Host messages.
.EXAMPLE
    PS> PSRemote -ComputerName Computer1 -Credential matthewjd
    
    Description
    -----------
    This will attempt to start a PSSession with Computer1 as matthewjd. This will prompt for a password for matthewjd.
.EXAMPLE
    PS> PSRemote Computer1 -IncludeModule AdminToolkit
    Description
    -----------
    This will use the currently signed in account to connect to attempt a connection with Computer1 and import the module Helpdesk.
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
    
    Change Log:
    Version: 2.0 - Added -IncludeModule parameter. This will allow you to import a local module into your remote session.
    Version: 1.0 - Function Creation.
#>

Function PSRemote() {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$ComputerName,
        [Parameter(Mandatory = $false, Position = 1)]
        [PSCredential]$Credential,
        [Parameter(Mandatory = $false)]
        [string[]]$IncludeModule,
        [Parameter(Mandatory = $false)]
        [string]$IncludeProfile
    )

    begin {
        function Import-ModuleRemotely {
            Param (
                [string] $moduleName,
                [System.Management.Automation.Runspaces.PSSession] $session
            )
        
            Import-Module $moduleName -ErrorAction SilentlyContinue
        
            $Script = @"
            if (get-module $moduleName)
            {
                remove-module $moduleName;
            }
        
            New-Module -Name $moduleName { $($(Get-Module $moduleName).Definition) } | Import-Module
"@

        
            Invoke-Command -Session $Session -ScriptBlock {
                Param($Script)
                . ([ScriptBlock]::Create($Script))
                #Get-Module
            } -ArgumentList $Script
        }
    }

    process {
        try {
            if ($Credential) {
                $Session = New-PSSession -ComputerName $ComputerName -Credential $credential -ErrorAction Stop
            }
            else {
                $Session = New-PSSession -ComputerName $ComputerName -ErrorAction Stop
            }
            if ($PSBoundParameters.ContainsKey('IncludeProfile')) {
                Invoke-Command -FilePath $IncludeProfile -Session $Session -ErrorAction Stop
            }
            if ($IncludeModule) {
                foreach ($Module in $IncludeModule) {
                    Import-ModuleRemotely -moduleName $Module -session $Session
                }
            }
            Enter-PSSession -Session $Session -ErrorAction Stop
        }
        catch {
            Write-Error "$($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)"
        }
    }
}
#EndRegion PSRemote
#Region Push-LocalScheduledTask

<#
.SYNOPSIS
    Deploy a local scheduled task to a remote machine

.DESCRIPTION
    This function exports the XML for a local scheduled task and creates that task on a remote machine.

.PARAMETER ComputerName
    This parameter specifies the remote host(s) to create the task(s) on. This parameter supports tab-completion based on the current domain.
    I can type '-ComputerName cgo-2' and this will tab-complete computer objects in the current domain that match the string 'cgo-2'

.PARAMETER TaskName
    This parameter specifies the local task name to export and create on a remote machine. I have not tested how to export nested tasks (See Register-ScheduledTask '-TaskPath' parameter)
    You can specify multiple task names, separated by comma please.

.PARAMETER Credential
    Specifies a user account that has permission to perform this action. The default is the current user.
    This credential is used for a task against the local pc, and the remote PC. because of this, the account used must have rights to do the required tasks on all computers involved.

    Type a user name, such as User01 or Domain01\User01 , or enter a PSCredential object generated by
    the `Get-Credential` cmdlet. If you type a user name, you're prompted to enter the password.

    Credentials are stored in a PSCredential
    (/dotnet/api/system.management.automation.pscredential)object and the password is stored as a
    SecureString (/dotnet/api/system.security.securestring).

    > [!NOTE] > For more information about SecureString data protection, see > How secure is
    SecureString? (/dotnet/api/system.security.securestring#how-secure-is-securestring).

.PARAMETER Force
    Instructs the cmdlet to perform the operation without prompting for confirmation.
    Additionally this will overwrite any remote tasks with the same name.
    If you attempt to deploy a local task on a remote machine without using -Force, the export will fail.

.PARAMETER PassThru
    I haven't gotten this switch to be accurate. Currently this function will ALWAYS spit out Scheduled Task objects that it creates whether -PassThru is present or not.
    The goal is to have the function not return objects if -PassThru is not present... like every other advanced function.

.EXAMPLE
    PS>Deploy-LocalScheduledTask -ComputerName Computer1,Computer2 -TaskName "Task1","Task2"

    Description
    -----------
    This will export both Task1 and Task2 scheduled tasks to both Computer1 and Computer2.

.EXAMPLE
    PS>Deploy-LocalScheduledTask -ComputerName Computer1 -TaskName "Task1" -Credential (Get-Credential) -Force

    Description
    -----------
    This will export the task Task1 to Computer1 using the provided credentials. You can also save the results of Get-Credential to a variable and use the '-Credential $cred' method.
    This will also overwrite a possible existing task named Task1 since -Force is used.

.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Push-LocalScheduledTask() {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]] $ComputerName,

        [Parameter(Mandatory)]
        [string[]] $TaskName,

        [PSCredential] $Credential,

        [switch] $Force,

        #? Should this switch just be tossed?
        #TODO This switch doesn't do anything. Need to figure out how to supress Invoke-Command output for Register-ScheduledTask command.
        [switch] $PassThru
    )

    begin {
        $ComputerName = foreach ($Computer in $ComputerName) {
            if (Test-Connection -ComputerName $Computer -Count 1 -ErrorAction SilentlyContinue) {
                $Computer
            }
            else {
                Write-Warning "Test-Connection to '$ComputerName' failed."
            }
        }
    }

    process {
        try {
            foreach ($Computer in $ComputerName) {
                $InvokeParams = @{
                    ComputerName = $Computer
                }
                if ($PSBoundParameters.ContainsKey('Credential')) { $InvokeParams += @{Credential = $Credential } }
                
                foreach ($Task in $TaskName) {
                    if ($PSBoundParameters.ContainsKey('Credential')) {
                        $TaskXML = Invoke-Command -ComputerName $env:COMPUTERNAME -Credential $Credential -Command { Export-ScheduledTask $using:Task | Out-String }
                    }
                    else {
                        $TaskXML = Export-ScheduledTask $Task | Out-String
                    }

                    $TaskParams = @{
                        Xml      = $TaskXML
                        TaskName = $Task
                    }
                    if ($PSBoundParameters.ContainsKey('Force')) { $TaskParams += @{Force = $true } }
                    if (-Not($PSBoundParameters.ContainsKey('PassThru'))) { $TaskParams += @{InformationAction = 'Ignore' } }

                    Invoke-Command @InvokeParams -ScriptBlock {
                        Register-ScheduledTask @using:TaskParams
                    }
                }
            }
        }
        catch {
            Write-Error "$($_.Exception.Message) - Line Number: $($_.InvocationInfo.ScriptLineNumber)"
        }
    }

    end {}
}
#EndRegion Push-LocalScheduledTask
#Region Remove-AllTempFiles

<#
.Synopsis
    Generic Cleanup of temp files on a computer.

.Description
    This Command removes log files, temp files, and empties the recycle bin. Access denied errors do not indicate a failure of the script. Run for the local or a remote PC.

.PARAMETER ComputerName
    Specify a remote computer to run against.

.EXAMPLE
    PS> Remove-All

    Description
    -----------
    Free up space on the local computer

.EXAMPLE
    PS> Remove-All -Computer Test-PC-01

    Description
    -----------
    Free up space on a remote PC. May be more effective if run locally depending on in place security.

.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Remove-AllTempFiles() {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter()]$ComputerName
    )

    #Statement of Free Space before Cleaning
    Write-Host " "
    Write-Host "Free Space Before Cleaning" -ForegroundColor Yellow
    Get-CimInstance win32_logicaldisk -filter "drivetype=3" -computer $ComputerName |
    Format-Table -Property DeviceID, Volumename, `
    @{Name = "SizeGB"; Expression = { [math]::Round($_.Size / 1GB) } }, `
    @{Name = "FreeGB"; Expression = { [math]::Round($_.Freespace / 1GB, 2) } }, `
    @{Name = "PercentFree"; Expression = { [math]::Round(($_.Freespace / $_.size) * 100, 2) } }

    #Statement that the function is freeing up space
    Write-Host "Freeing up space. Enjoy your Coffee!" -BackgroundColor Black -ForegroundColor Green

    #Free up space on the local or remote computer
    if ($null -ne $ComputerName) {
        $ErrorActionPreference = 'SilentlyContinue'

        Get-Service -ComputerName $ComputerName TrustedInstaller | Stop-Service -Force
        Get-ChildItem -path "\\$ComputerName\C$\windows\logs" -Include '*.log' -Recurse -force | Remove-Item -force -Recurse
        Get-ChildItem -path "\\$ComputerName\C$\windows\logs" -Include '*.cab' -Recurse -force | Remove-Item -force -Recurse
        Get-ChildItem -path "\\$ComputerName\C$\ProgramData\Microsoft\Windows\WER" -Include '*.*' -Recurse -force | Remove-Item -force -Recurse
        Get-ChildItem -path "\\$ComputerName\C$\`$recycle.bin" -Include '*' -Recurse -force | Remove-Item -force -Recurse
        Get-ChildItem -path "\\$ComputerName\C$\Users\*\AppData\Local\Google\Chrome\User Data\Default\Cache\" -include '*.*' -Recurse -force | Remove-Item -force -Recurse
        Get-ChildItem -path "\\$ComputerName\C$\Users\*\AppData\Local\Microsoft\Terminal Server Client\" -include '*.*' -Recurse -force | Remove-Item -force -Recurse
        $tempfolders = @("\\$ComputerName\C$\Windows\Temp\*", "\\$ComputerName\C$\Windows\Prefetch\*", "\\$ComputerName\C$\Documents and Settings\*\Local Settings\temp\*", "\\$ComputerName\C$\Users\*\Appdata\Local\Temp\*")
        Remove-Item $tempfolders -force -recurse -errorAction SilentlyContinue
        $tempinternetfolders = @("\\$ComputerName\C$\Users\*\Appdata\Local\Microsoft\Windows\INetCache\*", "\\$ComputerName\C$\Users\*\Appdata\Local\Microsoft\Windows\Cookies\*", "\\$ComputerName\C$\Users\*\AppData\Local\Microsoft\Windows\Temporary Internet Files\*.*")
        Remove-Item $tempinternetfolders -force -recurse -errorAction SilentlyContinue
        Get-Service -ComputerName $ComputerName -Name TrustedInstaller | Start-Service

        $ErrorActionPreference = 'Continue'

        Write-Host " "
        Write-Host "Free Space After Cleaning" -ForegroundColor Yellow
        Get-CimInstance win32_logicaldisk -filter "drivetype=3" -computer $ComputerName |
        Format-Table -Property DeviceID, Volumename, `
        @{Name = "SizeGB"; Expression = { [math]::Round($_.Size / 1GB) } }, `
        @{Name = "FreeGB"; Expression = { [math]::Round($_.Freespace / 1GB, 2) } }, `
        @{Name = "PercentFree"; Expression = { [math]::Round(($_.Freespace / $_.size) * 100, 2) } }
    }

    else {
        $ErrorActionPreference = 'SilentlyContinue'

        Stop-Service TrustedInstaller -Force
        Get-ChildItem -path "C:\windows\" -Include '*.log' -Recurse -force | Remove-Item -force -Recurse
        Get-ChildItem -path "C:\windows\logs" -Include '*.cab' -Recurse -force | Remove-Item -force -Recurse
        Get-ChildItem -path "C:\ProgramData\Microsoft\Windows\WER" -Include '*.*' -Recurse -force | Remove-Item -force -Recurse
        Get-ChildItem -path "c:\`$recycle.bin" -Include '*' -Recurse -force | Remove-Item -force -Recurse
        Get-ChildItem -path "C:\Users\*\AppData\Local\Google\Chrome\User Data\Default\Cache\" -include '*.*' -Recurse -force | Remove-Item -force -Recurse
        Get-ChildItem -path "C:\Users\*\AppData\Local\Microsoft\Terminal Server Client\" -include '*.*' -Recurse -force | Remove-Item -force -Recurse
        $tempfolders = @("C:\Windows\Temp\*", "C:\Windows\Prefetch\*", "C:\Documents and Settings\*\Local Settings\temp\*", "C:\Users\*\Appdata\Local\Temp\*")
        Remove-Item $tempfolders -force -recurse -errorAction SilentlyContinue
        $tempinternetfolders = @("C:\Users\*\Appdata\Local\Microsoft\Windows\INetCache\*", "C:\Users\*\Appdata\Local\Microsoft\Windows\Cookies\*", "C:\Users\*\AppData\Local\Microsoft\Windows\Temporary Internet Files\*.*")
        Remove-Item $tempinternetfolders -force -recurse -errorAction SilentlyContinue
        powercfg.exe /hibernate off
        Remove-Item c:\hiberfil.sys -force -ErrorAction SilentlyContinue
        Start-Service TrustedInstaller

        $ErrorActionPreference = 'Continue'

        Write-Host " "
        Write-Host "Free Space After Cleaning" -ForegroundColor Yellow
        Get-CimInstance win32_logicaldisk -filter "drivetype=3" -computer $ComputerName |
        Format-Table -Property DeviceID, Volumename, `
        @{Name = "SizeGB"; Expression = { [math]::Round($_.Size / 1GB) } }, `
        @{Name = "FreeGB"; Expression = { [math]::Round($_.Freespace / 1GB, 2) } }, `
        @{Name = "PercentFree"; Expression = { [math]::Round(($_.Freespace / $_.size) * 100, 2) } }
    }
}
#EndRegion Remove-AllTempFiles
#Region Remove-Application

<#
.Synopsis
    Attempt to Uninstall an application.

.DESCRIPTION
    This command uninstalls an application. Good for when elevated privileges are needed from a user session.

.PARAMETER Application
    Specify the application name to delete.

.EXAMPLE
    Specify the installed application being uninstalled. The full application name must be used.

    Remove-AppName -Application 'App Name has spaces'
    
.EXAMPLE
    Find application using Get-Applications and pipe the correct item into Remove-Application.
    
    Get-Applications | Where-Object { $_.DisplayName -match 'vim' } | Remove-Application

.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit

    Change Log:
    Version: 1.0 - Function Creation.
#>

Function Remove-Application() {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory, ValueFromPipeline)]
        $Application
    )
    Begin {}
    
    Process {
        if ($_ -is [PSCustomObject]) {
            $AppToRemove = $_
        }
        else {
            $AppToRemove = Get-Applications | Where-Object { $_.DisplayName -match $Application }
        }
        
        switch ($true) {
            { $AppToRemove.QuietUninstallString } {
                Write-Output "Running Quiet Uninstall String: $($AppToRemove.QuietUninstallString)"
                & $AppToRemove.QuietUninstallString
            }
            { $AppToRemove.UninstallString } {
                Write-Output "Running Uninstall String: $($AppToRemove.UninstallString)"
                & $AppToRemove.UninstallString
            }
            DEFAULT { Write-Error "No Uninstall String is provided for this application." }
        }
    }
}
#EndRegion Remove-Application

#Region Remove-OlderThan

<#
.Synopsis
    Remove files in a directory recursively based on how many days since the files was changed. Use negative values for -DaysBack.

.Description
    This scripts function is to delete files and folders older than x days recursively.

.PARAMETER Path
    Specify the root path to delete items from.
.PARAMETER DaysBack
    Specify the amount of days old since a file was edited to delete.
.PARAMETER Recurse
    Search recursively for files.

.Example
    Delete-OlderThan -Path "C:\Folder" -DaysBack 90

.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit

    Change Log:
    Version: 1.0 - Function Creation.
#>

Function Remove-OlderThan() {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]$Path,
        [Parameter(Mandatory = $true)][ValidateScript( { $_ -gt 0 })][int]$DaysBack,
        [Parameter(Mandatory = $false)][Switch]$Recurse
    )
    
    $CurrentDate = Get-Date
    $DatetoDelete = $CurrentDate.AddDays("-$Daysback")
    Get-ChildItem $Path | Where-Object { $_.LastWriteTime -lt $DatetoDelete } | Remove-Item -Force
}
#EndRegion Remove-OlderThan
#Region Remove-Path

<#
.Synopsis
    Deletes folder recursively, so be careful. If -Include is empty, it will delete all files, otherwise it will delete only the ones you -Include.

.DESCRIPTION
    This command deletes all files recursively in a path that match the included filename.

.PARAMETER Path
    Specify the path to recursively delete.

.PARAMETER Include
    Restrict the deletion to specific file names, types, etc.. by specifying them in this parameter.
    See `Get-Help Get-ChildItem -Parameter Include` for more information.

.EXAMPLE
    PS>Remove-Path C:\temp

    Description
    -----------
    Specify the parent folder from which the command runs and specify file names to include. Wildcards are supported.

    Remove-Path -path c:\Folder -include "*.logs"

.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Remove-Path() {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true)]$Path,
        [Parameter(Mandatory = $true)]$Include
    )

    Get-ChildItem -path "$Path" -Include "$Include" -Recurse -force | Remove-Item -force -Recurse
}
#EndRegion Remove-Path
#Region Remove-PrintQueue

<#
.Synopsis
    Quickly clear print que from all installed printers.
.DESCRIPTION
    This command clears print queues for all printers, including network printers. If you specify a single printer using -Printer, you will NOT clear all installed printers.
.PARAMETER Printer
    Specify the printer name to clear.
.EXAMPLE
    PS> Remove-PrintQueue -Printer Some_printer_name1
    
    Description
    -----------
    This will delete all of the current print jobs on the network printer 'Some_printer_name1'
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit

    Change Log:
    Version: 1.0 - Function Creation.
#>

Function Remove-PrintQueue() {
    [CmdletBinding()]
    param (
        [string]$Printer
    )

    if ($Printer) {
        $Printers = Get-Printer -Name *$Printer*
    }
    else {
        $Printers = Get-Printer 
    }

    foreach ($printer in $printers) {
        $printjobs = Get-PrintJob -PrinterObject $printer
        foreach ($printjob in $printjobs) {
            Remove-PrintJob -InputObject $printjob
        }
    }
}
#EndRegion Remove-PrintQueue
#Region Reset-NetworkAdapter

<#
.SYNOPSIS
    Reset a network interface.

.DESCRIPTION
    Reset a specified interface with -Interface.

.PARAMETER Interface
    Specify the name of the network interface name to reset.

.EXAMPLE
    Reset-NetworkAdapter -Interface "Local Area Connection"

.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit

    Change Log:
    Version: 1.0 - Function Creation.
#>

Function Reset-NetworkAdapter() {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]$Interface
    )

    netsh.exe interface set interface $Interface admin=disable
    netsh.exe interface set interface $Interface admin=enable
}
#EndRegion Reset-NetworkAdapter
#Region Reset-NetworkStack

<#
.SYNOPSIS
    Reset Network Stack. Will require a reboot.
.DESCRIPTION
    Resets the TCP/IP and Winsock Stacks
.EXAMPLE
    PS> Reset-NetworkStack
    
    Description
    -----------
    This will reset the winsock and ip, ipv4, and ipv6 interfaces.
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit

    Change Log:
    Version: 1.0 - Function Creation.
#>

Function Reset-NetworkStack() {
    [CmdletBinding()]
    param (
    )

    netsh.exe winsock reset
    netsh.exe int ip reset
    netsh.exe int ipv4 reset reset.log
    netsh.exe int ipv6 reset reset.log
    Write-Output "[-] You will need to restart this computer."
}
#EndRegion Reset-NetworkStack
#Region SU
$DetectedOS = switch($true) {
    $IsWindows {'Windows'}
    $IsLinux   {'Linux'}
    $IsMacOS   {'MacOS'}
    DEFAULT    {'Windows'}
}

If ($DetectedOS -eq 'Windows') {
    
    <#
    .SYNOPSIS
        Windows version of the linux command `SU`
    .DESCRIPTION
        Immitate SU on Linux. This creates new PoSH Session as an admin.
    .PARAMETER NoExit
        Default `su` will close the current session and launch a new one as admin
        Specify `-NoExit` to keep the non-admin shell running.
    .EXAMPLE
        PS> su
        
        Description
        -----------
        Depending on what edition of powershell is running, this will start an elevated process and close the original process.
    .EXAMPLE
        PS> su -NoExit
    
        Description
        -----------
        This will keep the non-admin shell running and you will have two processes open.
    .NOTES
        Author: Matthew J. DeGarmo
        Handle: @matthewjdegarmo
    
        You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
            or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
            on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
    #>

    function su() {
        [CmdletBinding()]
        param (
            [Switch] $NoExit
        )
        switch ($($PSVersionTable.PSEdition)) {
            "Desktop" { 
                $Parent = (Get-Process -Id $PID)
                Switch($Parent.ProcessName) {
                    'powershell_ise' {
                        Start-Process $Parent.Path -Verb RunAs 
                    }
                    'Code' {
                        #TODO: Detect VSCode running powershell.exe somehow
                    }
    
                    DEFAULT {
                        Start-Process Powershell -Verb RunAs 
                    }
                }
    
            }
            "Core" {
                $Parent = (Get-Process -Id $PID).Parent
                Switch($Parent.ProcessName) {
                    'WindowsTerminal' {
                        Start-Process wt.exe -Verb RunAs
                        If (-Not($NoExit.IsPresent)) {
                            exit
                        }
                    }
                    'Code' {
                        Start-Process $Parent.Path -Verb RunAs
                    }
                    DEFAULT {
                        Start-Process Pwsh -Verb RunAs
                    }
                }
            }
        }
    }
}

#EndRegion SU
#Region TimeStamp

<#
This is to pass the cmdlet exporting pester tests since this is a filter
Function TimeStamp() {
#>


<#
.SYNOPSIS
    This is a filter used to place colorized timestamps on any output messages.
.DESCRIPTION
    The function `TimeStamp` is a colorized version of this command `DateStamp`, but `TimeStamp` output cannot be written to a file. You will want to use `DateStamp` if you are going to output your messages into a log or txt file.
.PARAMETER Color
    Specify the color to display the message text.
    See `[System.ConsoleColor].GetEnumNames()` for full list of colors.
.PARAMETER NoNewLine
    Specify this to change the color of the first segment of text, and not the rest. See Example #3.

.EXAMPLE
    "ERROR: Something bad happened on this line of the script" | TimeStamp

    [08/04/2020 11:55:39] : ERROR: Something bad happened on this line of the script

    Description
    -----------
    This line will place a time stamp at the beginning of the line that can only be written to the console and not to a file.
.EXAMPLE
    "ERROR: Something bad happened on this line of the script" | TimeStamp Red

    [08/04/2020 11:56:40] : ERROR: Something bad happened on this line of the script

    Description
    -----------
    This will colorize the timestamp, and turn the provided string red. You can provide any color usable by Write-Host -ForegroundColor.
.EXAMPLE
    "ERROR: " | TimeStamp Red NoNewLine;"Something bad happened on this line fo the script"

    [08/04/2020 11:58:54] : ERROR: Something bad happened on this line fo the script

    Description
    -----------
    This will colorize the TimeStamp, and make "ERROR: " Red, and with `NoNewLine` provided, you can add additional non-colorized text to the same line.
.NOTES
    Author: Matthew J. DeGarmo
    Site: https://matthewjdegarmo.github.io

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Filter TimeStamp($color, $NoNewLine) {
    # [CmdletBinding()] # This is to pass the advanced function pester tests.
    # param () # This is to pass the advanced function pester tests.
    # Function # This is to pass the advanced function pester tests.
    if ($color -eq 'NoNewLine') {
        $color = 'White'
        $NoNewLine = 'NoNewLine'
    }
    Write-Host "[" -ForegroundColor Yellow -NoNewLine
    Write-Host $(Get-Date -Format "MM/dd/yyyy HH:mm:ss") -ForegroundColor Green -NoNewLine
    Write-Host "] " -ForegroundColor Yellow -NoNewLine
    Write-Host ": " -ForegroundColor Red -NoNewLine
    if ($NoNewLine) {
        Write-Host "$_" -ForegroundColor $color -NoNewline
    }
    elseif (!$Color) {
        Write-Host "$_"
    }
    else {
        Write-Host "$_" -ForegroundColor $color
    }
}
#EndRegion TimeStamp
#Region Update-PowerShell

<#
.SYNOPSIS
    This will both Install the latest release of PowerShell or update your current PowerShell.
.DESCRIPTION
    This one-liner is provided by [Tyler Leonhardt](https://github.com/TylerLeonhardt). I have added some parameters to help customize the install of the .MSI
.PARAMETER Preview
    Specifying this switch will install the latest preview version of PowerShell. Otherwise this will install / update the latest stable release.
.PARAMETER Quiet
    Specifying this switch will install or update quietly with no gui popup, taking all defaults of the install. You need to run as admin to use this switch.
.EXAMPLE
    PS> Update-Powershell -Preview
    
    Description
    -----------
    This will update or install PowerShell with the latest Preview release.
.EXAMPLE
    PS> Update-Powershell -Quiet
    
    Description
    -----------
    This will update or install the latest General Release version of PowerShell.
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Update-PowerShell() {
    [CmdletBinding()]
    param(
        [switch] $Preview,
        [switch] $Quiet
    )
    if ($PSBoundParameters.ContainsKey('Preview')) { $PreviewOption = '-Preview' }
    if ($PSBoundParameters.ContainsKey('Quiet')) { $QuietOption = '-Quiet' }
    Invoke-Expression -Command "& {$(Invoke-RestMethod https://aka.ms/install-powershell.ps1)} -UseMSI $PreviewOption $QuietOption"
}
#EndRegion Update-PowerShell
#Region Watch-Command

<#
.SYNOPSIS
    Loop through a command forever until canceled (Ctrl + C)
.DESCRIPTION
    This is meant to be a powershell equivalent to the linux `watch` command.
.PARAMETER Command
    This parameter takes in a command to evaluate. This parameter takes in a string, and uses Invoke-Expression to run the command. This means that complex commands must be wrapped within quotation marks.
.PARAMETER WaitSeconds
    This parameter takes in an Int (number) which equates to the number of seconds to wait after the completion of the command before executing again.
.PARAMETER Differences
    This switch will not overwrite the original text displayed if something in the output has changed. It will place a timestamp in between the previous output and the current (changed) output. This also breaks out what items were `Added` or `Removed` from the previous output to assist with monitoring visually.
.EXAMPLE
    PS> Watch-Command -Command Get-Process

    Description
    -----------
    This will run Get-Process, wait 5 seconds (the default amount of time) and run it again.
.EXAMPLE
    PS> Watch-Command "Get-Process | Select-Object -First 12" -Differences -WaitSeconds 3

    Description
    -----------
    This will run the Get-Process command through the pipeline, and monitor for differences every 3 seconds. Notice this command is treated as a string and `"wrapped in quotes"`
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo

    You can either submit a [PR](https://github.com/matthewjdegarmo/AdminToolkit/pulls)
        or create an [Issue](https://github.com/matthewjdegarmo/AdminToolkit/issues/new)
        on this GitHub project at https://github.com/matthewjdegarmo/AdminToolkit
#>

Function Watch-Command() {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [string] $Command,

        [Parameter(Position = 1)]
        [int] $WaitSeconds = 5,

        [switch] $Differences
    )
    begin {
        $Output = $null
        $PreviousOutput = $null
        $Difference = $null
        $SaveX = [console]::CursorLeft
        $SaveY = [console]::CursorTop + 1
    }
    process {
        try {                
            Write-Output "Watching command: '$Command' | Interval: $WaitSeconds`s | Time: $([datetime]::Now)"
            While ($true) {
                [console]::SetCursorPosition($SaveX, $SaveY)
                $Output = (Invoke-Expression -Command $Command -ErrorAction SilentlyContinue)
                if ($PreviousOutput -and $Output -and $Differences.IsPresent) {
                    # $Properties = $PreviousOutput | Get-Member -MemberType "*Property*"
                    $Difference = (Compare-Object $PreviousOutput $Output -PassThru)
                    if ($Difference) {
                        ($PreviousOutput | Out-String).Trim()
                        "|-------------------------------| |-----------------|"
                        "There was a change in the output: $([datetime]::Now)"
                        "|-------------------------------| |-----------------|"
                        $AddedDifferences = $Difference | Where-Object { $_.SideIndicator -eq "=>" }
                        $RemovedDifferences = $Difference | Where-Object { $_.SideIndicator -eq "<=" }
                        if ($AddedDifferences) { "Added:"; ($AddedDifferences | Out-String).Trim(); "" }
                        if ($RemovedDifferences) { "Removed:"; ($RemovedDifferences | Out-String).Trim(); "" }
                        # ($Difference | Out-String).Trim()
                        # $Difference = $null
                        # $AddedDifferences = $null
                        # $RemovedDifferences = $null
                        ""; Write-Output "Watching command: '$Command' | Interval: $WaitSeconds`s | Time: $([datetime]::Now)"
                        $SaveX = [console]::CursorLeft
                        $SaveY = [console]::CursorTop
                    }
                }
                if ($Differences.IsPresent) {
                    $PreviousOutput = $Output
                }
                ($Output | Out-String).Trim()
                Start-Sleep -Seconds $WaitSeconds
            }
        }
        finally {
            $SaveX = $null
            $SaveY = $null
            $Output = $null
            $PreviousOutput = $null
            # $Difference = $null
        }
    }
    end {}
}
#EndRegion Watch-Command