Dimmo.psm1

function Get-DuplicateFiles {
<#
.SYNOPSIS
    Generates a list of duplicate files in the current or specified directory.
.DESCRIPTION
    Duplicate files are calculated using a filehash. This can be very time consuming and resource intensive. Only files between 1 MB and 1 GB are scanned.
.EXAMPLE
    Get-DuplicateFiles
    This command scans for duplicate files in the current directory.
.EXAMPLE
    Get-DuplicateFiles -MinimumFileSize 1KB -MaximumFileSize 1GB
    This command scans for duplicate files in the current directory specifying alternate file sizes.
.EXAMPLE
    Get-DuplicateFiles -Path C:\Data -Verbose
    This command scans for duplicate files in the specified directory, and displays verbose output.
.EXAMPLE
    Get-DuplicateFiles -Path C:\Data -Verbose | Export-CliXml Dups.xml
    This command scans for duplicate files in the specified directory, displays verbose output, and writes to a CliXml file for future reference.
.EXAMPLE
    Get-DuplicateFiles | Out-GridView -OutputMode Multiple | Remove-Item -Confirm
    This command scans for duplicate files in the current directory and displays a graphical listing. Any selected files will be removed after confirmation.
.LINK
    www.dimensionit.nl
.NOTES
    By Dimitri Koens
 
    Contact me through:
    http://www.DimensionIT.nl
    Twitter: @DimensionIT https://twitter.com/DimensionIT
    Linkedin: http://nl.linkedin.com/in/dimitrikoens
    Facebook: http://www.facebook.com/dimitri.koens
 
    This function uses Get-FileHash function introduced in PowerShell 4.
 
    To do: implement workflows?, start-job?
 
    requires ps4?
#>


    [CmdletBinding()]
    param(
        # Specify a path with files to be checked for duplicates.
        [string[]]$Path = (Get-Location),   #(Get-Location).ProviderPath

        # Minimum file size to be checked.
        [int64]$MinimumFileSize = 1MB,

        # Maximum file size to be checked. Large files can take a long time to check.
        [int64]$MaximumFileSize = 1TB,

        # Different algorithms can have a huge impact on performance. SHA1 and MD5 are fast but regarded least reliable.
        [ValidateSet('SHA1', 'SHA256', 'SHA384', 'SHA512', 'MACTripleDES', 'MD5', 'RIPEMD160')]
        [string]$Algorithm='SHA256',

        # When specified sorts files by length ascending.
        [switch]$Sort
    
        # When specified will only calculate filehash on a section of the file. This can produce unreliable results.
        #[switch]$FastAndUnreliable
    
        #implement filter, exclude, params from get-childitem
    )

    Write-Progress -Activity "Traversing" -CurrentOperation "$path" -Percent 0

    $Files = Get-ChildItem -Path $Path -Recurse -File |
        Where-Object { $_.Length -ge $MinimumFileSize -and $_.Length -le $MaximumFileSize }

    # sort on length when required
    if ($Sort) { $Files = $Files | Sort-Object Length }

    $Files = $Files |
        Group-Object -Property Length |
        Where-Object { $_.Count -gt 1 }

    Write-Progress -Activity "Traversing" -CurrentOperation "Calculating total file size and number of suspects..." -Percent 80
    $SuspectsFound = 0
    $TotalSize = 0
    $Files | Foreach {
        $_.Group | Foreach { 
            $TotalSize += $_.Length
            $SuspectsFound++
        }
    }
    Write-Progress -Activity "Traversing" -Completed
    Write-Verbose "$("{0:N2}" -f ($TotalSize/1GB)) GB in $SuspectsFound suspect files"
    # if ($FastAndUnreliable) { Write-Warning "Calculating filehash on a section of the file. This can produce unreliable results!" }

    $start = Get-Date
    $i = 0
    $bytesProcessed = 0
    $DuplicateFilesFound = 0

    # PROCESS
    $Files | ForEach-Object {

        $_.Group | ForEach-Object {

            $i++

            $CurrentOp = "{0} Duplicates found. File {1} of {2}, {3:N2} GB in {4}" -f $DuplicateFilesFound, $i, $SuspectsFound, ($_.length/1GB), $_.FullName

            # only include remaining time when reliable
            if ($bytesProcessed -ge 1 -and ((Get-Date)-$start).TotalSeconds -gt 3) { 

                $TotalEstTime = ((Get-Date)-$start).TotalSeconds/$bytesProcessed*$TotalSize   # in seconds
                $secRemaining = $TotalEstTime - ((Get-Date)-$start).totalseconds

                Write-Progress -Activity "Calculating hash value" -CurrentOperation $CurrentOp -Percent ($bytesProcessed/$TotalSize*100) -SecondsRemaining $secRemaining

            } else {

                Write-Progress -Activity "Calculating hash value" -CurrentOperation $CurrentOp -Percent ($bytesProcessed/$TotalSize*100)

            }

            #if ($FastAndUnreliable) {
            $FileHash = (Get-FileHash -Path $_.FullName -Algorithm $Algorithm).Hash   # rewrite with try ... catch
            
            $_ | Add-Member -MemberType NoteProperty -Name FileHash -Value $FileHash
            $bytesProcessed += $_.length

        } # $_.Group

      $_.Group |
        Where-Object { $_.FileHash -ne $null } |   # if FileHash is null file could probably not be opened for reading - filehash is still in properties, CHECK !!!!!!!!!!!!!!!!!!!!!!!!
        Group-Object -Property FileHash |
        Where-Object { $_.Count -gt 1 } |
        Foreach {
            $DuplicateFilesFound++
            $_.Group # don't select, because piping to remove-item won't work anymore, use type ps1xml instead
            $_.Group | ForEach { Write-Debug ($("{0,12} bytes {1}" -f $_.Length, $_.FullName)) }
        }

    } # $Files

    Write-Progress -Activity "Calculating hash value" -Completed

    $TimeTaken = ((Get-Date) - $start).TotalSeconds
    if     ($TimeTaken -lt 60)   { Write-Verbose ("{0} Duplicates found, time taken: {0:N1} seconds" -f $DuplicateFilesFound,  $TimeTaken) }
    elseif ($TimeTaken -lt 3600) { Write-Verbose ("{0} Duplicates found, time taken: {0:N1} minutes" -f $DuplicateFilesFound, ($TimeTaken/60)) }
    else                         { Write-Verbose ("{0} Duplicates found, time taken: {0:N1} hours"   -f $DuplicateFilesFound, ($TimeTaken/3600)) }
    
    if ($DuplicateFilesFound -lt 1) { Write-Warning 'No duplicate files found' }
    
}   # end of function


function Get-FolderSpaceReport {
    <#
    .SYNOPSIS
        Returns a list of total space usage per folder
    #>

    param([string]$directory='.')

    Get-ChildItem -Directory -Path $directory |
        foreach {
            $subfiles = Get-ChildItem $_ -Recurse
              $totalsize = 0
              $subfiles | foreach { $totalsize += $_.length }
              "{0,8:N0} {1,16:N0} {2}" -f $subfiles.count, $totalsize, $_.fullname
     }
}


function Enable-PSRemotingThroughWmi {
    <#
    .SYNOPSIS
        This function enables PowerShell Remoting through WMI
    .DESCRIPTION
        PowerShell Remoting is disabled by default. It can be enabled by running the command Enable-PSRemoting. With this function you can enable PowerShell Remoting through the WMI interface, which is enabled on many Windows systems.
    .EXAMPLE
        Enable-PSRemotingThroughWmi -ComputerName server5
        This command enables PowerShell Remoting on server5.
    .EXAMPLE
        Enable-PSRemotingThroughWmi -ComputerName server5 -Verbose
        This command enables PowerShell Remoting on server5 and displays verbose output.
    #>

    
    [cmdletbinding()]param(
        $ComputerName=$env:COMPUTERNAME
    )

    Write-Verbose "Invoking WMI"
    Try {
        $cmd = "cmd /c powershell enable-psremoting -force -skipnetworkprofilecheck && net stop WinRM && net start WinRM && net stop MpsSvc && net start MpsSvc"
        $wmi = Invoke-WmiMethod -class Win32_process -name Create -ArgumentList $cmd -ComputerName $ComputerName -ErrorAction Stop
        if ($wmi.ReturnValue -eq 0) { Write-Verbose "Success. It may take up to 60 seconds to complete..." } else { Write-Error "WMI failed. ReturnValue: $($wmi.ReturnValue)" }
    }
    Catch {
        Write-Error "WMI failed. Make sure Remote WMI service is running and allowed in firewall."
    }
        
}


function NetworkMonitor {

    <#
    .SYNOPSYS
        Sends a ping to all hosts on the local subnet.
 
    .DESCRIPTION
        This function sends a ping to all hosts on the local subnet. This is repeated every 3 seconds to monitor hosts that are coming online or are going offline. The interval is configurable through the interval parameter.
 
    .EXAMPLE
        NetworkMonitor
        This command sends a ping to all hosts with the default interval of 3 seconds.
 
    .EXAMPLE
        NetworkMonitor -interval 30
        This command sends a ping to all hosts with an interval of 30 seconds.
 
    .EXAMPLE
        NetworkMonitor -noloop
        This command sends a ping to all hosts on the local subnet and stops. Output is pipeline-ready.
 
    .NOTES
        By Dimitri Koens
        http://www.dimensionit.nl
        Doesn't run on W7SP1
    #>


    param
    (
        [int]$interval=3,
        [switch]$noloop
    )

    function Get-IPrange
    { 
        param  
        (  
          [string]$start,  
          [string]$end,  
          [string]$ip,  
          [string]$mask,  
          [int]$cidr  
        )  
  
    function IP-toINT64 () {  
      param ($ip)  
      $octets = $ip.split(".")  
      return [int64]([int64]$octets[0]*16777216 +[int64]$octets[1]*65536 +[int64]$octets[2]*256 +[int64]$octets[3])  
    }  
  
    function INT64-toIP() {  
      param ([int64]$int)  
      return (([math]::truncate($int/16777216)).tostring()+"."+([math]::truncate(($int%16777216)/65536)).tostring()+"."+([math]::truncate(($int%65536)/256)).tostring()+"."+([math]::truncate($int%256)).tostring() ) 
    }  
  
    if ($ip) {$ipaddr = [Net.IPAddress]::Parse($ip)}  
    if ($cidr) {$maskaddr = [Net.IPAddress]::Parse((INT64-toIP -int ([convert]::ToInt64(("1"*$cidr+"0"*(32-$cidr)),2)))) }  
    if ($mask) {$maskaddr = [Net.IPAddress]::Parse($mask)}  
    if ($ip) {$networkaddr = new-object net.ipaddress ($maskaddr.address -band $ipaddr.address)}  
    if ($ip) {$broadcastaddr = new-object net.ipaddress (([system.net.ipaddress]::parse("255.255.255.255").address -bxor $maskaddr.address -bor $networkaddr.address))}  
  
    if ($ip) {  
      $startaddr = IP-toINT64 -ip $networkaddr.ipaddresstostring  
      $endaddr = IP-toINT64 -ip $broadcastaddr.ipaddresstostring  
    } else {  
      $startaddr = IP-toINT64 -ip $start  
      $endaddr = IP-toINT64 -ip $end  
    }  
  
  
    for ($i = $startaddr; $i -le $endaddr; $i++)  
    {  
      INT64-toIP -int $i  
    } 
 
    } # function get-iprange

    function PingAsync {
        param($ComputerName)
        $t=$ComputerName | foreach { (New-Object Net.NetworkInformation.Ping).SendPingAsync($_,250) }
        [Threading.Tasks.Task]::WaitAll($t)
        $t.Result
    }

    Write-Host "NetworkMonitor: ping entire subnet, use with care! (interrupt with Ctrl-C)" -ForegroundColor Cyan

    $netip = Get-NetIPAddress -AddressFamily ipv4 | 
        Where-Object ipaddress -notmatch '127\.0\.0\.1|169\.254\.'

    if ($netip.count -gt 1)
    {
        $netip = $netip | 
            Out-GridView -OutputMode single -Title 'Select an IP address' 
    }

    $ips = Get-IPrange -ip $netip.IPAddress -cidr $netip.PrefixLength
    $ips = $ips[1..($ips.count-2)]   # exclude network address and broadcast address

    if ($ips.count -gt 256)
    {
        Write-Warning "IP Range very large, can take a long time!"
    }

    Write-Host ("{0:hh:mm:ss} Sending initial ping to {1} addresses... " -f (get-date), $ips.count) -NoNewline

    $res1 = PingAsync $ips

    Write-Host "$(($res1 | Where-Object Status -eq 'Success').count) IP addresses responding"
    
    if (!$noloop) {
        $res1 | 
            Where-Object Status -eq 'Success' | 
            foreach { Write-Host ("{0:hh:mm:ss} {1,-15} is responding" -f (get-date), $_.address) -foregroundcolor green }
    }
    else {
        $res1 | 
            Where-Object Status -eq 'Success' |
            Select-Object Status, Address, RoundtripTime
    }

    while(!$noloop) {

        Start-Sleep $interval
        $res2 = PingAsync $ips

        Compare-Object $res1 $res2 -Property status, address -passthru | foreach {

            if ($_.sideIndicator -eq "=>" -and $_.status -eq 'success') {
                Write-Host ("{0:hh:mm:ss} {1,-15} is responding"     -f (get-date), $_.address) -foregroundcolor green  
            }   # success

            if ($_.sideIndicator -eq "<=" -and $_.status -eq 'success') { 
                Write-Host ("{0:hh:mm:ss} {1,-15} is not responding" -f (get-date), $_.address) -foregroundcolor yellow
            }   # prev success

        } # foreach

      $res1 = $res2

    }  # while
} # function


function ProcessMonitor {

    <#
    .SYNOPSIS
    Displays changes in the process list on this or a remote PC.
    .DESCRIPTION
    Great for monitoring logon/startup scripts, batch jobs, software installations, etc... Especially on terminal servers. Works for local and remote computers.
    .EXAMPLE
    ProcessMonitor
    Compares changes in the process list every second on the local computer.
    .EXAMPLE
    ProcessMonitor -Interval 30
    Compares changes in the process list for every 30 seconds.
    .EXAMPLE
    ProcessMonitor -Computername ServerB
    Compares changes in the process list on ServerB.
    .NOTES
    Created by Dimitri Koens, www.dimensionit.nl
    Version 1.3: display current time when results in compare are empty
    Version 1.4: commandlineWidth implemented
    Next version: adapt for ISE
    #>


    param([int]$Interval=1, [string]$Computername='.')

    Write-Host "ProcessMonitor (interrupt with Ctrl-C)" -ForegroundColor Cyan

    $minimumWidth = 40
    $refProcs = Get-WmiObject win32_process -ComputerName $Computername

    Do {
      Start-Sleep $Interval
      $diffProcs = Get-WmiObject win32_process -ComputerName $Computername
      $result = Compare-Object $refProcs $diffProcs -Property ProcessId -passthru
      $result | foreach {

        # construct primary string
        $msg = "{0:hh:mm:ss} {1,5} pid {2,15} " -f (Get-Date) , $_.ProcessId, $_.Name

        # construct rest of string, .commandline also contains .path
        $commandlineWidth = $Host.UI.RawUI.WindowSize.Width - $msg.Length   # measure everty time to address screen resize
        If ($commandlineWidth -lt $MinimumWidth) { $commandlineWidth = $MinimumWidth }
        If ($_.commandline.length -lt $commandlineWidth) { 
            $msg = $msg + $_.commandline   
        } else {
            $msg = $msg + $_.commandline.SubString(0,$commandlineWidth-1)
        }

        if ($_.sideIndicator -eq "=>") { Write-Host $msg -foregroundcolor green  }   # new process running
        if ($_.sideIndicator -eq "<=") { Write-Host $msg -foregroundcolor yellow }   # existing process stopped

      } # foreach
      if ($result -eq $null) { 
        $msg = "{0:hh:mm:ss}" -f (Get-Date)
        Write-Host -NoNewline $msg
        $Host.UI.RawUI.CursorPosition = New-Object System.Management.Automation.Host.Coordinates 0,($Host.UI.RawUI.CursorPosition.y)
      }
      $refProcs = $diffProcs
    } while (1)

} # function


function ServiceMonitor {
    <#
    .SYNOPSIS
    Displays changes in services list
    .DESCRIPTION
    version 1.0
    .EXAMPLE
    ServiceMonitor
    Compares changes in the services list every second on the local computer or remote computers.
    .EXAMPLE
    ServiceMonitor -Interval 30
    Compares changes in the services list for every 30 seconds.
    .NOTES
    Niet te zien of een service wijzigt of nieuw is
    #>


    param($computername='localhost', [int]$Interval = 1)

    Write-Host "ServiceMonitor (interrupt with Ctrl-C)" -fore cyan

    $svcBaseline = Get-Service -computername $computername

    Do {

      Start-Sleep $Interval

      $svcCurrent = Get-Service -computername $computername

      Compare-Object $svcBaseline $svcCurrent -Property machinename, name, displayname, status | 
        where { $_.sideindicator -eq '=>' } | foreach {
          $msg = "{0:hh:mm:ss} {1} {2} ({3})" -f (get-date) , $_.machinename, $_.name, $_.displayname
          if ($_.status -eq "running") { Write-Host $msg -foregroundcolor green  }   # service started
          if ($_.status -eq "stopped") { Write-Host $msg -foregroundcolor yellow }   # service stopped
      } # foreach

      $svcBaseline = $svcCurrent

    } while ( 1 -eq $true)

} # function


function EventLogMonitor {

    param( $Log='system' , $computername='.', $interval =1, $window= 100, $color="cyan" )

    # nog verwerken: echo $log tijdens init
    # nog verwerken: echo warning over breedte van console
    # nog verwerken: afbreken einde regel werkt niet goed

    <#
    .SYNOPSIS
    Monitors the eventlog for new entries and displays them on screen.
    .DESCRIPTION
    With this tool you can monitor the eventlog for new entries as they occur. Great for troubleshooting.
    #>


    # security events opsommen, ook over rdp
    # scom acs kevin holman
    # ook mogelijk om ipv compare een where te doen icm index property, maar dan misschien moeilijker werkend te maken over meerdere computers

    $MinimumWidth = 160

    function GetEventLogFromMultipleComputers {
        $tmpEvents = @()   # array leegmaken
        foreach ($computer in $computername) {
            $tmpEvents += Get-EventLog $Log -newest $window -ComputerName $computer
        }
        $tmpEvents | sort timegenerated   # need to sort because output is chronological and default sort is probably by machinename
    }

    function SimplifyEventLogMessage {
        param([string]$Message)
        $returnmessage = ""
        Foreach ($line in $message) {
            $returnmessage += $line.ToString()
        }
        $returnmessage = $returnmessage.replace([char]10, " ").replace([char]13, " ").replace(" ", " ").replace(" ", " ").replace(" ", " ")
    if ($returnmessage.length -ge $width) { $returnmessage.SubString(0,$width) } else { $returnmessage }
    # if ($returnmessage.length -gt $width) { $returnmessage = $returnmessage.SubString(0,$width) }
     # $returnmessage
    }

    Write-Host "EventLog Monitor v1.0" -foregroundcolor cyan
    If ($computername.count -gt 1) { "running across $($computername.count) computers: $computername" }

    Write-Warning "Events will not show up when source computers log more than $window events in $interval seconds"

    $Width = $Host.UI.RawUI.WindowSize.Width - 30
    If ($Width -lt $MininmumWidth) { $Width = $MinimumWidth; Write-Warning "Output width set to $Width" }

    write-progress -Activity 'reading eventlog'
    $a = GetEventLogFromMultipleComputers
    write-progress -activity 'reading eventlog' -Completed

    Do {
      Sleep $interval
      $b = GetEventLogFromMultipleComputers

      Compare-Object $a $b -Property MachineName, Index -passthru | where { $_.sideindicator -eq "=>" } | foreach {
        if ($_ -ne $null) {
            if ($_.MachineName.length -gt 15) { $MachineName = $_.MachineName.SubString(0,15) } else { $MachineName = $_.MachineName }
            $msg = "{0,15} {1:HH:mm:ss} {2,6} {3} | {4}" -f $MachineName, $_.TimeGenerated, $_.EventID, $_.Source, (SimplifyEventLogMessage $_.Message)   # colored output
            switch ($_.entrytype)
            {
                'error'   { Write-Host $msg -foregroundcolor 'red' }
                'warning' { Write-Host $msg -foregroundcolor 'yellow' }
                Default   { Write-Host $msg -foregroundcolor 'cyan' }
            }
         }
      } # foreach
      $a = $b
    } while ( 1 -eq $true)

}


function FolderMonitor {

    <#
    .SYNOPSIS
    Displays changes in a folder
    .DESCRIPTION
    version 1.1
    .EXAMPLE
    FolderMonitor C:\SCCMContentlib, C:\SMSPKG, 'C:\SMSPKGC$', C:\SMSPKGSIG, 'C:\SMSSIG$'
    Compares changes in the specified folders every second on the local computer.
    #>


    [cmdletbinding()]param(
        [string[]]$folder,   # required
        [int]$Interval = 1
        # to do: force, recurse, output to address specific changes (mode, length, lastwritetime)
    )

    Write-Host "FolderMonitor, interrupt with Ctrl-C, inspecting $folder" -ForegroundColor cyan

    $f1 = Get-ChildItem -path $folder -Force -Recurse
    Write-Verbose "$($f1.count) items found"

    Do {
      Start-Sleep $Interval
      $f2 = Get-ChildItem -path $folder -Force -Recurse
      Compare-Object $f1 $f2 -Property name, mode, length , lastwritetime -passthru | foreach {
        $msg = "{0} {1} {2} {3}" -f $_.mode, $_.lastwritetime, $_.length, $_.fullname
        if ($_.sideindicator -eq "=>") { Write-Host $msg -foregroundcolor green  }
        if ($_.sideindicator -eq "<=") { Write-Host $msg -foregroundcolor yellow }
      } # foreach
      $f1 = $f2
    } while (1)

} # function


function Send-WakeOnLan {
    <#
    .DESCRIPTION
    Enter MAC Address in the following form: 00-00-00-00-00-00 or 00:00:00:00:00:00
    .NOTES
    A Magic Packet is a broadcast frame containing anywhere within its payload 6 bytes of all 255 (FF FF FF FF FF FF in hexadecimal), followed by sixteen repetitions of the target computer's 48-bit MAC address, for a total of 102 bytes (source: Wikipedia)
    Troubleshooting: uitsluitend wol indien pc uitstaat dmv softknop (oftewel: dient al eens aangestaan hebben)
 
    To do: retrieve mac addresses from dhcp
    To do: accept DHCP leases as pipeline input
    to do: param macaddress throw 'verplicht'
    #>


    param(
        [string[]]$MacAddress, 
        [int]$repeat=1,
        [int]$interval=1
    )

    $MacAddress = $MacAddress.Split('-').Split(':' )
    $formattedMacAddress = $MacAddress | foreach { [System.Convert]::ToByte($_ ,16) }

    # constructing magic packet
    $packet = [byte[]](,0xFF * 102)   # construct 102 bytes of FF
    6..101 | foreach { $packet[$_] = [byte]$formattedMacAddress[($_%6)] }

    $UDPclient = New-Object System.Net.Sockets.UdpClient
    $UDPclient.Connect(([System.Net.IPAddress]::Broadcast), 4000)

    for ($i=0; $i -lt $repeat; $i++) 
    {
        "{0:HH:mm:ss} Sending wake-up packet to {1}" -f (Get-Date), "$MacAddress"
        $result = $UDPclient.Send($packet, $packet.Length)   # result: number of bytes sent

        if ($result -ne 102)
        {
            Write-Error "Something went wrong: $result bytes sent"
        }

        if ($i -lt $repeat-1) { Start-Sleep $interval }
    }

    $UDPclient.Close()  # finalize? dispose? :(

}


function MultiPing {
    <#
    .SYNOPSIS
    Sends a ping to a specified host or several hosts. Colors the output to indicate latency.
    .DESCRIPTION
    Provides a simple network monitoring solution, without the need to install any software.
    .EXAMPLE
    MultiPing ServerX
    Sends a ping to ServerX every second. Repeats forever.
    .EXAMPLE
    MultiPing ServerX, ServerY, 10.1.1.254, www.google.com
    Sends a ping to two servers, the IP address of the default gateway and a webserver on the internet
    #>


    param($computername="localhost", [switch]$ExcludeDefaultGateway=$false, [switch]$NoRepeat=$false, [int]$PingCritical=8, [int]$PingWarning=4, [int]$Delay=1000)

    if ($ExcludeDefaultGateway -eq $false) {
        $gw = Get-WmiObject Win32_NetworkAdapterConfiguration | Where { $_.IPEnabled -and $_.DefaultIPGateway -ne $null } | Select -expand DefaultIPGateway
        $computername += $gw
    }

    Write-Host "Pinging $($computername.count) remote systems. Interrupt with Ctrl-C." -Foregroundcolor cyan
    Write-Host "Delay: $Delay ms. Thresholds: critical=$PingCritical, warning=$PingWarning" -Foregroundcolor cyan

    $i = 0   # line numbers

    Do {
      $height = $host.ui.RawUI.WindowSize.Height - 2
      if ($height -lt 5) { $height = 5 }   # height = 0 in ISE
      if ([int]($i / $height) -eq ($i / $height)) { " $computername" }   # write header
      $computername | foreach {
        $a = Test-Connection $_ -Count 1 -ErrorAction SilentlyContinue
        if (!$?) { $msg = "---".PadLeft($_.length) + " "; Write-Host $msg -nonewline -fore red }
        else {
          $msg = "$($a.ResponseTime.ToString().Padleft($_.length)) "   # used $($a.Address) to write hostname on screen
          if     ($a.ResponseTime -ge $PingCritical) { write-host $msg -nonewline -fore red }
          elseif ($a.ResponseTime -ge $PingWarning)  { write-host $msg -nonewline -fore yellow }
          else                                       { write-host $msg -nonewline }
        }
      }
      $i++
    Write-Host ""
    if (!$NoRepeat) { Start-Sleep -Milliseconds $Delay }   # perform delay only when repeat is true
    } while (!$NoRepeat)   # exit loop after first round when -NoRepeat is specified
}


function Get-MessageOfTheDay {
    $msg = 
    "Knowledge is power",
    "Power is the ultimate aphrodisiac",
    "With great power comes great responsibility",
    "If computers get too powerful, we can organize them into a committee -- that will do them in.",
    "Nearly all men can stand adversity, but if you want to test a man's character, give him power.",
    "Never go backward. Attempt, and do it with all your might. Determination is power.",
    "Power is like being a lady... if you have to tell people you are, you aren't.",
    "Power is always dangerous. Power attracts the worst and corrupts the best.",
    "There are 10 types of people who understand binary: those who do and those who don't.",
    "I'm sure the universe is full of intelligent life. It's just been too intelligent to come here.",
    "If you think it's expensive to hire a professional, wait until you hire an amateur",
    "Why is it drug addicts and computer afficionados are both called users?",
    "Progress isn't made by early risers. It's made by lazy men trying to find easier ways to do something.",
    "That's the thing about people who think they hate computers... What they really hate are lousy programmers."

    # select a random message
    $msg[(random($msg.length))]

}

Set-Alias motd Get-MessageOfTheDay


function Invoke-Speak {

    <#
    .Synopsis
       Reads text through the default speakers
    .DESCRIPTION
       Long description
    .EXAMPLE
       Invoke-Speach
       This command reads an inspiring message through the default speakers
    .EXAMPLE
       Invoke-Speach -Volume 50 -Rate 1 "Nearly all men can stand adversity, but if you want to test a man's character, give him PowerShell!"
       Another example of how to use this cmdlet
    .EXAMPLE
       "Never go backward. Attempt, and do it with all your might. Determination is power." | Invoke-Speach
       A string being passed through the pipeline will be spoken
    .EXAMPLE
        The measure of a man is what he does with PowerShell | say
        Example of the use of the alias
    .NOTES
         Verbs that apply: Out, Invoke, Read
    #>


    [CmdletBinding()]
    Param
    (
        # The sentence or word that is being spoken
        [Parameter(ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0)]
        [string]
        $Sentence='With great power comes great responsibility',

        # Volume between 0 and 100, default is 100
        [Parameter(ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=1)]
        [int]
        $volume=100,

        # The rate, or speed of the sentence that's being spoken. -10 is extremely slow, 10 is extremely fast, 0 is the default.
        [int]
        $rate=0,

        # The voicenumber that is found on the system, default is 0
        [int]
        $VoiceNumber=0
    )

    Begin
    {
        $a = New-Object -ComObject SAPI.SpVoice
        $a.Volume = $Volume
        $a.Rate = $Rate
        $a.voice = ($a.GetVoices[$VoiceNumber])
    }

    Process
    {
        $a.speak($sentence)
    }

    End
    {
        $a = $null
    }
}

Set-Alias say Invoke-Speak

# Export-ModuleMember -Alias * -Cmdlet * -Function * -Variable *