Import-RegEx.ps1

function Import-RegEx
{
    <#
    .Synopsis
        Imports Regular Expressions
    .Description
        Imports saved Regular Expressions.
    .Example
        Import-RegEx
    .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
    [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 {
        if (-not $script:_RegexLibrary) { $script:_RegexLibrary = @{}}
        if (-not $script:_RegexLibraryMetaData) { $script:_RegexLibraryMetaData = @{}}

        $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')


        $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 {
                $regex
            }
        }

        filter importRegexPattern {
            process {
                $c = 0
                $rxLines =
                    @(if ($_ -is [IO.FileInfo]) {
                        [IO.File]::ReadLines($_.Fullname)
                    } elseif ($_ -is [string]) {
                        $_ -split '(?>\r\n|\n)'
                    } elseif ($_.Pattern) {
                        $_.Pattern -split '(?>\r\n|\n)'
                    })

                $name =
                    if ($_ -is [IO.FileInfo]) {
                        $_.Name -replace '\.regex\.txt$', ''
                    } elseif ($_ -is [string] -and $_ -match '(?<StartsWithCapture>\A\(\?\<(?<FirstCaptureName>\w+))>') {
                        $matches.FirstCaptureName
                    } else {
                        $_.Name
                    }

                $description =
                    @(for (;$c -lt $rxLines.Length;$c++) {
                        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]@{
                        PSTypeName   = 'Irregular.RegEx'
                        Name = $name ; Description = $description
                        Pattern = $rx; Path = $in.FullName
                        IsGenerator = $false;IsPattern = $true
                    }

                $regex =
                    if ($matches.IsPattern -as [bool] -and $SavedCaptureReferences.IsMatch($rx)) {
                        $firstReplaceTry = $savedCaptureReferences.Replace($rx, $replaceSavedCapture)
                        if ($firstReplaceTry -ne $rx -and -not $savedCaptureReferences.IsMatch($firstReplaceTry)) {
                            $regex.Pattern = $firstReplaceTry
                            $regex
                        } else {
                            $regex.Pattern  = if ($firstReplaceTry) { $firstReplaceTry } else { $rx }
                            $tryTryAgain.Enqueue($regex)
                        }
                    } else {
                        $regex
                    }

                return $regex
            }
        }

        filter importRegexFile {
            process {
                $in = $_
                $nameOk = $_.Name -match '^(?<Name>.*?)\.regex\.((?<IsGenerator>ps1)|(?<IsPattern>txt))$'
                if (-not $nameOk) {
                    return
                }
                $n = $matches.Name
                if ($Name -or $PSBoundParameters.ContainsKey('Name')) {
                    :FoundIt do {
                        foreach ($pn in $name) {
                            if ($n -like $pn) {
                                break FoundIt
                            }
                        }
                        return
                    } while ($false)
                }

                if ($matches.IsGenerator) {
                    $findDescription = [Regex]::new(@'
\.Description # Description Start
\s{0,} # Optional Whitespace
(?<Content>(.|\s)+?(?=(\.\w+|\#\>|\z))) # Anything until the next .\word or \comment
'@
, 'IgnoreCase,IgnorePatternWhitespace')
                    $generatorScript =
                        $ExecutionContext.SessionState.InvokeCommand.GetCommand($in.FullName, 'ExternalScript')

                    $matched = $findDescription.Match($generatorScript.ScriptContents)

                    return [PSCustomObject][Ordered]@{
                            PSTypeName   = 'Irregular.RegEx'
                            Name = $n    ; Description = $matched.Groups["Content"].ToString();
                            Pattern = ''; Path = $in.FullName
                            IsGenerator = $true;IsPattern = $false
                        }
                }

                $_ | importRegexPattern
            }
        }

        filter 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 {
                            Write-Error -Message "Could not import $($regex.Name): $($_.Exception.Message)" -Exception $_.Exception -TargetObject $_
                        }
                    }
                $script:_RegexLibraryMetaData[$regex.Name] = $regex
                if ($pattern -and -not $MyInvocation.MyCommand.ScriptBlock.Module.ExportedAliases."?<$($regex.Name)>") {
                    $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)
                }
                if ($PassThru) { $regex }
            }
        }
    }

    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
                    # Otherwise, any valid module directory for Irregular will work.
                    $splitChar = if ($PSVersionTable.Platform -eq 'Unix') { ':' } else {';'}
                    foreach ($_ in $env:PSModulePath -split $splitChar) {
                        $_ + [IO.Path]::DirectorySeparatorChar + 'Irregular'
                    }
                }
            }
        #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
                } elseif ([IO.Directory]::Exists($p)) {
                    ([IO.DirectoryInfo]"$p").EnumerateFiles('*.regex.*', 'AllDirectories')
                })
            }
        }) |
            importRegexFile |
            importIntoLibrary

        if ($Pattern) {
            $Pattern |
                importRegexPattern |
                importIntoLibrary
        }

        $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-Error "Patience Exceeded. Expressions most likely have circular references" -ErrorId Irregular.Import.Lost.Patience
                break
            }
        }) | importIntoLibrary
        #endregion Get RegEx files
    }
}