Import-RegEx.ps1
function Import-RegEx { <# .Synopsis Imports Regular Expressions .Description Imports saved Regular Expressions. .Example Import-RegEx # Imports Regex from Irregular and the current directory. .Example Import-Regex -FromModule AnotherModule # Imports Regular Expressions stored in another module. .Example Import-RegEx -Name NextWord .Link Use-RegEx .Link Write-RegEx #> [OutputType([nullable], [PSObject])] param( # The path to one or more files or folders containing regular expressions. # Files should be named $Name.regex.txt or $Name.regex.ps1 [Parameter(ValueFromPipelineByPropertyName=$true)] [Alias('Fullname')] [string[]]$FilePath, # If provided, will get regular expressions from any number of already imported modules. [string[]] $FromModule, # One or more direct patterns to import [Parameter(ValueFromPipelineByPropertyName=$true)] [string[]] $Pattern, # The Name of the Regular Expression. [Parameter(ValueFromPipelineByPropertyName=$true)] [string[]] $Name, # If set, will output the imported regular expressions. [switch] $PassThru ) begin { # Initialize the library and metadata if (-not $script:_RegexLibrary) { $script:_RegexLibrary = @{}} if (-not $script:_RegexLibraryMetaData) { $script:_RegexLibraryMetaData = @{}} $importInvocation = $MyInvocation # Determine if we're being called from an Import-Module, and, if so, which one. $ModuleCaller = $(foreach ($cs in Get-PSCallStack) { if ($cs.InvocationInfo.MyCommand.Name -notlike '*.psm1') { continue } $cs.InvocationInfo.MyCommand.ScriptBlock.Module break }) # We need to be able to write regexes that use other Regexes, so we need this fancy Regex to find Capture References. $SavedCaptureReferences = [Regex]::new(@' (\(\?\<(?<NewCaptureName>\w+)\>)? (?<!\() # Not preceeded by a ( \?\<(?<CaptureName>\w+)\> # ?<CaptureName> (?<HasArguments> (?: \((?<Arguments> # An open parenthesis (?> # Followed by... [^\(\)]+| # any number of non-parenthesis character OR \((?<Depth>)| # an open parenthesis (in which case increment depth) OR \)(?<-Depth>) # a closed parenthesis (in which case decrement depth) )*(?(Depth)(?!)) # until depth is 0. )\) # followed by a closing parenthesis )| (?: \{(?<Arguments> # An open bracket (?> # Followed by... [^\{\}]+| # any number of non-bracket character OR \{(?<Depth>)| # an open bracket (in which case increment depth) OR \}(?<-Depth>) # a closed bracket (in which case decrement depth) )*(?(Depth)(?!)) # until depth is 0. )\} # followed by a closing bracket ) )? '@, 'IgnoreCase, IgnorePatternWhitespace', '00:00:01') # We'll also need to replaced saved captures as we see them. $replaceSavedCapture = { $m = $args[0] $startsWithCapture = '(?<StartsWithCapture>\A\(\?\<(?<FirstCaptureName>\w+))>' $regex = $script:_RegexLibrary.($m.Groups["CaptureName"].ToString()) if (-not $regex) { return $m } $regex = if ($regex -isnot [Regex]) { if ($m.Groups["Arguments"].Success) { $args = @($m.Groups["Arguments"].ToString() -split '(?<!\\),') & $regex @args } else { & $regex } } else { $regex } if ($m.Groups["NewCaptureName"].Success) { if ($regex -match $startsWithCapture -and $matches.FirstCaptureName -ne $m.Groups['NewCaptureName']) { $repl= $regex -replace $startsWithCapture, "(?<$($m.Groups['NewCaptureName'])>" $repl.Substring(0, $repl.Length - 1) } else { "(?<$($m.Groups['NewCaptureName'].Value)>$regex$([Environment]::NewLine)" } } else { $regex } } # We'll need an internal command to handle importing regexes. $importRegexPattern = { process { $patternIn = $_ $c = 0 $rxLines = @(if ($_ -is [IO.FileInfo]) { # If the regex came in from a file, [IO.File]::ReadLines($_.Fullname) # read each line } elseif ($_ -is [string]) { # Otherwise, split out newlines. $_ -split '(?>\r\n|\n)' } elseif ($_.Pattern) { $_.Pattern -split '(?>\r\n|\n)' }) $name = if ($_ -is [IO.FileInfo]) { # If the regex came from a file if ($_.Directory.Name -ne 'RegEx') { # that wasn't beneath a Regex folder, # Include the parent path $dirPart = ($_.Directory.FullName.Substring($importPath.Length) -replace '(?:\\|/)RegEx(?:\\|/)','') if (-not $dirPart) { $dirPart = $_.Directory.Name } $dirPart + '_' + $_.Name -replace '\.regex\.txt$', '' } else { $_.Name -replace '\.regex\.txt$', '' } } elseif ($_ -is [string] -and $_ -match '(?<StartsWithCapture>\A\(\?\<(?<FirstCaptureName>\w+))>') { $matches.FirstCaptureName } else { $_.Name } $description = @( # The pattern's description will be if ($patternIn.Description) { $patternIn.Description } for (;$c -lt $rxLines.Length;$c++) { # Any number of initial lines starting with comments. if ($rxLines[$c] -notlike '#*') { break } $rxLines[$c].TrimStart('#').Trim() } ) -join [Environment]::NewLine $rx = @(for (;$c -lt $rxLines.Length;$c++) { $rxLines[$c] }) -join [Environment]::NewLine $regex = [PSCustomObject][Ordered]@{ # Create the RegEx object PSTypeName = 'Irregular.RegEx' Name = $name ; Description = $description Pattern = $rx; Path = $in.FullName IsGenerator = $false;IsPattern = $true } $regex = # If it contained other RegExes if ($regex.IsPattern -as [bool] -and $SavedCaptureReferences.IsMatch($rx)) { # try replacing them $firstReplaceTry = $savedCaptureReferences.Replace($rx, $replaceSavedCapture) if ($firstReplaceTry -ne $rx -and -not $savedCaptureReferences.IsMatch($firstReplaceTry)) { $regex.Pattern = $firstReplaceTry $regex } else { # If we couldn't, try, try again. $regex.Pattern = if ($firstReplaceTry) { $firstReplaceTry } else { $rx } $tryTryAgain.Enqueue($regex) } } else { $regex } return $regex } } # We want an internal function to keep import a single file. $importRegexFile = { process { $in = $_ # See if it matches our naming convention $nameOk = $_.Name -match '^(?<Name>.*?)\.regex\.((?<IsGenerator>ps1)|(?<IsPattern>txt))$' # If it doesn't, bounce if (-not $nameOk) { return } $n = $matches.Name if ($Name -or $PSBoundParameters.ContainsKey('Name')) { # If we're filtering imports by name :FoundIt do { # check if we want to import this one. foreach ($pn in $name) { if ($n -like $pn) {break FoundIt} } return } while ($false) } if ($matches.IsGenerator) { # If the file was a generator $findDescription = [Regex]::new(@' \.Description # Description Start \s{0,} # Optional Whitespace (?<Content>(.|\s)+?(?=(\.\w+|\#\>|\z))) # Anything until the next .\word or \comment '@, 'IgnoreCase,IgnorePatternWhitespace') # find it's description from inline help $generatorScript = $ExecutionContext.SessionState.InvokeCommand.GetCommand($in.FullName, 'ExternalScript') $matched = $findDescription.Match($generatorScript.ScriptContents) $gn = # then determine the name of the Regex $(if ($in.Directory.Name -eq 'RegEx') { $n # (if it's in a directory called Regex, it's the file name) } else { $dirPart = ($in.Directory.FullName.Substring($importPath.Length) -replace '(?:\\|/)RegEx(?:\\|/)','') if (-not $dirPart) { $dirPart = $in.Directory.Name } # Otherwise, it's directoryname_$n $dirPart + '_' + $n }) return [PSCustomObject][Ordered]@{ PSTypeName = 'Irregular.RegEx' Name = $gn ; Description = $matched.Groups["Content"].ToString(); Pattern = ''; Path = $in.FullName IsGenerator = $true;IsPattern = $false } } $_ | & $importRegexPattern } } $importIntoLibrary = { process { $regex = $_ $script:_RegexLibrary[$regEx.Name] = if ($regex.IsGenerator) { $ExecutionContext.SessionState.InvokeCommand.GetCommand($regex.Path, 'ExternalScript') } else { try { [Regex]::new( ("(?<$($regex.Name)>", $($regex.Pattern -join [Environment]::NewLine), ')' -join [Environment]::NewLine), 'IgnoreCase,IgnorePatternWhitespace', '00:00:05.00') } catch { $PSCmdlet.WriteError( [Management.Automation.ErrorRecord]::new("Could not import $($regex.Name): $($_.Exception.Message)",$_.Exception, 'OpenError',$_) ) } } $script:_RegexLibraryMetaData[$regex.Name] = $regex if ($PassThru) { $regex } if ($importInvocation.InvocationName -eq '&' -or $importInvocation.InvocationName -eq '.') { return } $foundAlias = if ($ModuleCaller) { if ($ModuleCaller -and $ModuleCaller.ExportedAliases.Count) { $ModuleCaller.ExportedAliases["?<$($regex.Name)>"] } else { $true } } else { $ExecutionContext.SessionState.InvokeCommand.GetCommand("?<$($regex.Name)>",'Alias') } if ($regex -and -not $foundAlias) { $tempModule = New-Module -Name "?<$($regex.Name)>" -ScriptBlock { Set-Alias "?<$args>" Use-RegEx; Export-ModuleMember -Alias * } -ArgumentList $regex.name | Import-Module -Global -PassThru if (-not $script:_RegexTempModules) { $script:_RegexTempModules = [Collections.Queue]::new() } $script:_RegexTempModules.Enqueue($tempModule) } } } } process { #region Determine the Path List $pathList = & { if ($Pattern) { return } if ($FilePath) { # If any file paths were provided foreach ($fp in $filePath){ # resolve them $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($fp) } return # and use just this pathlist. } if ($FromModule) { # If -FromModule was passed, $loadedModules = Get-Module # get all loaded modules. $loadedModuleNames = foreach ($lm in $loadedModules) { # get their names $lm.Name } $OkModules = foreach ($fm in $fromModule) { # filter the ones that are OK $loadedModuleNames -like $fm } foreach ($lm in $loadedModules) { if ($OkModules -contains $lm.Name) { $lm | Split-Path } } } else { $MyInvocation.MyCommand.ScriptBlock.File | Split-Path } } #endregion Determine the Path List $tryTryAgain = [Collections.Queue]::new() # Create a queue to store retries #region Get RegEx files $pathList = $pathList | Select-Object -Unique foreach ($p in $pathList) { $p = "$p" if ([IO.Directory]::Exists($p) -or [IO.File]::Exists($p)) { @( if ([IO.File]::Exists($p)) { [IO.FileInfo]$p $ImportPath = ([IO.FileInfo]$p).Directory.FullName } elseif ([IO.Directory]::Exists($p)) { $ImportPath = $p ([IO.DirectoryInfo]"$p").EnumerateFiles('*', 'AllDirectories') }) | & $importRegexFile | . $importIntoLibrary } } #endregion Get RegEx files #region Import Patterns Directly if ($Pattern) { $Pattern | & $importRegexPattern | . $importIntoLibrary } #endregion Import Patterns Directly #region Retry Nested Imports $patience = 1kb @(while ($tryTryAgain.Count) { $tryAgain = $tryTryAgain.Dequeue() $countBefore = $tryTryAgain.Count $tryAgain | & $importRegexPattern $countAfter = $tryTryAgain.Count if ($countAfter -gt $countBefore) { $patience-- } if ($patience -le 0) { Write-Verbose "Patience Exceeded. Expressions most likely have circular references" #-ErrorId Irregular.Import.Lost.Patience break } }) | . $importIntoLibrary #endregion Retry Nested Imports } } |