RoughDraft.Irregular.ps1
#region Irregular Embedded [0.6.4] : https://github.com/StartAutomating/Irregular $ImportRegex = { <# .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 New-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 } } $UseRegex = { <# .Synopsis Uses a saved regular expression. .Description Uses a saved regular expression, or an expression provided with -Parameter. Use-RegEx is normally called with an alias that is the name of a saved RegEx, for example: ?<Digits> .Link Get-RegEx .Link New-RegEx .Example "abc" | Use-RegEx -Pattern '.' .Example 'true', 'false', 'neither' | ?<TrueOrFalse> # ?<TrueOrFalse> is a saved RegEx and alias to Use-RegEx .Example $txt = "true or false or true or false" $m = $txt | ?<TrueOrFalse> -Count 1 do { $m $m = $m | ?<TrueOrFalse> -Count 1 -Scan } while ($m) # Looping over each match until non are found. ?<TrueOrFalse> is an alias to Use-RegEx #> [CmdletBinding(DefaultParameterSetName='Pattern')] [OutputType([Text.RegularExpressions.Match], [string], [PSObject])] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectComparisonWithNull", "", Justification="This is explicitly checking for null (lazy -If would miss 0)")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidAssignmentToAutomaticVariable", "", Justification="Risk understood and behavior is desired")] param( # One or more strings to match. [Parameter(Mandatory=$true,ParameterSetName='Text',ValueFromPipeline,Position=0)] [Parameter(ParameterSetName='Pattern',Position=0,ValueFromPipelineByPropertyName)] [Alias('InputObject','Text', 'Matches','Value')] [string[]]$Match, # If set, will return a boolean indicating if the regular expression matched [switch]$IsMatch, # If set, will measure the number of matches. [switch]$Measure, # The count of matches to return, or the number of matches split or replaced. [Alias('Number')] [int]$Count = 0, # The starting position of the match [Parameter(ValueFromPipelineByPropertyName)] [Alias('StartingAt')] [int]$StartAt = 0, # If set, will remove the regular expression matches from the text. [switch]$Remove, # If set, will replace the text with a replacement string. # For more information about replacement strings, see: # https://docs.microsoft.com/en-us/dotnet/standard/base-types/substitutions-in-regular-expressions [string]$Replace, [switch]$Scan, # If provided, will replace the match if any of the conditions exist. [ValidateScript({ foreach ($kv in $_.GetEnumerator()) { if ($kv.Key -isnot [ScriptBlock]) { throw "Keys must be ScriptBlocks" } } return $true })] [Collections.IDictionary] $ReplaceIf, # If provided, will each match will be passed to the Replacer ScriptBlock. # The values returned from this script block will replace the match. [Alias('Replacer','Evaluator')] [ScriptBlock]$ReplaceEvaluator, # If set, will split the input text according to the expression. [switch]$Split, # If set, will get the text until the expression. [switch]$Until, # If -IncludeMatch and -Until are provided, will include the match with the result of -Until. # If -IncludeMatch and -Split are provided, will include the matches with the result of -Split. # If -IncludeMatch is provided with -Extract, a .Match property will be included in the result. # If neither -Split or -Until is provided, this parameter is ignored. [Alias('IncludingMatch')] [switch]$IncludeMatch, # If set, will trim returned strings. [switch]$Trim, # If set, will extract capture groups into a custom object. [switch]$Extract, # If provided, will add typename information to the returned objects. # This implies -Extract. [string] $PSTypeName, # If provided, will transform each match with a replacement string. # For more information about replacement strings, see: # https://docs.microsoft.com/en-us/dotnet/standard/base-types/substitutions-in-regular-expressions [string]$Transform, # If provided, will cast named capture groups to a given type. This implies -Extract. [ValidateScript({ foreach ($kv in $_.GetEnumerator()) { if ($kv.Key -isnot [string]) { throw "Keys must be a string" } if ($kv.Value -isnot [type] -and $kv.Value -isnot [ScriptBlock]) { throw "Values must be a type or Script Block" } } return $true })] [Alias('Cast')] [Collections.IDictionary]$Coerce, # If provided, will filter the extracted data of a match. [ScriptBlock] $Where, # One or more conditions. If the condition is true, the value will be returned. # If the value is a script block, it will be executed. # If the value is a string, it will be treated as a Replacement string (like -Transform). [ValidateScript({ foreach ($kv in $_.GetEnumerator()) { if ($kv.Key -isnot [ScriptBlock]) { throw "Keys must be ScriptBlocks" } } return $true })] [Collections.IDictionary]$If, # The regular expression options, by default, IgnoreCase and IgnorePatternWhitespace [Alias('Options')] [Text.RegularExpressions.RegexOptions] $Option = 'IgnoreCase, IgnorePatternWhitespace', # If set, will go from right to left, instead of left to right. [switch] $RightToLeft, # The match timeout. By default, five seconds. [Timespan] $Timeout = "00:00:05", # Indicates that the cmdlet makes matches case-sensitive. By default, matches are not case-sensitive. [switch]$CaseSensitive, # A regular expression. [Parameter(ParameterSetName='Pattern',ValueFromPipelineByPropertyName)] [Management.Automation.ArgumentCompleter({ # While we don't want to restrict the steps here, we _do_ want to be able to suggest steps that are built-in. param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) if ($wordToComplete) { Get-Regex | Where-Object Name -like "$wordtocomplete*" | Select-Object -ExpandProperty Name } else { Get-RegEx | Select-object -ExpandProperty Name } })] [Alias('Expression')] [string]$Pattern, # A pattern generator. This script will generate a regular expression [ScriptBlock] $Generator, # Named parameters for the regular expression. These are only valid if the regex is a Generator. [Alias('ExpressionParameters')] [Collections.IDictionary] $ExpressionParameter = @{}, # A list of arguments. These are only valid if the regex is using a Generator script. [Alias('ExpressionArguments','ExpressionArgs')] [PSObject[]]$ExpressionArgumentList = @() ) dynamicParam { $myInv = $MyInvocation # If we didn't have a regex library if (-not $script:_RegexLibrary -or -not $script:_RegexLibrary.Count) { # it could be because we're invoke in a place where $script: variables aren't accessible. if ($myInv.MyCommand.Module) { # If that's the case, and this command is within a module $script:_RegexLibrary = @{} # then we can try to look at the RegexLibraryMetadata to reconstruct out regex liberary $regexMetadata = . $myInv.MyCommand.Module {$_RegexLibraryMetadata} if ($regexMetadata -and $regexMetadata.getEnumerator) { # If we found metadata foreach ($kv in $regexMetadata.GetEnumerator()) { # Walk over each piece of metadata $script:_RegexLibrary[$kv.Key] = # the key format is the same for RegexLibrary. # If the value has a pattern, it's a RegEx if ($kv.Value.Pattern) { [Regex]::new($kv.Value.Pattern, 'IgnoreCase,IgnorePatternWhitespace','00:00:05') } # If the path was like *.ps1, it's a RegEx Generator. elseif ($kv.Value.Path -like '*.ps1') { $ExecutionContext.SessionState.InvokeCommand.GetCommand($kv.Value.Path, 'ExternalScript') } } } } if (-not $script:_RegexLibrary) { $script:_RegexLibrary = @{} } } # Then, determine what the name of the pattern in the library would be. $mySafeName = if ('.', '&' -contains $myInv.InvocationName -and ( $myInv.Line.Substring($MyInvocation.OffsetInLine) -match '^\s{0,}\?\<(?<Name>\w+)\>' ) -or ( $myInv.Line.Substring($MyInvocation.OffsetInLine) -match '^\s{0,}\$\{\?\<(?<Name>\w+)\>\}' ) ) { $matches.Name } else { $myInv.InvocationName -replace '\W', '' } # Find the regex in the library. $regex = $script:_RegexLibrary[$mySafeName] $DynamicParameterNames = @() if ($regex -isnot [Management.Automation.ExternalScriptInfo]) { return } $generator = $regex $generatorMetaData = [Management.Automation.CommandMetaData]$generator $DynamicParameters = [Management.Automation.RuntimeDefinedParameterDictionary]::new() foreach ($kv in $generatorMetaData.Parameters.GetEnumerator()) { $DynamicParameters.Add($kv.Key, [Management.Automation.RuntimeDefinedParameter]::new( $kv.Value.Name, $kv.Value.ParameterType, $kv.Value.Attributes ) ) } $DynamicParameterNames = $DynamicParameters.Keys -as [string[]] return $DynamicParameters } begin { if ($DynamicParameterNames) { foreach ($dynamicParameterName in $DynamicParameterNames) { if ($PSBoundParameters.ContainsKey($DynamicParameterName)) { $ExpressionParameter[$dynamicParameterName] = $PSBoundParameters[$dynamicParameterName] } } } # Now figure out if we'll be extracting later $isExtracting = $MyInvocation.InvocationName -eq '.' -or $Extract -or $PSTypeName -or $coerce.Count -or $If.Count # If -Where or -If was provided, we need to recreate the script blocks for $_ to work. if ($Where) { $where = [ScriptBlock]::Create($Where) } # In order for $_ to work correctly, # we need to recreate any script block parameters passed within dictionaries. # Rather than write this three times, let's loop over each collection foreach ($coll in $if, $ReplaceIf, $Coerce) { if (-not $coll) { continue } foreach ($k in @($coll.Keys)) { $v = $coll[$k] if ($v -is [ScriptBlock]) { $v = [ScriptBlock]::Create($v) } $coll.Remove($k) if ($k -is [ScriptBlock]) { $k = [ScriptBlock]::Create($k) } $coll[$k] = $v } } #region [ScriptBlock]$ExtractMatch $extractMatch = { process { $m = $_ $xm = [Ordered]@{} foreach ($g in $m.Groups[1..($m.Groups.Count -1)]) { if ($g.Name -as [int] -ge 1) { continue } if ($g.Name -eq $mySafeName -and $m.Groups.Count -gt 2) { continue } # Unroll the captures. If there's only one, we want it to be a single value, not an array. $gcv = foreach ($gc in $g.Captures) { $gc.Value } if ($Coerce -and $Coerce.$($g.Name) -is [type]) { $xm[$g.Name] = foreach ($v in $gcv) { $v -as $Coerce.$($g.Name) } } elseif ($Coerce -and $Coerce.$($g.Name) -is [ScriptBlock]) { $xm[$g.Name] = foreach ($v in $gcv) { $_ = $v; & $Coerce.$($g.Name) $v } } else { $xm[$g.Name] = foreach ($cv in $gcv) { if ($cv -as [float] -ne $null) { if ($cv -as [float] -ne $cv -as [int]) { $cv -as [float] } else { if ($cv -ge 0 -and $cv -lt 256) { $cv -as [byte] } else { $cv -as [int] } } } elseif ($cv -eq 'true') { $true } elseif ($cv -eq 'false') { $false } elseif ($cv.Contains -and $cv.Contains(':')) { $cvFixPunctuation = $cv.Replace(',','.').Replace('.', [CultureInfo]::CurrentCulture.NumberFormat.NumberDecimalSeparator) if ($cvFixPunctuation -as [Timespan]) { $cvFixPunctuation -as [Timespan] } else { $cv } } elseif ($cv -as [DateTime]) { $cv -as [DateTime] } else { $cv } } } } if ($IncludeMatch) { $xm.Match = $m } $xm.PSTypeName = if ($PSTypeName) {$PSTypeName } else { 'Irregular.Match.Extract' } [PSCustomObject]$xm } } #endregion [ScriptBlock]$ExtractMatch #region [ScriptBlock]$FilterMatches $FilterMatches = { process { if ($_ -is [Boolean] -or $_ -is [string]) { return $_ } $currentMatch = $_ $MatchMetaData = [Ordered]@{ StartIndex = $_.Index EndIndex = $_.Index + $_.Length # Input = $_.Result('$_') } if ($isExtracting -or $Where) { $xm = $currentMatch | & $extractMatch } if ($where) { $this = $_ = $xm $IsThere = . $where $in if (-not $IsThere) { return } $_ = $currentMatch } if ($transform) { return . $decorateString $currentMatch.Result($transform) $matchMetaData } if ($if.Count) { $in = $_ = $xm foreach ($ifCondition in $if.GetEnumerator()) { $ifResult = & $ifCondition.Key $in if ($ifResult) { if ($ifCondition.Value -is [ScriptBlock]) { $_ = $xm . $ifCondition.Value $in } elseif ($ifCondition.Value -is [string]) { . $decorateString $currentMatch.Result($ifCondition.Value) $matchMetaData } else { $ifCondition.Value } } } return } if ($isextracting) { return $xm } $psProps = $currentMatch.psobject.properties if ($psProps['EndIndex'] -isnot [PSScriptProperty]) { # add on two script properties we might want: $psProps.Remove('EndIndex') # EndIndex $psProps.add([PSScriptProperty]::new('EndIndex', { $this.Index + $this.Length })) } if ($psProps['Input'] -isnot [PSScriptProperty]) { $psProps.Remove('Input') $psProps.add([PSScriptProperty]::new('Input', { $this.Result('$_') })) # and Input. } if ($inputObject) { $psProps.Remove('InputObject') $psProps.add([PSNoteProperty]::new('InputObject', $inputObject)) } else { $psProps.Remove('InputObject') $psProps.add([PSAliasProperty]::new('InputObject', 'Input')) } return $currentMatch } } #endregion [ScriptBlock]$FilterMatches #region [ScriptBlock]$DecorateString $DecorateString = { param( [string]$string, [Collections.IDictionary]$property = @{}) if ($trim) { $string = $string.Trim() } $psString = [PSObject]::new($string) foreach ($kv in $property.GetEnumerator()) { $psString.psobject.properties.add([PSNoteProperty]::new($kv.Key, $kv.Value)) } $psString } #endregion [ScriptBlock]$DecorateString } process { #region Prepare Input $in = $inputObject = $_ if ($_.Input) { # First we want to see if the piped in object had an input property. $match = $_.Input # If it did, we're using it to cheat in the value to -Match. } if ($in -is [IO.FileInfo]) { # If the input was a file, $match = [IO.File]::ReadAllText($in.FullName) # we want to match the file contents } if ($in -is [Management.Automation.ExternalScriptInfo]) { # If we were passed an external script $match = "{$($in.ScriptContents)}" # we want to match it's contents. } if ($in -is [Management.Automation.FunctionInfo]) { # If we're passed a function, $match = "function $($in.Name) {$($in.ScriptBlock)}" # we want to match the definition. } if ($in -is [ScriptBlock]) { $match = "{$in}" } if ($_ -is [Text.RegularExpressions.Match] -and -not $StartAt) { # If the input was a [Match] and we don't have a start if (-not $_.psobject.properties['EndIndex']) { # add on two script properties we might want: $_.psobject.properties.add( # EndIndex [PSScriptProperty]::new('EndIndex', { $this.Match.Index + $this.Match.Length }) ) } if (-not $_.psobject.properties['Input']) { $_.psobject.properties.add( # and Input. [PSScriptProperty]::new('Input', { $this.Match.Result('$_') }) ) } if ($Scan) { $startAt = $_.Index + $_.Length } } #endregion Prepare Input #region Initialize Regular Expression # If the saved RegEx is a generator if ($regex -is [Management.Automation.ExternalScriptInfo] -or $regex -is [ScriptBlock]) { if ($generator -and $mySafeName -and $mySafeName -ne ($MyInvocation.MyCommand.Name -replace '\W', '')) { Write-Error "Will not override ?<$mySafeName>" -ErrorId RegEx.No.Override -Category InvalidOperation return } $Generator = if ($regex -is [Management.Automation.ExternalScriptInfo]) { $regex.ScriptBlock } else { $regex } } if ($Generator) { # (or one was provided) $regex = & $Generator @ExpressionArgumentList @ExpressionParameter # run the generator. if ($regex -and $mySafeNAme -and -not "$regex".StartsWith("(?<$mySafeName") -and -not $mySafeName -eq 'UseRegEx') { $regex = "(?<$mySafeName>$($regex;[Environment]::NewLine;))" } } if ($Pattern) { # If we've been provided a pattern # and it would overriding something if ($mySafeName -and $mySafeName -ne ($MyInvocation.MyCommand.Name -replace '\W', '')) { Write-Error "Will not override ?<$mySafeName>" -ErrorId RegEx.No.Override -Category InvalidOperation return } if ($script:_RegexLibrary) { if (($pattern -match '^\?\<(?<Name>\w+)\>' -or $Pattern -match '^(?<Name>[\w_]+)$') -and $script:_RegexLibrary.($matches.Name) ) { $pattern = $script:_RegexLibrary.($matches.Name) } } # If we didn't have to warn them, we've propably piped in a [Regex] or the output of New-Regex. $regex = [Regex]::new($Pattern, 'IgnoreCase,IgnorePatternWhitespace') } if (-not $regex) { return } # If for any reason our regex is invalid, return. if ($RightToLeft) { # If we're going RightToLeft $Option = $Option -bor 'RightToLeft' # adjust the Regex options if ($StartAt -and $_.EndIndex -eq $startAt -and $_.Index -ne $null) { # and adjust the start if needed. $startAt = $_.Index } if (-not $startAt -and $_.EndIndex) { return } } if ($CaseSensitive) { # If we're using CaseSensitive, $option = $option -bxor 'IgnoreCase' # adjust the RegEx options. } # Then recreate the regex with the new options and timeout $regex = [Regex]::new("$regex", $Option, $Timeout) if (-not $regex) { return } # If for any reason our regex is invalid, return. #endregion Initialize Regular Expression if (-not $Match) { # If we haven't been given any text to match $regex.pstypenames.add('Irregular.Regular.Expression') # decorate the Regex for the formatter. return $regex # and return it. This will let "true" -match (?<TrueOrFalse>) be valid PowerShell. } $OriginalStartAt = $StartAt foreach ($m in $Match) { # Walk over each text we're supposed to match $$, $methodArgs = $null, $null if ($RightToLeft -and -not $OriginalStartAt) { $startAt = $m.Length } if ($until) { # If we're matching until that point $matches = $regex.Match($m, $StartAt) # find the first match after StartAt. if (-not $matches.Success) { continue } # If the match failed, continue. if ($measure) { if ($RightToLeft) { $startAt - ($matches.Index - $matches.Length) } else { $matches.Index - $startAt } continue } $ei = # Determine the EndIndex if ($IncludeMatch) { # ( if we're including the match $matches.Index + $matches.Length # its the end of the match, } else { $matches.Index # otherwise, it's the start of the match). } if ($startAt, ($ei - $startAt) -lt 0) { continue } # Then get the substring and decorate it with the following properties: . $DecorateString ($m.Substring($startAt, $ei - $startAt)) ([Ordered]@{ StartIndex = $startAt # | StartIndex| The Start Index | EndIndex = $ei # | EndIndex| The End Index | Input = $matches.Result('$_') # | Input | The Match Input String | }) } elseif ($Split) { # If we're splitting, we get the matches. # (this lets us -IncludeMatch and sidestep a .NET bug when splitting -RightToLeft) $matches = @($regex.Matches($M,$StartAt) | & $filterMatches) $upTo = if ($Count) { $count } else {$matches.Count} $commonInfo = [Ordered]@{Input=$m;InputObject=$in} if ($RightToLeft) { $s = if ($startAt -ne $m.Length) { $startAt } else { $m.Length } for ($mc=0;$mc -lt $upTo;$mc++) { $me = $matches[$mc].Index + $matches[$mc].Length if ($me -lt $s) { . $decorateString $m.Substring($me, $s - $me) } if ($IncludeMatch) { . $decorateString $matches[$mc] ([Ordered]@{ StartIndex = $matches[$mc].Index EndIndex = $matches[$mc].Index + $matches[$mc].Length } + $commonInfo) } $s = $matches[$mc].Index } if ($s -gt 0) { . $decorateString $m.Substring(0, $s) } } else { $s = $startAt for ($mc=0;$mc -lt $upTo;$mc++) { if ($matches[$mc].Index - $s) { . $decorateString $m.Substring($s, $matches[$mc].Index - $s) } if ($IncludeMatch) { . $decorateString $matches[$mc] ([Ordered]@{ StartIndex = $matches[$mc].Index EndIndex = $matches[$mc].Index + $matches[$mc].Length } + $commonInfo) } $s = $matches[$mc].Index + $matches[$mc].Length } if ($s -ne $m.Length) { . $decorateString $m.Substring($s) } } } elseif ($Remove -or $Replace -or $ReplaceEvaluator -or $ReplaceIf.Count) { $$ = 'Replace' $methodArgs = @( $M if ($remove) { '' } elseif ($Replace) { $Replace } elseif ($ReplaceEvaluator) { $ReplaceEvaluator } elseif ($ReplaceIf) { { $tm = $($args[0]) $xm = $($tm | & $filterMatches | & $extractMatch ) foreach ($kv in $ReplaceIf.GetEnumerator()) { $_ = $xm $kvR = . $kv.Key $xm if ($kvR) { if ($kv.Value -is [ScriptBlock]) { return "$(. $kv.Value $xm)" } return $tm.Result("$($kv.Value)") } } return "$tm" } } if ($Count) { $Count } else { [int]::MaxValue } $StartAt ) } elseif ($IsMatch) { $$= 'IsMatch' $methodArgs = @($M;$StartAt) } elseif ($Count) { $$ =0 $methodArgs = @($M;$StartAt) $matches = $regex.Match.Invoke($methodArgs) if ($Measure) { $t = 0 } while ($matches.Success -and $$ -lt $Count) { if (-not $measure) { $matches | & $filterMatches } else { $t++ } $$++ $matches = $matches.NextMatch() } if ($measure) { $t } } else { $$ = 'Matches' $methodArgs = @($M;$StartAt) } if ($regex.$$ -and $methodArgs) { if ($measure) { @($regex.$$.Invoke($methodArgs)).Length } else { & { try { $regex.$$.Invoke($methodArgs) } catch { $PSCmdlet.WriteError([Management.Automation.ErrorRecord]::new($_.Exception, 'Regular.Expression.Error', 'NotSpecified', $inputObject)) } } | & $filterMatches } } } } } #endregion Irregular Engine [0.6.4] : https://github.com/StartAutomating/Irregular . $ImportRegex $(if ($psScriptRoot) { $psScriptRoot } else { $pwd }) foreach ($k in $script:_RegexLibrary.Keys) { $executionContext.SessionState.PSVariable.Set("?<$k>", $useRegex) } |