Library.psm1

$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
   Write-Host "Module Library.psm1 removed on $(Get-Date)"
}
$MyInvocation.MyCommand.ScriptBlock.Module.Description = "Contains functions Run, Backup, killx, Show-Usage, Extract, Debug-Regex"
$PRIVATE:PrivateData = $MyInvocation.MyCommand.Module.PrivateData  #Available to all functions below.
function Run([String]$scriptName = '-BLANK-') {
<#
.SYNOPSIS
Record any scripts that are run 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.
#>

    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\library ps1,psm1,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 GH {
   $date = "{0:F}" -f [DateTime]::Now
   Get-History -Count 100 | Sort commandLine -Unique | sort id | 
      Select @{name='Execution Time';expression={$_.EndExecutionTime}}, `
             @{name='Id';expression={$_.Id}}, CommandLine |
         Out-GridView -Title "$date Session Command History" -outputMode Single | 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'.
.NOTES
Firstly set any names not in Library module ('-begin'). The '$pattern' will get
all the commands, including aliases.
#>

   $pattern = get-command -Module library | 
      ForEach-Object -begin   {$pattern = "gprs\b|Backup-ToUSB2\b|Get-GprsTime\b|"} `
                     -process {$pattern += $_.Name + "\b|"} `
                     -end     {$pattern -replace "\|$", ""}
                 
   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
      
}

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.
.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 ( 
    [String][ValidatePattern('^[a-z]{2}(-[A-Z]{2})?$',Options='IgnoreCase')]$culture = 'en-US',
    [ScriptBlock]$script= {Get-Winevent -Logname Microsoft-Windows-TaskScheduler/Operational `
      -MaxEvents 100 | Where {$_.Message -like "*fail*"} | fl message, TimeCreated} ) {
<#
.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.
.EXAMPLE
Use-Culture es-ES {Get-Date}
Shows the current date in Spanish.
 
miércoles, 06 de noviembre de 2013 6:58:05
.EXAMPLE
$scriptblock = {Get-Winevent -Logname Microsoft-Windows-TaskScheduler/Operational
      -MaxEvents 100 | Where {$_.Message -like "*fail*"} | fl message, TimeCreated}
 
Use-Culture en-US $scriptblock
 
Message : Task Scheduler failed to start "\GoogleUpdateTaskMachineUA" task
              for user "NT AUTHORITY\System". Additional Data: Error Value:
              2147750687.
TimeCreated : 11/6/2013 6:44:51 AM
   (...)
.NOTES
This is an example from the book 'Windows PowerShell Cookbook' by Lee Holmes,
page 220.
#>

    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'

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