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 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
#>

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
#>

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 Source
    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
#>

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

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

        [Parameter()]
        [Switch] $IncludeACL
    )

    Write-Progress -Activity "Gathering data from $Source"
    $Source = (Resolve-Path -Path $Source).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 = ($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

<#
.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
#>

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
 
    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
#>

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
#>

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
#>

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
#>

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
#>

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.
.EXAMPLE
    PS> Get-FolderSize
 
    Description
    -----------
    This will display the folder size of the current folder location `Get-Location`
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo
#>

Function Get-FolderSize {
    [CmdletBinding()]
    Param (
        [Parameter(Position = 0)]$Folder = (Get-Location)
    )

    $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-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
#>

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
#>

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
#>

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
#>

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
#>

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
#>

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
#>

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
        }
        $PublicIP = @{
            IP           = $ipinfo.ip
            City         = $ipinfo.city
            Region       = $ipinfo.region
            Country      = $ipinfo.country
            Coord        = $ipinfo.loc
            Organization = $ipinfo.org
            Postal       = $ipinfo.Postal
            TimeZone     = $ipinfo.timezone
        }
        $PublicIP.getenumerator() | Sort-Object Key
    }
    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
#>

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
#>

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

<#
.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
#>

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
#>

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
#>

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
#>

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
 
    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 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
#>

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
     
    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
#>

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
#>

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
 
    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
 
    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
#>

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
 
    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
 
    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
 
    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

<#
.SYNOPSIS
    Windows version of the linux command `SU`
.DESCRIPTION
    Immitate SU on Linux. This creates new PoSH Session as an admin.
.EXAMPLE
    PS> su
     
    Description
    -----------
    Depending on what edition of powershell is running, this will start an elevated process.
.NOTES
    Author: Matthew J. DeGarmo
    Handle: @matthewjdegarmo
#>

function su() {
    [CmdletBinding()]
    param ()
    switch ($($PSVersionTable.PSEdition)) {
        "Desktop" { Start-Process Powershell -Verb RunAs }
        "Core" { Start-Process Pwsh -Verb RunAs }
    }
}
#EndRegion SU
#Region 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
#>

filter TimeStamp($color, $NoNewLine) {
    # [CmdletBinding()] # This is to pass the advanced parameter pester tests.
    # param () # This is to pass the advanced parameter pester tests.
    # function # This is to pass the advanced parameter 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
#>

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
#>

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