pf-string.ps1

function Get-CharRange([char]$Start = 'A', [char]$End = 'Z') {
    $startLetter = [int]$Start
    $endLetter = [int]$End
    for ( $letter = $startLetter; $letter -le $endLetter; $letter++) {
        [char]$letter
    }
}

function Get-Chars([switch]$word,[switch]$number,[switch]$symbol, 
        [switch]$UpperCase, [switch]$LowerCase) {
    if ($word -or $UpperCase) {
        Get-CharRange 'A' 'Z'
    }
    if ($word -or $LowerCase) {
        Get-CharRange 'a' 'z'
    }
    if ($number) {
        Get-CharRange '0' '9'
    }
    if ($symbol) {
        $symbolSet = '!%()[]{}@#=*+-_?|' 
        # & not included as not easy include in xml configurations
        for($i = $symbolSet.Length - 1; $i -ge 0; $i--) {
            $symbolSet[$i]
        }
    }
}
function Get-Chars:::Test {
    ( Get-Chars -UpperCase ).count -eq 26 | assert
    ( Get-Chars -LowerCase ).count -eq 26 | assert
    ( Get-Chars -word ).count -eq 26*2 | assert
    ( Get-Chars -number ).count -eq 10 | assert
    ( Get-Chars -symbol ).count -gt 8 | assert
}

function Get-RandomString ($length = 20, $seed = [Guid]::NewGuid(), $validChars) {
    if ($validChars) {
        $valid = $validChars
    }
    else {
        $valid = Get-Chars -word -number -symbol
    }
    if ($seed) {
        $seed = $seed.GetHashCode()
        $randomizer = New-Object 'System.Random' -ArgumentList $seed
    }

    [string]$result = ''
    for ($i = $length; $i -gt 0; $i --) {
        $ch = $randomizer.Next($valid.Length)
        $ch = $valid[$ch]
        $result += $ch
    }
    $result
}
function Get-RandomString:::Example {
    Get-RandomString -seed 'AnyObject'  -length 23 -validChars (Get-Chars -LowerCase -number )
}

function Get-HeadAndTail{
    Param(
        [Parameter(ValueFromPipeline=$true)]
        [String]$string,
        $fieldSeparator = ':',
        [Switch]$RightToLeft,
        [String]$FieldHead = 'Head',
        [String]$FieldTail = 'Tail'
    )
    process {
        if (-not $string) { return }
        $pos = if ($RightToLeft) { $string.LastIndexOf($fieldSeparator) }
                            else { $string.IndexOf($fieldSeparator) }
        if ($pos -eq -1) {
            return [PSCustomObject]@{ $FieldHead = $string; $FieldTail = "" }
        }
        $head = $string.Substring(0,$pos)
        $tail = $string.Substring($pos+1)
        $result = [PSCustomObject]@{ $FieldHead = $head; $FieldTail = $tail }
        if ($RightToLeft) {
            return [PSCustomObject]@{ $FieldHead = $result.Tail; $FieldTail = $result.Head }
            $string.LastIndexOf($fieldSeparator)
        }
        return $result
    }
}

function Get-HeadAndTail:::Test{
    Get-HeadAndTail -string 'A:B:C'
    Get-HeadAndTail -string 'A:B'
    Get-HeadAndTail -string 'A' 
    Get-HeadAndTail -string '' 

    Get-HeadAndTail -RightToLeft -string 'A:B:C'
    Get-HeadAndTail -RightToLeft -string 'A:B'
    Get-HeadAndTail -RightToLeft -string 'A' 
    Get-HeadAndTail -RightToLeft -string '' 
}

function Get-Regex_Match {
    [CmdLetBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [String]$regex,
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        $value
    )
    process {
        if ( [String]::IsNullOrEmpty($value) ) { return }
        $value = $value.ToString()
        $match = $value -match $regex
        if ( $match ) {
            return New-Object PsObject -Property $Matches
        }
    }   
}
function Get-Regex_Match:::Test {
    $expected = @{a = 'ABC'; b = 'Commit'; '0' = "ABC_Commit" } 
    "ABC_Commit" | Get-Regex_Match "(?<a>.+)_(?<b>.+)" | ConvertFrom-PSObject_ToHashtable
         | Assert  $expected
}

function Get-Like_Match {
    [CmdLetBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [String]$like,
        [Parameter(ValueFromPipeline=$true)]
        $value,
        [string[]]$ProperyNames
    )
    begin {
        $TokenReplace = [Guid]::NewGuid().ToString().Replace('-','')
        $regex = $like.Replace('*',$TokenReplace)
        $regex = [RegEx]::Escape($regex)
        $regex = $regex.Replace($TokenReplace,'*')
        if ( $regex.EndsWith('*') ) {
            $regex += '$' 
        }
        $regex = $regex.Replace('*', '(.*?)')
    }
    process {
        if ( [String]::IsNullOrEmpty($value) ) { return }
        $value = $value.ToString()
        $match = $value -match $regex
        if ( $match ) {
            if ($ProperyNames) {
                $propObj = [Ordered]@{}
                $propcount = 1
                $ProperyNames | ForEach-Object { 
                    $propObj.Add($_ , $Matches[$propcount] ) 
                    $propcount++
                }
                if ($ProperyNames -notcontains 'match') {
                    $propObj.Add('match' , $Matches[0] ) 
                }

                return New-Object PsObject -Property $propObj
            }
            return New-Object PsObject -Property $Matches -ErrorAction SilentlyContinue 
        }
    }   
}

function Get-Like_Match:::Test {
    "File_(123)_(ABC).xml" | Get-Like_Match "file_(*)_(*).xml"  
    "File_(123)_(ABC).xml" | Get-Like_Match "file_(*)_(*).*"
    "File_(123)_(ABC).xml" | Get-Like_Match "file_(*)_(*).*" -ProperyNames 'Number','string','ext','more'
}

function Get-RegEx_MatchGroup ( $src, $regex, $groupName ) {
    $match = $src -match $regex
    if ( -not $match ) {
        throw "Cannot find '$groupName' using format '$regex' in '$src'"
    }
    return $matches[$groupName]
}

function Get-RegEx_MatchGroup:::Test {
    Get-RegEx_MatchGroup  "ABC_Commit" "(?<Package>.+)_(?<buildname>.+)" -groupName "Package" | Should -Be 'ABC'
    { Get-RegEx_MatchGroup  "ABCCommit" "(?<Package>.+)_(?<buildname>.+)" -groupName "Package" } | Should -throw 'Cannot find*'
}

function Join-String_Ex ( [string]$join, [string]$beforeItem, [string]$AfterItem, $whenEmpty ) {
    end {
        $items = $input | ForEach-Object { $_ }
        if ( ( -not $items ) -and $whenEmpty ) { $items = $whenEmpty }
        $strItems = $items | ForEach-Object { $beforeItem + "$_" + $AfterItem }
        if (-not $strItems) { return } 
        [String]::Join($join, $strItems)
    }
}

function Join-String_Ex:::Test {
    {$a -eq $b} , {$b -eq $c},  {$c -eq $d} | Join-String_Ex ' -and ' '( ' ' )' | 
        Should -be '( $a -eq $b ) -and ( $b -eq $c ) -and ( $c -eq $d )'
    {$a -eq $b} | Join-String_Ex ' -and ' '( ' ' )' | Should -be '( $a -eq $b )'
    Join-String_Ex ' -and ' '( ' ' )' { $true } | Should -be '( $true )'
    Join-String_Ex ' -and ' '( ' ' )' | Should -be $null
}

function Join-Script ( [string]$join, $whenEmpty  ) {
    end {
        $result = $input | Join-String_Ex " $join " '(' ')' $whenEmpty 
        return [scriptblock]::Create($result)
    }
}
function Join-Script:::Examples {
    {$a -eq $b} , {$b -eq $c},  {$c -eq $d} | Join-Script '-and'
    {$a -eq $b} | Join-Script '-and'
    Join-Script '-and'
    Join-Script '-and' {$true}
}

function Split-StringInColumns ($columns, [switch]$Trim) {
    begin {
        $colDefList = $columns.GetEnumerator() | 
            ForEach-Object { [PSCustomObject]@{ Column = $_.Key; Width = $_.Value } }
    }
    process {
        [string]$line = $_
        $startPos = 0
        $result = [Ordered]@{}
        foreach ($colDef in $colDefList) {
            if ($startPos -gt $line.Length) {
                $result.Add($colDef.Column, $null)
                continue
            }
            $width = [Math]::Min($line.Length - $startPos, $colDef.Width)
            $value = $line.Substring($startPos, $width)
            if ($Trim) {
                $value = $value.Trim()
            }
            $result.Add($colDef.Column, $value)
            $startPos += $colDef.Width
        }
        [PSCustomObject]$result
    }
}

function Split-StringInColumns:::Test {
    $columns = [Ordered]@{ Current = 1; SessionName = 15; USERNAME = 20; ID = 11; State = 7; Type = 11; Device = 10}
    $line = '>rdp-tcp#5 Administrator 2 Active'
    $line | Split-StringInColumns -columns $columns
}

function Update-String_Enclose {
    param ( 
        $prefix, 
        $suffix = $prefix, 
        $escape,
        [Switch]$conditional
    ) 
    process {
        [string]$value = $_
        if (-not $value) {
            return "$prefix$suffix"
        }
        if ( $conditional -and $value.StartsWith($prefix) -and $value.EndsWith($suffix)) {
            return $value
        }
        if ($escape -and $value.Contains($prefix)) {
            $value = $value.Replace($prefix,"$escape$prefix")
        }
        return $prefix + $value + $suffix
    }
}
function Update-String_Enclose:::Test {
    'ab"c' | Update-String_Enclose '"' -escape '`' | assert '"ab`"c"' 

    $null | Update-String_Enclose '"' | assert '""' 
    '' | Update-String_Enclose '"' | assert '""' 
    'abc' | Update-String_Enclose '"' | assert '"abc"' 
    'abc' | Update-String_Enclose "'" | assert "'abc'" 
    "'ddd'" | Update-String_Enclose '"' | assert "`"'ddd'`""
    "'ddd'" | Update-String_Enclose "'" -conditional | assert "'ddd'"
    "'ddd'" | Update-String_Enclose "'" | assert "''ddd''"
}

function Get-LongestPrefix ( $value, $property ) {
    begin {
        $match = $null
        $matchLength = 0
    }
    process {
        [string]$prefix = if ( $property ) { $_ | Select-Object -ExpandProperty $property } else { $_ }
        if ( ( $prefix.Length -gt $matchLength ) -and $value.StartsWith($prefix) ) {
            $matchLength = $prefix.Length;
            $match = $_    
        }
    }
    end {
        if ( $null -ne $match ) {
            return $match
        }
    }
}
function Get-LongestPrefix:::Test {
    'C:\', 'd:','C:\abc' | Get-LongestPrefix 'C:\abc\def\ghi' | Assert 'C:\abc'
}

function Get-RootPrefix {
    param(
        $prefixList
    )
    process {
        $sortedPrefixList = $prefixList | Sort-Object -Descending | Select-Object -Unique
        $lastPosition = $sortedPrefixList.Length - 1
        for ($i = 0; $i -le $lastPosition; $i++) {
            $current = $sortedPrefixList[$i]
            for( $j = $i+1; $j -le $lastPosition; $j++) {
                $prefix = $sortedPrefixList[$j]
                if ( $current -like "$prefix*" ) {
                    $current = $null
                    break
                }
            }
            if ($current) {
                $current
            }
        }
    }
}
function Get-RootPrefix:::Test {
    $prefixList = @'
D:\BuildDrop\LG_VM\CORP_LG_VM_QA-GR2_17449\VM-Corp.SysApp.CORP.QA-GR2.5.0.0.17446\VM\Virtual Machines
D:\BuildDrop\LG_VM\CORP_LG_VM_QA-GR2_17449\VM-Corp.SysApp.CORP.QA-GR2.5.0.0.17446
D:\BuildDrop\LG_VM\CORP_LG_VM_QA-GR2_17449
D:\BuildDrop\LG_VM\CORP_LG_VM_QA-GR2_17437\VM-Corp.SysApp.CORP.QA-GR2.5.0.0.17436\VM\Virtual Machines
D:\BuildDrop\LG_VM\CORP_LG_VM_QA-GR2_17437\VM-Corp.SysApp.CORP.QA-GR2.5.0.0.17436
D:\BuildDrop\LG_VM\CORP_LG_VM_QA-GR2_17437
'@

    $prefixList = $prefixList -split "`r`n"

    $expected = @'
D:\BuildDrop\LG_VM\CORP_LG_VM_QA-GR2_17449
D:\BuildDrop\LG_VM\CORP_LG_VM_QA-GR2_17437
'@
 -split "`r`n"

    $actual = Get-RootPrefix -prefixList $prefixList
    $actual.Count | assert -eq $expected.Count
    
}

function Test-Number ($varValue) {
    if ( $null -eq $varValue ) { return $false }
    $type = $varValue.GetType()
    switch ($type) {
        [Decimal] { return $true }
        [Char] { return $false }
        [Bool] { return $false }
        default { $type.IsPrimitive }         
    }
    # The following can be used to see all .Net primitiveTypes
    # [object].GetType().Assembly.getTypes() | ? { $_.IsPrimitive }
}
function Test-Number:::Test {
    Test-Number $null | Assert $false
    Test-Number ( -1 ) | Assert $true
    Test-Number 1 | Assert $true
    Test-Number 'a' | Assert $false
}

function convert-bool ( [string] $value, [string[]] $trueStrings, [string[]] $falseStrings ) {
    if (-not $value) {
        return $false
    }
    
    if (-not $trueStrings) {
        $trueStrings = "1","Y","TRUE"
    }
    if (-not $falseStrings) {
        $falseStrings = "0","N","FALSE"
    }

    [STRING] $valueUpperCase = $value.ToUpperInvariant()

    foreach ($trueValue in $trueStrings) {
        if ( $valueUpperCase -eq $trueValue.ToUpperInvariant() ) { 
            return $true 
        }
    }
    foreach ($falseValue in $falseStrings) {
        if ( $valueUpperCase -eq $falseValue.ToUpperInvariant() ) { 
            return $false 
        }
    }
    throw [System.ArgumentException] "Not found a converto from $value to boolean."
}
function convert-bool:::Test {
    convert-bool 'Y' | Assert $true
}

function Get-NewName_Preextension {
    param(
        [string]$fullname,
        $PreExtension = 'Generated'
    ) 
    $parts = $fullname.Split('.')
    $templateSufix = $parts[-1]
    $ext = $parts[-2]

    $fullname | Update-Suffix -Suffix ".$ext.$templateSufix" -replace ".$PreExtension.$ext"
}
function Get-NewName_Preextension:::Test {
    $filename = "$env:TEMP\Get-NewName_Preextension\input.sql.PSTemplate"
    $expected = "$env:TEMP\Get-NewName_Preextension\input.Generated.sql"
    Get-NewName_Preextension -fullname $filename | assert -eq $expected
}

function Get-Abbreviation([string]$str) {
    [string]$result = ''
    foreach ($ch in $str.GetEnumerator() ) {
        if ($ch -ge 'A' -and $ch -le 'Z') {
            $result += $ch
        }       
    } 
    return $result
}
function Get-Abbreviation:::Test {
    Get-Abbreviation 'KiloBytes' | Assert 'KB'
}

function Get-SizeText([double]$size, [int]$decimalPlaces = 2, [switch]$reduce) {
    $sizes = @("Bytes", 'KiloBytes','MegaBytes','GigaBytes','TeraBytes','PetaBites')
    $magnitud = 0;
    while ( $size -gt 1KB ) {
        $size = $size / 1KB
        $magnitud++
    }
    if ( $decimalPlaces -eq 0 ) {
        $size = [int]$size
    } else {
        $size = $size * ( 10 * $decimalPlaces )
        $size = [int]$size
        $size = [double]$size / ( 10 * $decimalPlaces )
    }
    $magnitudText = $sizes[$magnitud]
    if ($reduce) {
        $magnitudText = Get-Abbreviation $magnitudText
    }
    return "$size $magnitudText"
}
function Get-SizeText:::Test {
    Get-SizeText 512 | Assert '512 Bytes'
    Get-SizeText 10000mb 1 | Assert '9.8 GigaBytes'
}

function Get-TimeStamp {
    get-date -f 'yyyyMMdd_HHmmss_ffffff'
}

function Get-RandomPassword ($length = 20, $seed = [Guid]::NewGuid(), $validChars) {
    if ($validChars) {
        $valid = $validChars
    }
    else {
        $valid = Get-Chars -word -number -symbol
    }
    
    if (-not $randomizer) {
        $seed = $seed.GetHashCode()
        $global:randomizer = New-Object 'System.Random' -ArgumentList $seed
    }
    if ($seed) {
        $seed = $seed.GetHashCode()
        $randomizer = New-Object 'System.Random' -ArgumentList $seed
    }

    [string]$result = ''
    for ($i = $length; $i -gt 0; $i --) {
        $ch = $randomizer.Next($valid.Length)
        $ch = $valid[$ch]
        $result += $ch
    }
    $result
}
function Get-RandomPassword:::Example {
    Get-RandomPassword -seed 'AnyObject'
}

# Add a section to a file after the indicated line
function Add-Section {
    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
        $file,
        $newContentsection,
        $afterLineThatContains
    )
    process {
        $path = Get-Path $file
        if ($path) {
            $enc = Get-FileEncoding -FullName $path
            $content = ( Get-Content -Raw -Path $path )

            $pos = $content.IndexOf($afterLineThatContains, [System.StringComparison]::InvariantCultureIgnoreCase)
            if ($pos -lt 0 ) {
                throw "file '$path' does not have a line with '$afterLineThatContains' "
            }
            
            # Determine new line sequence used in the file
            $posNewLine = $content.IndexOf("`n", $pos)
            $newline = $content.Substring($posNewLine-1,2)
            if ( $newline -ne "`r`n" ) {
                $newline = "`n"
            }  

            $sb = New-Object 'System.Text.StringBuilder' -ArgumentList $content
            
            if ( $posNewLine -lt 0 ) {
                $sb.Append("`n" + $newContentsection);
            }
            else {
                $sb.Insert( $posNewLine, "`n" + $newContentsection )
            }

            $newContent = $sb.ToString()
            if ($newContent -ne $content) {
                Write-Verbose "Replacing in '$path' , '$find' -> '$replacement' "
                Set-Content -Path $path $newContent -Encoding $enc -NoNewline
            }
        }
    }
}
function Add-Section:::Example {
    $testfile = "$env:TEMP\Add-Section.txt"

    function GenTestContent ($lines = 10) {
        begin { @(1..$lines) }
        process { '<<< ADD AFTER THIS >>>' }    
        end { @(1..$lines) }
    }

    $newSection = "START SECTION`nINTERMEDIATE CONTENT`nEND SECTION"

    GenTestContent > $testfile 
    $testfile | Add-Section -newContentsection $newSection -afterLineThatContains 'ADD AFTER THIS'
    $newContent = Get-Content $testfile -Raw
    $newContent.Contains($newSection) | Assert $true

    Remove-Item $testfile -Force
}

function Test-Like {
    [CmdletBinding()]
    Param(
        $right,
        [Switch]$not,
        [ScriptBlock]$pattern = { "*$_*" },
        [Parameter(ValueFromPipeline=$true)]
        $left
    )
    begin {
        if ( -not $pattern ) {
            $pattern = { "$_" }
        }
        # Remove empty elements
        $right = $right | Where-Object { $_ }
        if ( -not $right ) {
            $matchToList = $null
        }
        else {
            $matchToList = $right | ForEach-Object { Invoke-Command -ScriptBlock $pattern }
        }
    }
    process {
        $matched = $false
        foreach ($matchTo in $matchToList) {
            $matched = $left -like $matchTo
            if ($matched) {
                if (-not $not) {
                    return $left
                }
                break
            }
        }
        if ($not -and -not $matched) {
            return $left
        }
    }
}
function Test-Like:::Test {
    'AA', 'BB', 'CC' | Test-Like 'b' | assert -eq 'BB'
    'AA', 'BB', 'CC' | Test-Like 'b','x' | assert -eq 'BB'
    'AA', 'BB', 'CC' | Test-Like -not #| assert -eq 'AA', 'BB'
    'AA', 'BB', 'CC' | Test-Like 'b' -not #| assert -eq 'AA', 'CC'
    'AA', 'BB', 'CC' | Test-Like 'b','x' -not #| assert -eq 'AA', 'CC'
}

function Test-SubString {
    [CmdletBinding()]
    Param(
        $right,
        [Switch]$not,
        [Parameter(ValueFromPipeline=$true)]
        $left
    )
    begin {
        $right = $right | Where-Object { $_ }
        if ( -not $right ) {
            $matchToList = $null
        }
        else {
            [string[]]$matchToList = $right | ForEach-Object { $_.ToLowerInvariant() }
        }
    }
    process {
        $matched = $false
        $value = $left
        if ($value) {
            $value = $value.ToLowerInvariant()
            foreach ($matchTo in $matchToList) {
                $matched = $matchTo.Contains($value)
                if ($matched) {
                    if (-not $not) {
                        return $left
                    }
                    break
                }
            }
            if ($not -and -not $matched) {
                return $left
            }
        }
    }
}
function Test-SubString:::Test {
    'AA', 'BB', 'CC' | Test-SubString 'bbb' | should -be 'BB'
    'AA', 'BB', 'CC' | Test-SubString 'bbb','xxx' | should -be 'BB'
    'AA', 'BB', 'CC' | Test-SubString 'bbb' -not | should -be @('AA','CC')
    'AA', 'BB', 'CC' | Test-SubString 'bbb', 'xxx' -not | should -be @('AA', 'CC')
}

function Get-ListRecords {
    Param(
        [Parameter(ValueFromPipeline=$true)]
        [string]$line,
        $fieldSeparator = ':'
    )
    process {
        $line = $line.Trim()
        if (-not $line) {
            if ( $result.Count ) {
                [PSCustomObject]$result
            } 
            $result = @{}
        }
        else {
            $parts = $line | Get-HeadAndTail -fieldSeparator $fieldSeparator
            $field = $parts.Head;
            $value = if ($line.Length -eq $field.Length) {
                    $true
                }
                else {
                    $parts.Tail.Trim()
                }
            $result.$field = $value
        }
    }
    end {
        if ( $result.Count ) {
            [PSCustomObject]$result
        } 
    }
}
function Get-ListRecords:::Example {
    $lines = @'
 
    Target: LegacyGeneric:target=MicrosoftAccount:user=test@hotmail.com
    Type: Generic
    User: test@hotmail.com
    Local machine persistence
     
    Target: LegacyGeneric:target=msteams_adalsso/adal_context_14
    Type: Generic
    Local machine persistence
'@
 -split "`n"

    $results = $lines | Get-ListRecords
    $results.count | assert -eq 2
    $results[0].User | assert -eq test@hotmail.com
}