Library.psm1

$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
   Write-Host "Module Library.psm1 removed on $(Get-Date)"
}
$PRIVATE:PrivateData = $MyInvocation.MyCommand.Module.PrivateData  #Available to all functions below.
function Run([String]$scriptName = '-BLANK-') {
<#
.SYNOPSIS
Record start and end times of any scripts that are run during this session in the Scripts Event Log.
.DESCRIPTION
This function records any running scripts in the Scripts Event Log. Start scripts with 'Run xxxx.ps1' to capture. Use
'Get-Help Run -Online' to access any required Windows services mentioned here.
.LINK
 Http://www.SeaStar.co.nf
#>

    if ($host.Name -ne 'ConsoleHost') {     #Use Run-Script on ISE Host.
        return
    } 
    $logfile = "$env:programfiles\Sea Star Development\" + 
        "Script Monitor Service\ScriptMon.txt"
    $parms  = $myInvocation.Line -replace "run(\s+)$scriptName(\s*)"
    $script = $scriptName -replace "\.ps1\b"       #Replace from word end only.
    $script = $script + ".ps1"
    If (Test-Path $pwd\$script) {
        If(!(Test-Path Variable:\Session.Script.Job)) {
            Set-Variable Session.Script.Job -value 1 -scope global `
                -description "Script counter"
        }
        $Job    = Get-Variable -name Session.Script.Job
        $number = $job.Value.ToString().PadLeft(4,'0')
        $startTime = Get-Date -Format "dd/MM/yyyy HH:mm:ss"
        $tag  = "$startTime [$script] start. --> $($myInvocation.Line)"
        If (Test-Path $logfile) {
            $tag | Out-File $logfile -encoding 'Default' -Append
        }
        Write-EventLog -Logname Scripts -Source Monitor -EntryType Information -EventID 2 -Category 001 -Message "Script Job: $script (PS$number) started."
        Invoke-Expression -command "$pwd\$script $parms"
        $endTime = Get-Date -Format "dd/MM/yyyy HH:mm:ss"
        $tag  = "$endTime [$script] ended. --> $($myInvocation.Line)"
        If (Test-Path $logfile) {
            $tag | Out-File $logfile -encoding 'Default' -Append
        }
        Write-Eventlog -Logname Scripts -Source Monitor -EntryType Information -EventID 1 -Category 001 -Message "Script Job: $script (PS$number) ended."
        #The next line is only needed in case any script itself updates this value.
        $Job = Get-Variable -name Session.Script.Job
        if ($job.Value.ToString().PadLeft(4,'0') -eq $number) {
           $job.Value += 1                    #Only increment here if not done elsewhere.
        } 
    } else {
        Write-Error "$pwd\$script does not exist."
    }
}

function backup {   #Edit to suit. This will fail if selected function is not available.
   Invoke-Expression "Select-Backup $pwd,$pwd\modules\modem,$pwd\modules\ebooks,$pwd\modules\library ps1,psm1,psd1,xml,xaml -exclude cd -interval $args"
} 

function killx {
<#
.SYNOPSIS
Delete duplicate instances of Windows Explorer running.
.DESCRIPTION
Often Windows Explorer can leave multiple instances in memory thereby wasting system resources.
#>

   $exCount = 0
   $exCount = @(Get-Process -ErrorAction SilentlyContinue explorer).count
   if ($exCount -gt 1) {
      Get-Process explorer | 
         Sort-Object -descending CPU | 
            Select-Object -Skip 1 ID | 
               ForEach-Object -Process {Stop-Process -Id $_.ID}
      Write-Warning "$($exCount-1) instance(s) of Windows Explorer terminated."
   }
}

function Get-SessionIDs {          #NOTE: This will not now run the last selected Id on exiting.
   $date = "{0:F}" -f [DateTime]::Now
   $selected = Get-History -Count 100 | Sort commandLine -Unique | sort id | 
      Select @{name='Execution Time';expression={$_.EndExecutionTime}}, `
             @{name='Id';expression={"{0:D3}" -f $_.Id}}, CommandLine |
         Out-GridView -Title "$date Session Command History" -outputMode Single 
         if ($null -eq $selected) {
            return                      #Cancel or X was clicked.
         } else {                       #OK was selected, so run chosen history Id.
            $selected | Invoke-History
         }
}

function Debug-Regex {
<#
.SYNOPSIS
Debug a Regex search string and show any 'Match' results.
.DESCRIPTION
Sometimes it is easier to correct any regex usage if each match can be shown
in context. This function will show each successful result in a separate colour,
including the strings both before and after the match.
.EXAMPLE
Debug-Regex '\b[A-Z]\w+' 'Find capitalised Words in This string' -first
Use the -F switch to return only the first match.
.EXAMPLE
Debug-Regex '\b[0-9]+\b' 'We have to find numbers like 123 and 456 in this'
.EXAMPLE
PS >$a = Get-Variable regexDouble -ValueOnly
PS >db $a 'Find some double words words in this this string'
.NOTES
Based on an idea from the book 'Mastering Regular Expressions' by J.Friedl,
page 429.
#>

   param ([regex]$regex = '(?i)(A|B)\d',
          [string]$string = 'ABC B1 a1 A1 B6 d2',
          [switch]$first)
   $m = $regex.match($string)
   if (!$m.Success) {
      Write-Host "No Match using Regex '$regex'" -Fore Cyan
      return
   }
   $count = 1
   Write-Host "MATCHES [--------------<"      -Fore Cyan  -NoNewLine
   Write-Host "match"                          -Fore White -NoNewLine
   Write-Host ">-------------------]"          -Fore Cyan
   while ($m.Success) {
      Write-Host "$count $($m.result('[$`<'))" -Fore Cyan  -NoNewLine 
      Write-Host "$($m.result('$&'))"          -Fore White -NoNewLine 
      Write-Host "$($m.result('>$'']'))"       -Fore Cyan 
      if ($first) {
         return
      }
      $count++
      $m = $m.NextMatch()
   }
   Write-Host "MATCHES above using regex [-<" -Fore Cyan  -NoNewLine
   Write-Host $regex                   -Fore White -NoNewLine
   Write-Host ">-]"                     -Fore Cyan
}

function Get-Function {
<#
.SYNOPSIS
Find any Function in a file.
.DESCRIPTION
Use 'Get-ChildItem' with a filter to pipe values to 'Select-String' in order
to extract the Name and Line number.
.EXAMPLE
Get-Function 'debug(.+)\b' -recurse
#>

   param ([Parameter(mandatory=$true)][regex]$pattern,
          [string]$path = $pwd,
          [string]$filter = '*.ps*',
          [switch]$recurse = $false)
   if ($filter -notmatch '\*\.ps(m?1|\*)') {
      Write-Warning "Filter '$filter' is invalid -resubmit"
      return
   }        
   Get-ChildItem $path -Filter $filter -recurse:$recurse | 
      Select-String -Pattern '\bfunction\b' | ForEach-Object {
         $tokens = [Management.Automation.PSParser]::Tokenize($_.Line,[ref]$null)
         $(
            foreach ($token in $tokens) {
               if ($token.Type -eq ‘Keyword’ -and $token.Content -match ‘function’) {
                  do { 
                     $more = $foreach.MoveNext()
                  } until ($foreach.Current.Type -eq ‘CommandArgument’ -or !$more)
                  New-Object PSObject -Property @{
                     Filename = $_.Path          #From Select-String values.
                     Line = $_.LineNumber 
                     FunctionName = $foreach.Current.Content  #CommandArgument.
                 } | Select-Object FunctionName, FileName, Line
               }
            }
         ) | Where-Object {$_.FunctionName -match '(?i)' + $pattern} 
   }
}


function Show-Usage {
<#
.SYNOPSIS
Show usage of certain console commands.
.DESCRIPTION
Scan the console Transcript file and count the non-PowerShell commands used. List
the results in a table in descending order. Use the exported alias 'summary'. All
commands and aliases from the Library module will be found, so only add extra parameters
for those not there, ie 'summary import-module'. Multiple parameters must be separated
by commas.
.EXAMPLE
PS >summary
With no input parameters the Transcript file is scanned for any use of any exported commands
or aliases.
 
Count Name
----- ----
   28 summary
   11 killx
    5 backup
    3 add
    2 ff
    1 show-usage
    1 addTime
    1 gh
.EXAMPLE
PS >summary publish-Module, Import-Module
The same as a default search, but with two extra parameters for commands that have been used,
which can be anything entered in the PowerShell console. Unused parameters will be ignored in
the search.
 
Count Name
----- ----
   48 import-module
   29 summary
   11 killx
    5 backup
    3 publish-module
    3 add
    2 ff
    1 show-usage
    1 addTime
    1 gh
.NOTES
All commands exported by the Library module will be included in the default search pattern,
including any aliases. To use an extra parameter just add it to the Show-Usage (or summary)
command with multiple parameters being separated by commas. See examples for details.
#>

   param([Array]$data,                          #Input string comma separated.
         $PRIVATE:extra = 'find-anything\b|')    #Just a blank parameter here.
   if ($data -ne $null) {
      $data | foreach {
         $PRIVATE:extra+= $_ + "\b|"
      }                        #Now we add $extra to the default pattern first.
   }                                                  #Followed by any aliases.
   [Array]$aliases = (Get-Module library).exportedAliases | foreach {$_.Keys}
   $aliases | foreach {
      $PRIVATE:extra+= $_ + "\b|"
   }

   $pattern = get-command -Module library | 
      ForEach-Object -begin   {$pattern = $extra} `
                     -process {$pattern += $_.Name + "\b|"} `
                     -end     {$pattern -replace "\|$", ""}
                 
   if (Test-Path .\transcript.txt) {
      Select-String -pattern "^PS" -path .\transcript.txt | select-object line | 
         Select-String -pattern $pattern -allMatches |
            ForEach-Object -process { $_.Matches } |
               Group-Object value -NoElement |
                  Sort-Object count -Descending
   } else {
      Write-Warning "File 'transcript.txt' not found. Use 'Start-Transcript .\transcript.txt' to create."
   }
      
}

function Get-ZIPfiles {
<#
.SYNOPSIS
Search for (filename) strings inside compressed ZIP or RAR files (V2.8).
.DESCRIPTION
In any directory containing a large number of ZIP/RAR compressed files this procedure
will search each individual file name for simple text strings, listing both the source
RAR/ZIP file and the individual file name containing the string. The relevant RAR/ZIP
can then be subsequently opened in the usual way. PS V3 will now use the OK button to
open the selected folder with WinRAR. The parameters -Table or -Gridview may be used
to produce any output in tabular format.
.EXAMPLE
extract -path d:\scripts -find 'library'
 
Searching for 'library'... (Use CTRL+C to quit)
[Editor.zip] My Early Life In The School Library.html
[Editor.rar] Using Library procedures in Win 7.mht
[Test2.rar] Playlists from library - Windows 7 Forums.mht
[Test3.rar] Module library functions UserGroup.pdf
Folder 'D:\Scripts' contains 4 matches for 'library' in 4 file(s).
.EXAMPLE
extract pdf desk
 
Searching for 'pdf' - please wait... (Use CTRL+C to quit)
[Test1.rar] VirtualBox_ Guest Additions package.pdf
[Test2.rar] W8 How to Install Windows 8 in VirtualBox.pdf
[Test2.rar] W8 Install Windows 8 As a VM with VirtualBox.pdf
Folder 'C:\Users\Sam\desktop' contains 3 matches for 'pdf' in 2 file(s).
 
This example uses the 'extract' alias to find all 'pdf' files on the desktop.
.NOTES
The first step will find any lines containing the selected pattern (which can
be anywhere in the line). Each of these lines will then be split into 2
headings: Source and Filename.
.LINK
 Http://www.SeaStar.co.nf
#>

   [CmdletBinding()]
   param([string][Parameter(Mandatory=$true)]$find,
         [string][ValidateNotNullOrEmpty()]$path = $pwd,
         [switch][alias("GRIDVIEW")]$table)

   Set-StrictMode -Version 2
   switch -wildcard ($path) {
      'desk*' { 
         $path = Join-Path $home 'desktop\*' ; break
      }
      'doc*' {
         $docs = [environment]::GetFolderPath("mydocuments") 
         $path = Join-Path $docs '*'; break
      }
      default { 
         $xpath = Join-Path $path '*' -ea 0
         if (!($?) -or !(Test-Path $path)) {
            Write-Warning "Path '$path' is invalid - resubmit"
            return
         }
         $path = $xpath
      }
   }

   Get-ChildItem $path -include '*.rar','*.zip' |
      Select-String -SimpleMatch -Pattern $find |
         foreach-Object `
            -begin {
                [int]$count = 0
                $container = @{}
                $lines = @{}
                $regex = '(?s)^(?<zip>.+?\.(?:zip|rar)):(?:\d+):.*(\\|/)(?<file>.*\.(mht|html?|pdf))(.*)$' 
                Write-Host "Searching for '$find' - please wait... (Use CTRL+C to quit)"
            } `
            -process {
                if ( $_ -match $regex ) {
                   $container[$matches.zip] +=1      #Record the number in each.
                   $source = Split-Path $matches.zip -Leaf 
                   $file   = $matches.file
                   $file = $file -replace '\p{S}|\p{Cc}',' '   #Some 'Dingbats'.
                   $file = $file -replace '\s+',' '         #Single space words.
                   if ($table) {
                      $key = "{0:D4}" -f $count
                      $lines["$key $source"] = $file       #Create a unique key.
                   }
                   else {
                      Write-Host "[$source] $file"
                   }
                   $count++ 
                }
            } `
            -end { 
                $total = "in $($container.count) file(s)." 
                $title = "Folder '$($path.Replace('\*',''))' contains $($count) matches for '$find' $total"
                if ($table -and $count -gt 0) {        
                   $lines.GetEnumerator() |  
                      Select-Object @{name = 'Source';expression = {$_.Key.SubString(5)}},
                                    @{name = 'Match' ;expression = {$_.Value}} |
                         Sort-Object Match |
                            Out-GridView -Title $title -outputMode single | % {invoke-item  "$($_.Source)"}
                }
                else {
                   if ($count -eq 0) {
                      $title = "Folder '$($path.Replace('\*',''))' contains no matches for '$find'."   
                   }
                   Write-Host $title
                }
            } 
} #End function.

function Use-Culture {
<#
.SYNOPSIS
Use different cultures for commands.
.DESCRIPTION
Submit the required culture, such as fr-FR, together with any scriptblock
containing the commands to be run. The default culture will be restored after
the command completes.
Get-Help Use-Culture -Online will produce a usable table of locale options.
.EXAMPLE
PS >Use-Culture es-ES {Get-Date}
miércoles, 06 de noviembre de 2013 6:58:05
 
Shows the current date in Spanish. Entered without any parameters, ie Use-Culture,
will show the current date in Danish.
.EXAMPLE
PS >Use-Culture vi-VN {Get-Date}
17 Tha´ng Muo'i Mô?t 2017 9:17:32 CH
 
Shows the current date in Vietnamese. Any untranslatable characters are shown as '?'.
.NOTES
This is an example from the book 'Windows PowerShell Cookbook' by Lee Holmes,
page 220.
.LINK
 https://msdn.microsoft.com/en-us/library/ee825488(v=cs.20).aspx
#>

    param( 
    [System.Globalization.CultureInfo]$culture = 'da-DK',
    [ScriptBlock]$script= {Get-Date})
 
    function Set-Culture([System.Globalization.CultureInfo]$culture) {
        [System.Threading.Thread]::CurrentThread.CurrentUICulture = $culture
        [System.Threading.Thread]::CurrentThread.CurrentCulture   = $culture
    }
    $oldCulture = [System.Threading.Thread]::CurrentThread.CurrentCulture
    trap { Set-Culture $oldCulture  } #Restore original culture if script has errors.
    [System.Threading.Thread]::CurrentThread.CurrentCulture = $culture
    Set-Culture $culture 

    Invoke-Command $script
    Set-Culture $OldCulture #Restore original culture information.
} #End function Use-Culture.

 
function AddTime ([String]$hhmmss) {
<#
.SYNOPSIS
Add times together in the format 00:00:00, ie 'hh:mm:ss' and return the result.
.DESCRIPTION
There are many occasions when this cannot be done with a standard Timespan command,
for example when a total greater than 24 hours is involved. A subtraction can also
be done with the SubtractTime function; and the total result can be reset to zero
with the ClearTime function
.EXAMPLE
AddTime 23:00:00 will initially return '23:00:00' and then AddTime 02:10:12 will
return '25:10:12'. SubtractTime 10:20:12 will return 14:50:00. Negative results can
appear if the result is less than 00:00:00 in the form '-02:12:59'.
.EXAMPLE
PS >AddTime 13:45:00
Result: 13:45:00
PS >AddTime 06:10:45
Result: 19:55:45
.NOTES
Used in the Get-Modem module to add internet Connect and Disconnect event
times from the Internet Explorer Event Log.
.LINK
http://www.SeaStar.co.nf
#>

   $PRIVATE:PrivateData = $MyInvocation.MyCommand.Module.PrivateData
   [Int]$h = $MyInvocation.MyCommand.Module.PrivateData['h']
   [Int]$m = $MyInvocation.MyCommand.Module.PrivateData['m']
   [Int]$s = $MyInvocation.MyCommand.Module.PrivateData['s']
      if ($hhmmss -match '^\d+:[0-5][0-9]:[0-5][0-9]$') {
         $plus = $hhmmss.Split(':')
         $h+= $plus[0]
         $m+= $plus[1]
         $s+= $plus[2]
         if ($s -gt 59) {
            [Int]$min = $s/60
            [Int]$sec = $s%60
            $m+= $min
            $s = $sec
         }
         if ($m -gt 59) {
            [Int]$hour = $m/60
            [Int]$min  = $m%60
            $h+= $hour
            $m = $min
         }                   #Now update global values before exit.
         $MyInvocation.MyCommand.Module.PrivateData['h'] = $h
         $MyInvocation.MyCommand.Module.PrivateData['m'] = $m
         $MyInvocation.MyCommand.Module.PrivateData['s'] = $s
         $clock = "{0:D3}:{1:D2}:{2:D2}" -f $h, $m, $s
         $content = '^(-?)(0)(\d+:\d\d:\d\d)$' 
         $clock = $clock -replace $content, '$1$3' 
         Write-Host "Result: $clock"
      }
   } #End function AddTime

function SubtractTime ([String]$hhmmss) {
   $PRIVATE:PrivateData = $MyInvocation.MyCommand.Module.PrivateData
   [Int]$h = $MyInvocation.MyCommand.Module.PrivateData['h']
   [Int]$m = $MyInvocation.MyCommand.Module.PrivateData['m']
   [Int]$s = $MyInvocation.MyCommand.Module.PrivateData['s']
   if ($hhmmss -match '^\d+:[0-5][0-9]:[0-5][0-9]$') {
      $minus = $hhmmss.Split(':')
      if (($s - [Int]$minus[2]) -lt 0) {   
         $s+= (60 - [Int]$minus[2])
         [Int]$minus[1]+= 1
      } else {
         $s = $s - [Int]$minus[2]
      }
      if (($m - [Int]$minus[1]) -lt 0) { 
         $m+= (60 - [Int]$minus[1])
         [Int]$minus[0]+= 1
      } else {
         $m = $m - [Int]$minus[1]
      }
      $h = $h - $minus[0] #Below we update global values before exit.
      $MyInvocation.MyCommand.Module.PrivateData['h'] = $h
      $MyInvocation.MyCommand.Module.PrivateData['m'] = $m
      $MyInvocation.MyCommand.Module.PrivateData['s'] = $s
      $clock = "{0:D3}:{1:D2}:{2:D2}" -f $h, $m, $s
      $content = '^(-?)(0)(\d+:\d\d:\d\d)$'
      $clock = $clock -replace $content,'$1$3'
      Write-Host "Result: $clock"
   }
}  #End function SubtractTime

function ClearTime {
   $MyInvocation.MyCommand.Module.PrivateData['h'] = 0
   $MyInvocation.MyCommand.Module.PrivateData['m'] = 0
   $MyInvocation.MyCommand.Module.PrivateData['s'] = 0
}

Set-Variable -Name regexDouble  -value '\b(\w+)((?:\s|<[^>]+>)+)(\1\b)' `
             -Description 'Find double words'
Set-Variable -Name regexNumbers -value '\b([0-9]+)\b' `
             -Description 'Find only numbers in a string'
Set-Variable -Name regexMonth   -value '[JFAMSOND](?# Lookbehind)(?:(?<=J)an|(?<=F)eb|(?<=M)a(r|y)|`
                                         (?<=A)pr|(?<=M)ay|(?<=J)u(n|l)|(?<=A)ug|(?<=S)ep|`
                                         (?<=O)ct|(?<=N)ov|(?<=D)ec)'
 `
             -Description 'Short months but case sensitive.'

New-Alias db Debug-Regex        -Description 'Test Regex expressions'
New-Alias ff Get-Function       -Description 'Find all functions in scripts'
New-Alias summary Show-Usage    -Description 'List all console commands'
New-Alias extract Get-ZIPfiles  -Description 'Find files inside ZIP/RAR'
New-Alias add AddTime        -Description 'Add hh:mm:ss to total time'
New-Alias sub SubtractTime    -Description 'Subtract hh:mm:ss from total time'
New-Alias reset ClearTime    -Description 'Reset total time to zero'
New-Alias uc Use-Culture    -Description 'Use different cultures for commands'
New-Alias gh Get-SessionIDs    -Description 'Show the last 100 console commands'

Export-ModuleMember -Function Get-SessionIDs, Get-Function, Run, killx, backup, GH, Debug-Regex, Show-Usage, `
    Get-ZIPfiles, Use-Culture, AddTime, SubtractTime, ClearTime -Variable regex* -Alias gh, uc, db, ff, summary, extract, `
        add, sub, reset