FindIn.psm1

function findin {# Find strings in file patterns matching a regex pattern, recursively.
param([string]$filePattern, [string]$script:string, [switch]$recurse, [switch]$quiet, [switch]$countonly, [switch]$summary, [switch]$long, [switch]$load, [switch]$header, [int]$characters = 500, [switch]$viewer, [switch]$help, [switch]$modulehelp)

if ($modulehelp) {# Inline help.
# Modify fields sent to it with proper word wrapping.
function wordwrap ($field, $maximumlinelength) {if ($null -eq $field -or $field.Length -eq 0) {return $null}
$breakchars = ',.;?!\/ '; $wrapped = @()

if (-not $maximumlinelength) {[int]$maximumlinelength = (100, $Host.UI.RawUI.WindowSize.Width | Measure-Object -Maximum).Maximum}
if ($maximumlinelength) {if ($maximumlinelength -lt 60) {[int]$maximumlinelength = 60}
if ($maximumlinelength -gt $Host.UI.RawUI.BufferSize.Width) {[int]$maximumlinelength = $Host.UI.RawUI.BufferSize.Width}}

foreach ($line in $field -split "`n") {if ($line.Trim().Length -eq 0) {$wrapped += ''; continue}
$remaining = $line.Trim()
while ($remaining.Length -gt $maximumlinelength) {$segment = $remaining.Substring(0, $maximumlinelength); $breakIndex = -1

foreach ($char in $breakchars.ToCharArray()) {$index = $segment.LastIndexOf($char)
if ($index -gt $breakIndex) {$breakChar = $char; $breakIndex = $index}}
if ($breakIndex -lt 0) {$breakIndex = $maximumlinelength - 1; $breakChar = ''}
$chunk = $segment.Substring(0, $breakIndex + 1).TrimEnd(); $wrapped += $chunk; $remaining = $remaining.Substring($breakIndex + 1).TrimStart()}

if ($remaining.Length -gt 0) {$wrapped += $remaining}}
return ($wrapped -join "`n")}

function scripthelp ($section) {# (Internal) Generate the help sections from the comments section of the script.
""; Write-Host -f yellow ("-" * 100); $pattern = "(?ims)^## ($section.*?)(##|\z)"; $match = [regex]::Match($scripthelp, $pattern); $lines = $match.Groups[1].Value.TrimEnd() -split "`r?`n", 2; Write-Host $lines[0] -f yellow; Write-Host -f yellow ("-" * 100)
if ($lines.Count -gt 1) {wordwrap $lines[1] 100| Out-String | Out-Host -Paging}; Write-Host -f yellow ("-" * 100)}
$scripthelp = Get-Content -Raw -Path $PSCommandPath; $sections = [regex]::Matches($scripthelp, "(?im)^## (.+?)(?=\r?\n)")
if ($sections.Count -eq 1) {cls; Write-Host "$([System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath)) Help:" -f cyan; scripthelp $sections[0].Groups[1].Value; ""; return}

$selection = $null
do {cls; Write-Host "$([System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath)) Help Sections:`n" -f cyan; for ($i = 0; $i -lt $sections.Count; $i++) {
"{0}: {1}" -f ($i + 1), $sections[$i].Groups[1].Value}
if ($selection) {scripthelp $sections[$selection - 1].Groups[1].Value}
$input = Read-Host "`nEnter a section number to view"
if ($input -match '^\d+$') {$index = [int]$input
if ($index -ge 1 -and $index -le $sections.Count) {$selection = $index}
else {$selection = $null}} else {""; return}}
while ($true); return}

if (-not $load -and -not $help -and -not $filepattern) {$help = $true}

# Use one of the saved search patterns.
if ($load) {$findinFile = "$PSScriptRoot\findin.txt"
if (-not (Test-Path $findinFile)) {Write-Host -f red "`nError: Saved search file 'find-in.txt' not found.`n"; return}
$savedSearches = Get-Content $findinFile | Where-Object {$_ -match "^\s*'(.+?)'\s+'(.+)'$"} | ForEach-Object {$matchName = $matches[1]; $matchPattern = $matches[2]; [PSCustomObject]@{Name = $matchName; Pattern = $matchPattern}}
if (-not $savedSearches) {Write-Host -f yellow "`nWarning: No valid searches found in 'find-in.txt'.`n"; return}
Write-Host -f yellow "`nSaved Searches:`n"
for ($i=0; $i -lt $savedSearches.Count; $i++) {Write-Host -f cyan "$($i+1). $($savedSearches[$i].Name)"}
$selection = Read-Host "`nEnter the number of the search to run"
if ($selection -match '^\d+$') {$selection = [int]$selection
if ($selection -gt 0 -and $selection -le $savedSearches.Count) {$script:string = $savedSearches[$selection - 1].Pattern; Write-Host -f green "`nLoaded pattern: " -n; Write-Host -f white $script:string}
else {Write-Host -f red "`nInvalid selection. Exiting.`n"; return}}
else {Write-Host -f red "`nInvalid selection. Exiting.`n"; return}}

# List the saved search patterns.
if ($list) {$findinFile = "$PSScriptRoot\find-in.txt"
if (-not (Test-Path $findinFile)) {Write-Host -f red "`nError: Saved search file 'find-in.txt' not found.`n"; return}
$savedSearches = Get-Content $findinFile | Where-Object {$_ -match "^\s*'(.+?)'\s+'(.+)'$"} | ForEach-Object {[PSCustomObject]@{Name = $matches[1]; Pattern = $matches[2]}}
if (-not $savedSearches) {Write-Host -f yellow "`nWarning: No valid searches found in 'find-in.txt'.`n"; ""; return}
Write-Host -f yellow "`nSaved Searches:"
foreach ($search in $savedSearches) {Write-Host -f cyan ($search.Name + ":").PadRight(30) -n; Write-Host -f white $search.Pattern}; ""; return}

# Add a new saved search pattern.
if ($add) {$findinFile = "$PSScriptRoot\find-in.txt"
$name = Read-Host "Enter a name for the search"; $pattern = Read-Host "Enter the regex pattern"
if (-not $name -or -not $pattern) {Write-Host -f red "`nError: Both name and pattern are required.`n"; return}
$safeName = $name -replace "'", "''"; $safePattern = $pattern -replace "'", "''"
"'$safeName' '$safePattern'" | Add-Content -Encoding UTF8 -Path $findinFile
Write-Host -f green "`nSaved '$name' to find-in.txt.`n"; return}

# Remove a saved search pattern.
if ($remove) {$findinFile = "$PSScriptRoot\find-in.txt"
if (-not (Test-Path $findinFile)) {Write-Host -f red "`nError: Saved search file 'find-in.txt' not found.`n"; return}
$lines = [System.Collections.Generic.List[string]]@(Get-Content $findinFile); $entries = $lines | Where-Object {$_ -match "^\s*'(.+?)'\s+'(.+)'$"} | ForEach-Object {[PSCustomObject]@{Name = $matches[1]; Pattern = $matches[2]}}
if (-not $entries) {Write-Host -f yellow "`nWarning: No valid searches found in 'find-in.txt'.`n"; return}
Write-Host -f yellow "`nSaved Searches:`n"; for ($i = 0; $i -lt $entries.Count; $i++) {Write-Host -f cyan "$($i+1). $($entries[$i].Name.PadRight(30))" -n; Write-Host -f white $entries[$i].Pattern}
$choice = Read-Host "`nEnter the number of the entry to remove"
if ($choice -match '^\d+$' -and $choice -ge 1 -and $choice -le $entries.Count) {$index = 0; $lineIndexToRemove = -1
foreach ($line in $lines) {if ($line -match "^\s*'(.+?)'\s+'(.+)'$") {if ($index -eq ($choice - 1)) {$lineIndexToRemove = [array]::IndexOf($lines, $line); break}; $index++}}
if ($lineIndexToRemove -ge 0) {$lines.RemoveAt($lineIndexToRemove); Set-Content -Path $findinFile -Value $lines -Encoding UTF8; Write-Host -f green "`nRemoved '$($entries[$choice - 1].Name)' from find-in.txt.`n"}
else {Write-Host -f red "`nError: Could not find the exact line to remove.`n"}}
else {Write-Host -f red "`nInvalid selection. Exiting.`n"}; return}

# Display the help screen.
if ($help) {Write-Host -f white "`nUsage: " -n; Write-Host -f yellow "findin `"Regex file pattern`" `"Regex string pattern`" -recurse -quiet -countonly -long -summary -load -list -add -remove -help`n"
Write-Host -f yellow "-recurse".PadRight(11) -n; Write-Host -f white " to look recursively through the directory structure"
Write-Host -f yellow "-quiet".PadRight(11) -n; Write-Host -f white " to suppress the messages for files where no matching pattern was found"
Write-Host -f yellow "-header ##".PadRight(11) -n; Write-Host -f white " to view the first ## (defaults to 500) characters of the file, when a match is found"
Write-Host -f yellow "-countonly".PadRight(11) -n; Write-Host -f white " to provide the numeric results of matches found, but suppress the contextual matches found"
Write-Host -f yellow "-long".PadRight(11) -n; Write-Host -f white " to provide an 80 character prefix and suffix for contextual matching, instead of 40"
Write-Host -f yellow "-summary".PadRight(11) -n; Write-Host -f white " to provide a numerical summary"
Write-Host -f yellow "-load".PadRight(11) -n; Write-Host -f white " to load a regex string from the saved options in the find-in.txt file"
Write-Host -f yellow "-add".PadRight(11) -n; Write-Host -f white " to save a new Regex pattern to the find-in.txt file"
Write-Host -f yellow "-remove".PadRight(11) -n; Write-Host -f white " to remove a Regex pattern from the find-in.txt file"
Write-Host -f yellow "-viewer".PadRight(11) -n; Write-Host -f white " to pass the files with matches to the internal artifact viewer interface, retaining the search terms"
Write-Host -f yellow "-help".PadRight(11) -n; Write-Host -f white " to display this screen"
Write-Host -f yellow "-modulehelp".PadRight(11) -n; Write-Host -f white " to display a helpscreen about the entire module`n"; return}

$base=Split-Path $filePattern -Parent; if (!$base) {$base="."; $filePattern=(Split-Path $filePattern -Leaf)}; $files=Get-ChildItem -Path $base -File -Recurse:($recurse.IsPresent) -ErrorAction SilentlyContinue | Where-Object {$_.Name -match $filePattern}; $totalMatches=0; $filesChecked=0; $context=if ($long) {80} else {40}; ""

# Display the no matching file pattern error.
if ($files.Count -eq 0) {Write-Host -f red "`nNo files match pattern '$filePattern'.`n"} 

# Begin searching through each file.
else {$matchedFiles = @(); $script:gzLineCounter = 0; foreach ($file in $files) {$filesChecked++

# Handle GZip files.
if ($file.Extension -eq ".gz") {try {$stream = [System.IO.File]::OpenRead($file.FullName); $gzip = New-Object System.IO.Compression.GzipStream($stream, [System.IO.Compression.CompressionMode]::Decompress); $reader = New-Object System.IO.StreamReader($gzip); $content = $reader.ReadToEnd(); $reader.Close(); $gzip.Close(); $stream.Close()
$matchesFound = $content -split "`r?`n" | ForEach-Object {$line = $_; $lineNum = $null
if ($line -match "(?i)$script:string") {$lineNum = ++$script:gzLineCounter; $mockMatch = [PSCustomObject]@{LineNumber = $lineNum; Line = $line; Matches = [regex]::Matches($line, "(?i)$script:string")}
return $mockMatch}} | Where-Object {$_ -ne $null}}
catch {Write-Host -f red "Failed to read: $($file.FullName)"}}

# Handle plaintext files.
else {$matchesFound = Select-String -Path $file.FullName -Pattern "(?i)$script:string" -AllMatches}

# Create an array of matching files
if ($matchesFound) {$matchedFiles += $file.FullName}

# Indicate when there are no matches found in file, but suppress this message if the -quiet switch is used.
if ($matchesFound.Count -eq 0) {if (!$quiet) {Write-Host -f gray "No matches in $($file.FullName)."}} 

# Display only the numeric results when the -countonly switch is used.
else {$totalMatches+=$matchesFound.Count; if ($countonly) {Write-Host -f cyan $file.FullName -n; Write-Host -f green ": $($matchesFound.Count) match(es)"} 

# Display all matches for the file accordingly, adding truncation characters if appropriate.
else {Write-Host -f yellow ("-"*100); Write-Host -f yellow "File: " -n; Write-Host -f cyan $file.FullName; Write-Host -f yellow ("-"*100)

# Display file header if requested.
if ($header) {Write-Host -f yellow "File header, $characters characters:`n"; (Get-Content $file.FullName -Raw).Substring(0,$characters); Write-Host -f yellow ("-"*100); ""}

$matchesFound | ForEach-Object {$lineNumber=$_.LineNumber; $line=$_.Line; $matches=$_.Matches; foreach ($match in $matches) {$matchIndex=$match.Index; $matchLength=$match.Length; $start=[Math]::Max(0,$matchIndex-$context); $preMatch=$line.Substring($start,$matchIndex-$start); $matchText=$line.Substring($matchIndex,$matchLength); $postStart=$matchIndex+$matchLength; $postLength=[Math]::Min($context,$line.Length-$postStart); $postMatch=$line.Substring($postStart,$postLength); <# truncation section #> $prefix=if ($start -gt 0) {"..."} else {""}; $suffix=if (($postStart+$postLength) -lt $line.Length) {"..."} else {""}; <# end of truncation section #> Write-Host -f cyan "${lineNumber}, ${matchIndex}: " -n; Write-Host -f white "$prefix$preMatch" -n; Write-Host -f black -b yellow "$matchText" -n; Write-Host -f white "$postMatch$suffix"}}
Write-Host -f green "`n$($matchesFound.Count) match(es) found."}}}

# Provide final totals when the -summary switch is used.
if ($summary) {Write-Host -f yellow ("-"*100); Write-Host -f green "Summary: $totalMatches match(es) in $($matchedfiles.count) of $filesChecked file(s)."}; ""}

# Output array.
if ($viewer) {Write-Host -f yellow "Are you ready to open the ArtifactViewer to investigate these files? (Y/N)" -n; $ready = Read-Host " "
if ($ready -match "(?i)Y") {artifactviewer -filearray $matchedFiles}
else {Write-Host -f red "Cancelled.`n"}}}

function artifactviewer ([string[]]$filearray) {# ArtifactViewer.
""; $script:viewfile = $viewfile; $script:viewfilearray = $filearray; 

# File array selection menu
function filemenu_virtual ($script:viewfilearray) {$page = 0; $perpage = 30; $script:viewfile = $null; $errormessage = $null
while ($true) {cls; $input = $null; $entryIndex = $null; $sel = $null
Write-Host -f cyan "Search Results (Page $($page + 1))`n"; $startIndex = $page * $perpage; $endIndex = [Math]::Min(($page + 1) * $perpage - 1, $script:viewfilearray.Count - 1); $paged = $script:viewfilearray[$startIndex..$endIndex]; $optionCount = 0
for ($i = 0; $i -lt $paged.Count; $i++) {$optionCount++; $name = Split-Path -Leaf $paged[$i]; Write-Host -f white "$optionCount. $name" -n; $sizeKB = try {[math]::Round(((Get-Item $paged[$i]).Length + 500) / 1KB, 0)} catch {" "}; Write-Host -f white " [$sizeKB KB]"}
if (($page + 1) * $perpage -lt $script:viewfilearray.Count) {$optionCount++; Write-Host "$optionCount. NEXT..." -f Cyan}
Write-Host -f red "`n$errormessage"; Write-Host -f White "Make a selection or press Enter" -n; $input = Read-Host " "
if (-not $input) {return}
if ($input -match '^\d+$') {$sel = [int]$input; $entryIndex = $sel - 1
if ($entryIndex -ge 0 -and $entryIndex -lt $paged.Count) {$script:viewfile = $paged[$entryIndex]; return}
elseif ($sel -eq $optionCount -and ($page + 1) * $perpage -lt $script:viewfilearray.Count) {$page++} else {$errormessage = "Invalid selection."}}
else {$errormessage = "Invalid input."}}}

filemenu_virtual $script:viewfilearray

# Error-checking
if (-not (Test-Path $script:viewfile -PathType Leaf -ErrorAction SilentlyContinue) -or (-not $script:viewfile)) {Write-Host -f red "`nNo file provided.`n"; return}
if (-not (Test-Path $script:viewfile)) {Write-Host -f red "`nFile not found.`n"; return}

# Read GZip files.
if ($script:viewfile -like "*.gz") {try {$stream = [System.IO.File]::OpenRead($script:viewfile); $gzip = New-Object System.IO.Compression.GzipStream($stream, [System.IO.Compression.CompressionMode]::Decompress); $reader = New-Object System.IO.StreamReader($gzip); $rawText = $reader.ReadToEnd(); $reader.Close(); $gzip.Close(); $stream.Close(); $content = $rawText -split "`r?`n"}
catch {Write-Host -f red "`nFailed to read compressed file: $script:viewfile`n"; return}}

# Read plaintext files.
else {$content = Get-Content $script:viewfile}

if (-not $content) {Write-Host -f red "`nFile is empty.`n"; return}

$searchTerm = $script:string; $pattern = "(?i)$script:string"; $searchHits = @(0..($content.Count - 1) | Where-Object {$content[$_] -match $pattern}); $currentSearchIndex = $searchHits[0]; $pos = 0
$separators = @(0) + (0..($content.Count - 1) | Where-Object {$content[$_] -match '^[=]{100}$'}); $pageSize = 35; $script:viewfileName = [System.IO.Path]::GetFileName($script:viewfile)

function getbreakpoint {param($start), $maxEnd = [Math]::Min($start + $pageSize - 1, $content.Count - 1); for ($i = $start + 29; $i -le $maxEnd; $i++) {if ($content[$i] -match '^[-=]{100}$') {return $i}}; return $maxEnd}

function showpage {cls; $start = $pos; $end = getbreakpoint $start; $pageLines = $content[$start..$end]; $highlight = if ($searchTerm) {$pattern} else {$null}
foreach ($line in $pageLines) {if ($line -match '^[-=]{100}$') {Write-Host -f Yellow $line}
elseif ($highlight -and $line -match $highlight) {$parts = [regex]::Split($line, "($highlight)")
foreach ($part in $parts) {if ($part -match "^$highlight$") {Write-Host -f black -b yellow $part -n}
else {Write-Host -f white $part -n}}; ""}
else {Write-Host -f white $line}}

# Pad with blank lines if this page has fewer than $pageSize lines
$linesShown = $end - $start + 1
if ($linesShown -lt $pageSize) {for ($i = 1; $i -le ($pageSize - $linesShown); $i++) {Write-Host ""}}}

# Main menu loop
$statusmessage = ""; $errormessage = ""; $searchmessage = "Search Commands"
while ($true) {showpage; $pageNum = [math]::Floor($pos / $pageSize) + 1; $totalPages = [math]::Ceiling($content.Count / $pageSize)
if ($searchHits.Count -gt 0) {$currentMatch = [array]::IndexOf($searchHits, $pos); if ($currentMatch -ge 0) {$searchmessage = "Match $($currentMatch + 1) of $($searchHits.Count)"}
else {$searchmessage = "Search active ($($searchHits.Count) matches)"}}
Write-Host ""; Write-Host -f yellow ("=" * 120)
if (-not $errormessage -or $errormessage.length -lt 1) {$middlecolour = "white"; $middle = $statusmessage} else {$middlecolour = "red"; $middle = $errormessage}
$left = "$script:fileName".PadRight(57); $middle = "$middle".PadRight(44); $right = "(Page $pageNum of $totalPages)"
Write-Host -f white $left -n; Write-Host -f $middlecolour $middle -n; Write-Host -f cyan $right
$left = "Page Commands".PadRight(55); $middle = "| $searchmessage ".PadRight(34); $right = "| Exit Commands"
Write-Host -f yellow ($left + $middle + $right)
Write-Host -f yellow "[F]irst [N]ext [+/-]# Lines p[A]ge # [P]revious [L]ast | [<][S]earch[>] [#]Match [C]lear | [D]ump [X]Edit [M]enu [Q]uit " -n
$statusmessage = ""; $errormessage = ""; $searchmessage = "Search Commands"

function getaction {[string]$buffer = ""
while ($true) {$key = [System.Console]::ReadKey($true)
switch ($key.Key) {'LeftArrow' {return 'P'}
'UpArrow' {return 'P'}
'Backspace' {return 'P'}
'PageUp' {return 'P'}
'RightArrow' {return 'N'}
'DownArrow' {return 'N'}
'PageDown' {return 'N'}
'Enter' {if ($buffer) {return $buffer}
else {return 'N'}}
'Home' {return 'F'}
'End' {return 'L'}
default {$char = $key.KeyChar
switch ($char) {',' {return '<'}
'.' {return '>'}
{$_ -match '(?i)[B-Z]'} {return $char.ToString().ToUpper()}
{$_ -match '[A#\+\-\d]'} {$buffer += $char}
default {$buffer = ""}}}}}}

$action = getaction

switch ($action.ToString().ToUpper()) {'F' {$pos = 0}
'N' {$next = getbreakpoint $pos; if ($next -lt $content.Count - 1) {$pos = $next + 1}
else {$pos = [Math]::Min($pos + $pageSize, $content.Count - 1)}}
'P' {$pos = [Math]::Max(0, $pos - $pageSize)}
'L' {$lastPageStart = [Math]::Max(0, [int][Math]::Floor(($content.Count - 1) / $pageSize) * $pageSize); $pos = $lastPageStart}

'<' {$currentSearchIndex = ($searchHits | Where-Object {$_ -lt $pos} | Select-Object -Last 1)
if ($null -eq $currentSearchIndex -and $searchHits -ne @()) {$currentSearchIndex = $searchHits[-1]; $statusmessage = "Wrapped to last match."; $errormessage = $null}
$pos = $currentSearchIndex
if (-not $searchHits -or $searchHits.Count -eq 0) {$errormessage = "No search in progress."; $statusmessage = $null}}
'S' {Write-Host -f green "`n`nKeyword to search forward from this point in the logs" -n; $searchTerm = Read-Host " "
if (-not $searchTerm) {$errormessage = "No keyword entered."; $statusmessage = $null; $searchTerm = $null; $searchHits = @(); continue}
$pattern = "(?i)$searchTerm"; $searchHits = @(0..($content.Count - 1) | Where-Object { $content[$_] -match $pattern })
if ($searchHits.Count -eq 0) {$errormessage = "Keyword not found in file."; $statusmessage = $null; $currentSearchIndex = -1}
else {$currentSearchIndex = $searchHits | Where-Object { $_ -gt $pos } | Select-Object -First 1
if ($null -eq $currentSearchIndex) {Write-Host -f green "No match found after this point. Jump to first match? (Y/N)" -n; $wrap = Read-Host " "
if ($wrap -match '^[Yy]$') {$currentSearchIndex = $searchHits[0]; $statusmessage = "Wrapped to first match."; $errormessage = $null}
else {$errormessage = "Keyword not found further forward."; $statusmessage = $null; $searchHits = @(); $searchTerm = $null}}
$pos = $currentSearchIndex}}
'>' {$currentSearchIndex = ($searchHits | Where-Object {$_ -gt $pos} | Select-Object -First 1)
if ($null -eq $currentSearchIndex -and $searchHits -ne @()) {$currentSearchIndex = $searchHits[0]; $statusmessage = "Wrapped to first match."; $errormessage = $null}
$pos = $currentSearchIndex
if (-not $searchHits -or $searchHits.Count -eq 0) {$errormessage = "No search in progress."; $statusmessage = $null}}
'C' {$searchTerm = $null; $searchHits.Count = 0; $searchHits = @(); $currentSearchIndex = $null}

'D' {""; gc $script:file | more; return}
'X' {edit $script:file; "" ; return}
'M' {artifactviewer -filearray $script:viewfilearray; return}
'Q' {"`n"; return}

default {if ($action -match '^[\+\-](\d+)$') {$offset = [int]$action; $newPos = $pos + $offset; $pos = [Math]::Max(0, [Math]::Min($newPos, $content.Count - $pageSize))}

elseif ($action -match '^(\d+)$') {$jump = [int]$matches[1]
if (-not $searchHits -or $searchHits.Count -eq 0) {$errormessage = "No search in progress."; $statusmessage = $null; continue}
$targetIndex = $jump - 1
if ($targetIndex -ge 0 -and $targetIndex -lt $searchHits.Count) {$pos = $searchHits[$targetIndex]
if ($targetIndex -eq 0) {$statusmessage = "Jumped to first match."}
else {$statusmessage = "Jumped to match #$($targetIndex + 1)."}; $errormessage = $null}
else {$errormessage = "Match #$jump is out of range."; $statusmessage = $null}}

elseif ($action -match '^A(\d+)$') {$requestedPage = [int]$matches[1]
if ($requestedPage -lt 1 -or $requestedPage -gt $totalPages) {$errormessage = "Page #$requestedPage is out of range."; $statusmessage = $null}
else {$pos = ($requestedPage - 1) * $pageSize}}

else {$errormessage = "Invalid input."; $statusmessage = $null}}}}}

function getheader ($file,[int]$number = 500) {# Get the header of a file for the specified number of characters
""; if (-not $file) {Write-Host -f cyan "Usage: getheader `"filename`" ##`n"; return}; Write-Host -f yellow ("-"*100); Write-Host -f yellow "`nFile header: $file for $number characters:`n"; (Get-Content $file -Raw).Substring(0,$number); Write-Host -f yellow ("-"*100); ""}
sal -Name header -Value getheader

function getline($file,[int]$linenumber){# Output a specific line number from a file to the screen and copy it to the clipboard.
""; if (-not $file) {Write-Host -f cyan "Usage: getline `"filename`" ##`n"; return};  if (Test-Path $file -ErrorAction SilentlyContinue) {$filearray = gc $file
if ($linenumber -gt $filearray.Count){$lines = $filearray.Count; Write-Host -f green "$file only has $lines lines."}
else {Write-Host -f cyan "$($filearray[$linenumber - 1])"; $filearray[$linenumber - 1] | Set-Clipboard}}
else {Write-Host -f green "$file is not a valid filename."}; ""}

Export-ModuleMember -Function findin, getheader, getline
Export-ModuleMember -Alias header

<#
## artifactviewer
This integrated utility allows you to investigate each of the files with matching criteria inside an interactive file viewer.
Once inside the viewer, the options include:
 
Navigation:
 
    [F]irst page
    [N]ext page
    [+/-]# to move forward or back a specific # of lines
    p[A]ge # to jump to a specific page
    [P]revious page
    [L]ast page
 
Search:
 
    [S]earch for a term
    [<] Previous match
    [>] Next match
    [#]Number to find a specific match number
    [C]lear search term
 
Exit Commands:
 
    [D]ump to screen with | MORE and Exit
    [X]Edit using Notepad++, if available. Otherwise, use Notepad.
    [M]enu to open the file selection menu
    [Q]uit
## findin
 
    Usage: findin "Regex file pattern" "Regex string pattern" -recurse -quiet -countonly -long -summary -load -list -add -remove -help
 
    -recurse to look recursively through the directory structure
    -quiet to suppress the messages for files where no matching pattern was found
    -header # to view the first # (defaults to 500) characters of the file, when a match is found
    -countonly to provide the numeric results of matches found, but suppress the contextual matches found
    -long to provide an 80 character prefix and suffix for contextual matching, instead of 40
    -summary to provide a numerical summary
    -load to load a regex string from the saved options in the find-in.txt file
    -add to save a new Regex pattern to the find-in.txt file
    -remove to remove a Regex pattern from the find-in.txt file
    -viewer to pass the files with matches to the internal artifact viewer interface, retaining the search terms
    -help to display this screen
    -modulehelp to display a helpscreen about the entire module
     
## getheader
 
    Usage: getheader <file> <number of characters to view>
     
The default is set to 500 characters.
# getline
 
    Usage: getline <file> <line number to view>
##>