Irregular.tests.ps1
#requires -Module Pester, Irregular [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingCmdletAliases", "", Justification="Irregular Uses Smart Aliases")] param() describe Get-Regex { it "Lets you keep a library of Regular Expressions" { Get-RegEx } it "Lets you get a particular item by name" { Get-RegEx -Name TrueOrFalse } it 'Can get RegExs from a -FilePath' { Get-Regex -FilePath (Get-Command Get-Regex | Get-Module | Split-Path) } it 'Can get Regexes -FromModule ' { Get-Regex -FromModule Irregular } it 'Can get a Regex -As a String' { Get-Regex -Name Digits -As String | should be '\d+' } it 'Can get a RegEx -As a File' { Get-Regex -Name Digits -As File | Select-Object -ExpandProperty Name | should be Digits.regex.txt } it 'Can get a RegEx -As a Pattern' { Get-Regex -Name Digits -As Pattern | should belike '*\d+*' } it 'Can get a RegEx -As a Hashtable' { Get-RegEx -Name Digits -As Hashtable | should belike '@{*' } it 'Can get a RegEx -As a Variable' { Get-RegEx -Name Digits -As Variable | should belike '$digits*=*\d+*' } it 'Can get a RegEx -As an Alias' { Get-RegEx -Name Digits -As Alias | should be 'Set-Alias ?<Digits> Use-RegEx' } } describe Import-Regex { it 'Imports Regular Expressions into the library' { Import-RegEx } it 'Can import -FromModule' { Import-RegEx -FromModule Irregular -PassThru } it 'Can Import from a -FilePath' { Import-RegEx -FilePath (Get-Module Irregular | Split-path) -PassThru } it 'Can import a -Pattern directly' { Import-RegEx -Pattern "(?<AnySymbol>\p{S})" (?<AnySymbol>).GetType() | should be ([Regex]) } } describe Show-Regex { it 'Is an interactive tool to preview simple Regex operations' { $o = Show-RegEx -Pattern '?<Digits>' -Match abc123def456 $o.Output.Count | should be 2 } it 'Can -Remove content' { $o = Show-RegEx -Pattern '?<Digits>' -Match abc123def456 -Remove $o.Output | should be abcdef } it 'Can -Replace content' { $o = Show-RegEx -Pattern '?<Digits>' -Match abc123def456 -Remove $o.Output | should be abcdef } it 'Will return the Regex is no other parameters than Pattern are passed' { $o = Show-RegEx -Pattern '?<Digits>' "$o" |should belike *\d+* } it 'Will show that invalid patterns are invalid' { $o = Show-RegEx -Pattern '(' $o.IsValid | should be $false } } describe Use-Regex { it 'Is normally used with an alias of a named expression (e.g. ?<Digits>)' { (?<Digits>).GetType() | should be ([Regex]) } it 'Lets you find all matches' { ?<Digits> -Match "123abc456" | Measure-Object | Select-Object -ExpandProperty Count| should be 2 } it 'Lets you find a single match' { ?<Digits> -Match '123abc456' -Count 1 | Measure-Object | Select-Object -ExpandProperty Count| should be 1 } it 'Lets you piped in multiple inputs' { "123abc456", "def789" | ?<Digits> | Measure-Object | Select-Object -ExpandProperty Count | should be 3 } it 'Can search -RightToLeft' { "123abc456" | ?<Digits> -RightToLeft -Count 1 | Select-Object -ExpandProperty Value | should be 456 } it 'Can -Extract results' { @('123abc456' | ?<Digits> -Extract | Select-Object -ExpandProperty Digits) | should match '\d+' } it 'Will assume -Extract if called with a .' { '123abc456' | . ?<Digits> | Select-Object -ExpandProperty Digits | should match \d+ } it 'Can -Coerce (or -Cast) results to a type' { '123abc456' | ?<Digits> -Coerce @{Digits=[int]} -Count 1 | Select-Object -ExpandProperty Digits | should be 123 } it 'Can -Coerce results with a [ScriptBlock]' { '123abc456' | ?<Digits> -Coerce @{ Digits={($_ -as [int]) * 2} } -Count 1 | Select-Object -ExpandProperty Digits | should be 246 } it 'Can filter results with -Where' { @('123abc456' | ?<Digits> -Where {$_.Digits %2 } | should be '123') } it 'Can -Remove matches' { '123abc456' |?<Digits> -Remove | should be abc } it 'Can -Replace matches' { '123abc456' |?<Digits> -Replace ' $1 ' | should be ' 123 abc 456 ' } it 'Can -ReplaceIf a conidition is met' { '123abc456' | ?<Digits> -ReplaceIf @{ { $_.Digits % 2 } = '$1 (is odd) '} | should be '123 (is odd) abc456' } it 'Can use a -Replacer [ScriptBlock]' { '123abc456' | ?<Digits> -ReplaceEvaluator { '_' } | should be _abc_ } it 'Can -Replace a -Count' { '123abc456' | ?<Digits> -Remove -Count 1 | should be abc456 } it 'Can -Transform matches into something else' { @('123abc456' | ?<Digits> -Transform '-$1' ) -join ' ' | should be '-123 -456' } it 'Can transform a match -If a condition is met' { '123abc456' | ?<Digits> -If @{{$_.Digits %2 } = '$1 is odd' } | should be '123 is odd' } it 'Can run a script -If a condition is met' { '123abc456' | ?<Digits> -If @{{$_.Digits %2 } = {"$([int]$_.Digits * 2) is even" }} | should be '246 is even' } it 'Can return an arbitrary value -If a condition is met' { $randomNumber = [Random]::new().Next() '123' | ?<Digits> -If @{{$_} = $randomNumber } | should be $randomNumber } it 'Will -Coerce before -If' { '123abc456' |?<Digits> -Coerce @{Digits=[int]} -If @{{$_.Digits %2 } = {"$($_.Digits * 2 ) is even" }} | should be '246 is even' } context '-Split' { it 'Will -Split a string' { "key:value" |?<Colon> -Split | Select-Object -First 1 | should be key } it 'Will -Split a string -RightToLeft' { 'key: value' | ?<Colon> -Split -Trim -RightToLeft | Select-Object -First 1 | should be value } it 'Will -Split -StartAt at point' { 'prefix: key: value' | ?<Colon> -Split -StartAt 'prefix:'.Length -Trim | Select-Object -First 1 | should be key } it 'Can -IncludeMatch with a -Split' { $k,$s, $v = "key:value" |?<Colon> -Split -IncludeMatch $s | should be ':' $k,$s, $v = "key:value" |?<Colon> -Split -IncludeMatch -RightToLeft $s | should be ':' } it 'Will -Split -Count number of times' { 'key: value: with a colon' | ?<Colon> -Split -Count 1 -Trim | Select-Object -First 1 -Skip 1 | should be 'value: with a colon' } it 'Will -Split -Count items from -RightToLeft' { 'lkey: value:value:rkey' | ?<Colon> -Split -Count 1 -RightToLeft -Trim | Select-Object -First 1 | should be 'rkey' } } context '-Until' { it 'Can get content -Until a point' { "How much wood would a woodchuck chuck if a woodchuck could chuck wood?" | ?<Punctuation> -Until | should be "How much wood would a woodchuck chuck if a woodchuck could chuck wood" } it 'Can -IncludeMatch with -Until' { "How much wood would a woodchuck chuck if a woodchuck could chuck wood?" | ?<Punctuation> -Until -IncludeMatch | should be "How much wood would a woodchuck chuck if a woodchuck could chuck wood?" } it 'Can -Measure the distance -Until a match' { 'key:value' |?<Colon> -Until -Measure | should be 3 'key:value' |?<Colon> -Until -Measure -RightToLeft | should be 7 } } it 'Can make any RegEx -CaseSensitive' { Use-RegEx -Pattern 'param' -Match 'Param' -IsMatch -CaseSensitive | should be $false } it 'Can -Measure the number of matches' { Use-RegEx -Pattern '(?>\r\n|\n)' -Measure -Match ([Environment]::NewLine * 6) | should be 6 Use-RegEx -Pattern '(?>\r\n|\n)' -Measure -Match ([Environment]::NewLine * 8) -Count 4 | should be 4 } it 'Can seek from one match to the next' { @($txt = "true or false or true or false" $m = $txt | ?<TrueOrFalse> -Count 1 do { $m $m = $m | ?<TrueOrFalse> -Count 1 } while ($m)) -join ' ' | # Looping over each match until non are found. ?<TrueOrFalse> is an alias to Use-RegEx should be 'true false true false' } it 'Can seek from one match to the next (from -RightToLeft)' { @($txt = "true or false or true or false" $m = $txt | ?<TrueOrFalse> -Count 1 -RightToLeft do { $m $m = $m | ?<TrueOrFalse> -Count 1 -RightToLeft } while ($m)) -join ' ' | # Looping over each match until non are found. ?<TrueOrFalse> is an alias to Use-RegEx should be 'false true false true' } context 'Special Piping Behavior' { it 'Will match the contents if piped in a file' { (Get-Command Write-RegEx | Select-Object -ExpandProperty ScriptBlock | Select-Object -ExpandProperty File) -as [IO.FileInfo] | ?<PowerShell_HelpField> | Select-Object -ExpandProperty InputObject | Select-Object -ExpandProperty Name | should be Write-Regex.ps1 } it 'Will match the script contents if passed an external script' { Get-Command ((Get-Command Write-RegEx | Select-Object -ExpandProperty ScriptBlock | Select-Object -ExpandProperty File)) | ?<PowerShell_HelpField> | Select-Object -ExpandProperty InputObject | Select-Object -ExpandProperty Name | should be Write-Regex.ps1 } it 'Will match the definition if passed a function' { Get-Command Write-RegEx | ?<PowerShell_HelpField> | Select-Object -ExpandProperty InputObject | Select-Object -ExpandProperty Name | should be Write-RegEx } } context Generators { it 'Can use a .regex.ps1 to generate a Pattern' { "{'hello world'}" | ?<BalancedCode> | Select-Object -ExpandProperty Value | should be "{'hello world'}" } it 'Can pass named -Parameter[s] to a generator' { "{'hello world'}" | ?<BalancedCode> -Parameter @{Open='{'} | Select-Object -ExpandProperty Value | should be "{'hello world'}" } it 'Can pass -Arguments to a generator' { "['hello world']" | ?<BalancedCode> -Arguments '[' | Select-Object -ExpandProperty Value | should be "['hello world']" } it 'Can use a dynamic generator' { $rx = Use-RegEx -Generator {param($t) "$t"} -Parameter @{t='hi'} "$rx"| should belike *hi* } } context 'Fault Tolerance' { it 'Will complain if -If is passed keys that are not script blocks' { { '123' |?<Digits> -If @{a='b'}} | should throw } it 'Will complain if -ReplaceIf is passed keys that are not script blocks' { { '123' |?<Digits> -ReplaceIf @{a='b'}} | should throw } it 'Will complain if -Coerce is passed non-strings as keys' { { '123' |?<Digits> -Coerce @{{'Digits'} = [int]}} | should throw } it 'Will complain if -Coerce is passed non-types as values ' { { '123' |?<Digits> -Coerce @{'Digits' = "alksldj"} } | should throw } it 'Will complain when a Named expression is passed a pattern' { { ?<Digits> -Pattern 'blah' -ErrorAction Stop } | should throw } } } describe Write-Regex { it "Helps you write -CharacterClasses" { Write-RegEx -CharacterClass LowerCaseLetter | Select-Object -ExpandProperty Pattern | should be '\p{Ll}' } it "Lets you look for repeated content" { Write-RegEx -CharacterClass Digit -Repeat | Select-Object -ExpandProperty Pattern | should be '\d+' } it "Simplifies lookahead with -Before (aka -LookAhead)" { Write-RegEx -Expression 'q' -LookAhead u | # Matches a q that is followed by a u Select-Object -ExpandProperty Pattern | should be 'q(?=u)' } it 'Simplifies lookbehind with -After (aka -LookBehind)' { Write-RegEx -Expression u -LookBehind q | # Matches a u that is preceeded by a q Select-Object -ExpandProperty Pattern | should be '(?<=q)u' } it 'Simplifies negative lookahead with -NotBefore' { Write-RegEx -Expression q -NotBefore u | # Matches a q that isn't followed by a u Select-Object -ExpandProperty Pattern | should be 'q(?!u)' } it "Simplifies negative lookbehind with -NotAfter (aka -NegativeLookBehind)" { Write-RegEx -Expression '"' -NegativeLookBehind '\\' | Select-Object -ExpandProperty Pattern | should be '(?<!\\)"' } it "Can pipe to itself to compound expressions" { Write-RegEx -Pattern '"' | Write-RegEx -CharacterClass Any -Repeat -Lazy -Before ( Write-RegEx -Pattern '"' -NotAfter '\\|`' ) | Write-RegEx -Pattern '"' | Select-Object -ExpandProperty Pattern | should be '".+?(?=(?<!\\|`)")"' } it 'Can combine more than on -CharacterClass' { Write-RegEx -CharacterClass Digit, Word | Select-Object -ExpandProperty Pattern | should be '[\d\w]' } it 'Can negate a -CharacterClass' { Write-RegEx -CharacterClass Digit, Word -Not | Select-Object -ExpandProperty Pattern | should be '[^\d\w]' } it 'Can handle -LiteralCharacters' { ?<> -Name UserName -LiteralCharacter .- -CharacterClass Word -Repeat | ?<> (?<> '\@' -NoCapture) | ?<> -Name Domain -LiteralCharacter .- -CharacterClass Word -Repeat } it 'Can use a -StartAnchor or -EndAnchor' { Write-RegEx -CharacterClass Whitespace -Min 0 -StartAnchor LineStart -EndAnchor LineEnd | Select-Object -ExpandProperty Pattern | should be '^\s{0,}$' } it 'Can check for -Min and -Max occurances' { Write-RegEx -CharacterClass Whitespace -Min 0 -Max 4 | Select-Object -ExpandProperty Pattern | should be '\s{0,4}' } it 'Can leave a comment' { Write-RegEx -CharacterClass Whitespace -Comment "Whitespace" | Select-Object -ExpandProperty Pattern | should belike "\s # Whitespace*" } it 'Can write a description' { Write-RegEx -CharacterClass Whitespace -Description "Whitespace" | Select-Object -ExpandProperty Pattern | should be "# Whitespace$([Environment]::NewLine)\s" } it 'Can name a capture' { Write-RegEx -Name Digits -CharacterClass Digit -Repeat | Select-Object -ExpandProperty Pattern | should be '(?<Digits>\d+)' } it 'Can write an expression that will always fail' { Write-RegEx -Not | select -ExpandProperty Pattern | should be '(?!)' } it 'Can write an anti expression' { Write-RegEx -Not foo | Select-Object -ExpandProperty pattern | should be '\A((?!(foo)).)*\Z' } it 'Can be -Atomic' { Write-RegEx -Atomic -Pattern 'do', 'die' -Or | select-object -expand Pattern | should be '(?>(do|die))' } it 'Can be -Greedy or -Lazy (or both)' { Write-RegEx -Pattern '(.|\s)' -Greedy -Lazy | Select-Object -ExpandProperty Pattern | Should be '(.|\s)*?' } it "Doesn't have to capture (with -NoCapture)" { Write-RegEx -NoCapture '\d+' | Select-Object -ExpandProperty Pattern | should be '(?:\d+)' } it 'Can be optional' { Write-RegEx -Pattern do, die -Or -Optional | select-object -expand Pattern | should be '(do|die)?' } it 'Can use Saved Expressions (with the format ?<Name>)' { Write-RegEx ?<Digits> | Select-Object -ExpandProperty Pattern | should belike '*\d+*' } it 'Can write conditionals' { Write-RegEx '((?<Digit>\d)|(?<NotDigit>\D))' -If Digit -Then '\D' -Else '\d' | Use-RegEx -IsMatch 'a1' | should be $true Write-RegEx '(?<Digit>\d)' | Write-RegEx -If Digit -Then '[abcdef]' } it 'Can write backreferences' { Write-RegEx -Backreference previousCapture | Select-Object -ExpandProperty Pattern | Should be '\k<previousCapture>' $(Write-RegEx -Backreference 1).ToString() | Should be '\1' } it 'Can refer to other saved captures in a pattern (by putting ?<CaptureName> without leading comments)' { Write-RegEx -Pattern '?<Digits>' | Use-RegEx -IsMatch -Match 1 | should be true } it 'Can rename a saved capture (by putting (?<NewCaptureName>?<OldCaptureName>)' { Write-RegEx -Pattern '(?<MyDigits>?<Digits>)' | Use-RegEx -Extract -Match 1 | Select-Object -ExpandProperty MyDigits | should be 1 } it 'Can match -Until a pattern' { $writeRegexCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Write-Regex','Function') Write-RegEx -Pattern \<\# | Write-RegEx -Name Block -Until \#> | Write-RegEx -Pattern \#\> | Use-RegEx -Extract -Match $writeRegexCmd.Definition | Select-Object -ExpandProperty Block | should belike *Write-Regex* } context '-Between' { it 'Makes it easy to match double quotes' { Write-RegEx -Between '"' -Name InQuotes | Use-RegEx -Extract -Match 'this is not in quotes. "This Is \"". This is not in quotes' | Select-Object -ExpandProperty InQuotes | should be 'This Is \"' } it 'Makes it easy to match single quotes' { Write-RegEx -Between "'" -Name InQuotes -EscapeSequence "''" | Use-RegEx -Extract -Match "this is not in quotes. 'This Is '''. This is not in quotes" | Select-Object -ExpandProperty InQuotes | should be "This Is ''" } it 'Makes it easy to match block comments' { Write-RegEx -Name BlockComment -Between '\<\#', '\#\>' -EscapeSequence '' | Use-RegEx -Extract -Match @' 1 <#BlockComment#> 2 '@ | Select-Object -ExpandProperty BlockComment | should be BlockComment } } it 'Can refer to a capture generator (parameters can be passed with () or {})' { Write-RegEx -Pattern '?<BalancedCode>{(}' | Use-RegEx -IsMatch -Match '({}' | should be $false Write-RegEx -Pattern '?<BalancedCode>({)' | Use-RegEx -IsMatch -Match '({}' | should be $true } } describe Export-RegEx { it 'Can Export a RegEx as a -Variable' { Export-RegEx -Name Digits -As Variable | should belike '$digits*=*\d+*' } it 'Can Export to a Path' { if ($env:TEMP) { Export-RegEx -Name Digits -Path $env:TEMP Get-Content (Join-Path $env:TEMP 'Digits.regex.txt') -raw | should belike *\d+* } } if (-not $env:Agent_ID -and $PSVersionTable.Platform -ne 'Unix') { # Skipping this test in AzureDev ops due to disk issues it 'Can Export -As a Script to a Temporary Path' { if ($env:TEMP) { $exFile= (Join-Path $env:TEMP Digits.ps1) Export-RegEx -Name Digits -Path $exFile -As Script $exFileContent = Get-Content $exFile -Raw $exFileContent| should belike '*\d+*' $exFileContent | should belike '*function UseRegex*' $exFileContent | should belike '*Set-Alias ?<Digits> UseRegex*' $exFile | Remove-Item } } it 'Can Export a Temporary Pattern' { Import-RegEx -Pattern '(?<SomeMoreDigits>\d+)' Export-RegEx -Name SomeMoreDigits $createdFile = Get-Module Irregular | Split-Path | Join-Path -Path { $_ } -ChildPath RegEx | Get-ChildItem -Filter SomeMoreDigits.regex.txt $createdFile.Name | should be SomeMoreDigits.regex.txt $createdFile | Remove-Item } } if ($PSVersionTable.Platform -ne 'Unix') { it 'Will complain when passed a filepath and multiple names (if -As is file)' { { Export-RegEx -Name Digits, OptionalWhitespace -Path "$env:TEMP\DigitsAndWhitespace.regex.txt" -ErrorAction Stop } | should throw } if (-not $env:Agent_ID) { it 'Can Export a RegEx as a -Script' { $irregularPath = Get-Module Irregular | Split-Path $ex = Export-RegEx -Name Digits -As Script Get-Command Export-RegEx | Select-Object -ExpandProperty Module| Remove-Module . ([ScriptBlock]::Create($ex)) 'abc123' | ?<Digits> | Select-Object -Property * Import-Module $irregularPath } } } } describe 'Expressions' { context '?<EmailAddress>' { it 'Will extract an email and domain' { 'foo@bar.com' | ?<EmailAddress> -Extract | % { $_.Username | should be foo $_.Domain | should be bar.com } } it 'Will not match a psuedo-email' { 'psued@oemail' | ?<EmailAddress> |should be $null } } context '?<Namespace>' { it 'Will match a namespace' { $nsExtract = @' namespace MyNamespace { public class foo() {} } '@ | ?<Namespace> -Extract $nsExtract.Content | should belike '{*foo()*}' $nsExtract.Name | should be MyNamespace } } } describe 'Generators' { context '?<MultilineComment>' { it 'Will auto-detect comment types' { Get-Module Irregular | Split-Path | Get-ChildItem -Recurse -Filter *.ps1 | ?<MultilineComment> -Count 1 | should belike '<#*#>' } it 'Will extract comments from a function' { Get-Command Write-Regex | ?<MultilineComment> -Count 1 | should belike '<#*#>' } } } describe Set-Regex { it 'Lets you store Regular Expressions' { Get-RegEx -Name Digits | Set-Regex -Confirm:$false } if (-not $env:Agent_ID -and $PSVersionTable.Platform -ne 'Unix') { it 'Lets you declare them temporarily' { Set-Regex -Name Period -Pattern '\.' -Temporary Use-RegEx -Pattern '?<Period>' -Match '.' -IsMatch | should be true } it 'Will infer the name' { Set-Regex -Pattern '(?<Period>\.)' -Description 'A period' -Temporary } it 'Will complain if the pattern was not named' { {Set-Regex -Pattern blah -Temporary -errorAction Stop} | should throw } it 'Can append to a an inline description' { Set-Regex -Pattern '# a math symbol (?<MathSymbol>\p{Sm})' -Description 'Using the special character class math' -Temporary Write-RegEx '?<MathSymbol>' | should belike '*\p{Sm}*' } it 'Can accept the output of Write-Regex' { Write-RegEx -LiteralCharacter := -Name ColonOrEquals | Set-Regex Get-Module Irregular | Split-Path | Join-Path -ChildPath 'Regex' | Join-Path -ChildPath 'ColonOrEquals.regex.txt' | Remove-Item } } it 'Can set a regex in an arbitrary path' { if ($env:TEMP) { Set-RegEx -Pattern '(?<Period>\.)' -Path $env:TEMP Get-ChildItem -LiteralPath $env:temp -Filter Period.regex.txt | Select-Object -ExpandProperty Name | should be Period.regex.txt } else { 'No temp directory found' } } } |