pf-file.ps1

function Get-PS_WorkingFolder {
    ( Get-Location -PSProvider 'FileSystem' ).Path
}

function Remove-PSPath_Prefix {
    process {
        if ( $_ ) {
            $_ -replace '.*::'
        }
    }
}

function Get-Path {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $path
    )
    process {
        if ( $null -eq $path ) { return $path }
        if ( $path -is [ScriptBlock] ) { 
            $result = Invoke-Command -ScriptBlock $path
            return $result | Get-Path
        }

        if ( $path -is [string] ) { return $path | Remove-PSPath_Prefix }
        if ( $path -is [System.IO.FileSystemInfo] ) { return $path.FullName | Remove-PSPath_Prefix }
        if ( $path -is [System.Management.Automation.PathInfo] ) { return $path.ProviderPath | Remove-PSPath_Prefix }
        if ( $path | Get-Member -Name 'Path' ) { return $path.Path | Remove-PSPath_Prefix }
        if ( $path | Get-Member -Name 'PSPath' ) { return $path.PSPath | Remove-PSPath_Prefix }
        if ( $path | Get-Member -Name 'DirectoryName' ) { return $path.DirectoryName | Remove-PSPath_Prefix }
        throw "Path cannot be extracted from '$path' "
    }
}
function Get-Path:::Test {
    $null | Get-Path | assert $null
    "c.ps1" | Get-Path | assert "c.ps1"
    { "c.ps1" } | Get-Path | assert "c.ps1"
    { $null } | Get-Path | assert $null

    Get-Path "c.ps1" | assert "c.ps1"
    Get-Path ([PSCustomObject]@{PsPath = 'Microsoft.PowerShell.Core\FileSystem::C:\backups'}) | assert 'C:\backups'
}

function Get-PathItem {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $path
    )
    process {
        if ( ( -not $path ) -or ($path.PSPath)) {
            return $path
        }
        $path = $path | Get-Path 
        $item = ( Get-Item -LiteralPath $path -Force )
        if (-not $item) {
            $Name = Split-Path $path -Leaf
            $parent = Split-Path $path -Parent
            $item = Get-ChildItem -Path $parent | Where-Object { $_.Name -eq $Name }
        }
        $Item
    }
}

function Split-Path_Ex {
    param(
        [Parameter(ValueFromPipeline=$true)]
        $Path,
        [int]$Level = 1,
        [switch]$Parent,
        [switch]$Leaf
    )
    begin {
        if ($Parent) {
            $Level = 1
        }
        if ($Leaf) {
            $Level = -1
        }
    }
    Process {
        [string]$result = $path | Get-Path 
        if ($Level -lt 0) {
            $resultParts = $result.Split([string[]]@('\','/'), [System.StringSplitOptions]::None)[$Level..-1] 
            return [string]::join([IO.Path]::DirectorySeparatorChar,$resultParts)
        }    
        for( $currentParent = $Level; $currentParent -gt 0 -and $result; $currentParent--) {
            $result = Split-Path $result -Parent
        }
        return $result;
    }
}
function Split-Path_Ex:::Test {
    'A\B\C' | Split-Path_Ex -Level -4 | Assert -eq 'A\B\C'
    'A\B\C' | Split-Path_Ex -Level -3 | Assert -eq 'A\B\C'
    'A\B\C' | Split-Path_Ex -Level -2 | Assert -eq 'B\C'
    'A\B\C' | Split-Path_Ex -Level -1 | Assert -eq 'C'
    'A\B\C' | Split-Path_Ex -Leaf     | Assert -eq 'C'
    'A\B\C' | Split-Path_Ex -Level 0  | Assert -eq 'A\B\C'
    'A\B\C' | Split-Path_Ex -Parent   | Assert -eq 'A\B'
    'A\B\C' | Split-Path_Ex           | Assert -eq 'A\B'
    'A\B\C' | Split-Path_Ex -Level 2  | Assert -eq 'A'
    'A\B\C' | Split-Path_Ex -Level 3  | Assert -eq ''
    'A\B\C' | Split-Path_Ex -Level 4  | Assert -eq ''
}

function Get-Url_NoPort {
    param(
        [Parameter(ValueFromPipeline=$true)]
        $uri
    )
    [Uri]$uriInternal = $uri
    $result = $uriInternal.Scheme + '://' + $uriInternal.Host
    return $result
}
function Get-Url_NoPort:::Test {
    Get-Url_NoPort -uri https://CCP-EMP-TST.eastriding.gov.uk:4401 | assert -eq https://CCP-EMP-TST.eastriding.gov.uk
}

function Update-Path_EnsureQuoted {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $path,
        [string[]]$PathBreakers = @(' ')
    )
    process {
        [string]$path = $path | Get-Path 
        if (-not $path) {
            return $path
        }
        if ( $path.StartsWith('"') -and $path.EndsWith('"') ) {
            return $path
        }
        if ($PathBreakers) {
            $breaksFound = $PathBreakers | ForEach-Object { $path.Contains($_) }
            if (-not $breaksFound) {
                return $path
            }
        }
        return $path | Update-String_Enclose '"'
    }
}
function Update-Path_EnsureQuoted:::Test {
    'a b' | Update-Path_EnsureQuoted | assert -eq '"a b"'
    'ab' | Update-Path_EnsureQuoted | assert -eq 'ab'
}

function Assert-FileExists {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $path
    )
    begin {
        $missing = $()
    }
    process {
        $path = Get-Path $path
        if ($path -and -not (Test-Path -path $path)) {
            $missing += $path
        }
    }
    end {
        if ( -not $missing ) { return }
        $fileList = $missing -join "`n`t" 
        throw [System.IO.FileNotFoundException] "File not found.`n`t $fileList"
    }
}

function Get-Path_Ancestors {
    param(
        [Parameter(ValueFromPipeline=$true)]
        $path,
        [int]$MaxCount = [Int]::MaxValue,
        [int]$skipCount = 1
    )
    begin {
        $separators = [char[]]@('/','\')
    }
    process {
        [string]$current = Get-Path -path $path
        $next = $last = $current.Length
        while ( ( $last -gt 0 ) -and (  $MaxCount -gt 0 ) ) {
            $next = $current.LastIndexOfAny($separators,$last - 1)
            $result = $current.Substring(0,$last)
            if ($result.LastIndexOfAny($separators) -ne ($result.Length - 1) ) {
                if ($skipCount -gt 0) {
                    $skipCount--
                }
                else {
                    $result
                    $MaxCount--
                }
            }
            $last = $next
        }
    }
}
function Get-Path_Ancestors:::Example {
    '\\srv\a\' | Get-Path_Ancestors | assert -eq @('\\srv')
    '\\srv\a\b\c\d' | Get-Path_Ancestors | assert -eq @('\\srv\a\b\c','\\srv\a\b','\\srv\a','\\srv')
    'srv\a/b\c\d' | Get-Path_Ancestors | assert -eq @('srv\a/b\c','srv\a/b','srv\a','srv')
}

Function Compress-Name ($maxLength = 240) {
  begin { $fso = New-Object -ComObject Scripting.FileSystemObject }
  process {
    $path = Get-Path $_
    if ( $path.length -lt $maxLength ) {
        return $path
    }

    $parents = $path | Get-Path_Ancestors
    $parentExists = $parents | Where-Object { Test-Path $_ } | Select-Object -First 1
    $folder = $fso.GetFolder($parentExists)
    $newPath = $path | Update-Prefix -prefix $parentExists -replace $folder.ShortPath
    return $newPath
  }
}
function Compress-Name:::Example {
    # requires an real existing path
    $testFile = Get-ChildItem -path "C:\Windows\Microsoft.NET\assembly\GAC_MSIL" -filter *.* -Recurse -ErrorAction SilentlyContinue | 
        Where-Object { $_.FullName.Length -gt 250 } | Sort-Object LastWriteTime | Select-Object -First 1 
    $longPath = $testFile.FullName
    $longPath
    $result = $longPath | Compress-Name -maxLength 50
    $result.length
}

function Expand-Path_LocalDrives {
    begin {
        $pattern = '*:\'
        $Drives = Get-PSDrive -PSProvider FileSystem
        $localDrives = $Drives | Where-Object DisplayRoot -notLike '\\*' | 
            Where-Object Root -Like $pattern
    }
    process {
        $current = $_
        if (-not $current) {
            return
        }
        if ( $current.StartsWith($pattern) ) {
            $localDrives.Root | ForEach-Object { $current | Update-Prefix -prefix $pattern -replace $_ }
        }
        elseif ( $current.StartsWith('*:.\') ) {
            $localDrives | ForEach-Object {
                    if ( $_.CurrentLocation ) 
                        { $_.Root + $_.CurrentLocation + '\' } 
                    else { $_.Root } } | 
                ForEach-Object { $current | Update-Prefix -prefix '*:.\' -replace $_ }
        }
        else {
            $current
        }
    }
}

# Resolve-Path that do not generate an exception when the path is not found
function Expand-Path {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $Path,
        [Switch]$AllVariables,
        $any = '*'
    )
    begin {
        function Env($name) {
            $result = Get-Item -Path ('Env:' + $name) -ErrorAction SilentlyContinue
            if ($result.value) {
                return $result.value
            }
            return '$Env:' + $name
        }
    }
    process {
        $result = Get-Path $Path
        if ($AllVariables) {
            $result = $result | Update-String_Enclose '"'
            $result = Invoke-Expression $result
        }
        $result = $result | Expand-Path_LocalDrives
        $result = $result | Where-Object { Test-Path $_ } | Resolve-Path
        $result | Get-Path
    }
}
function Expand-Path:::Test{
    ( $null | Expand-Path | Measure-Object ).Count -eq 0  | assert
    $env:TEMP | Expand-Path | assert $env:TEMP
    '*:\windows' | Expand-Path | Select-Object -First 1 | assert 'C:\windows'
    ( '*:.\' | Expand-Path | Measure-Object ).Count -gt 0 | assert
    ( '.\*\..\*' | Expand-Path | Measure-Object ).Count -gt 0 | assert

    '$($env:SystemRoot)\Sy$($any)\dr*s\*c\hosts' | Expand-Path -AllVariables `
        | assert -eq 'C:\Windows\System32\drivers\etc\hosts'
    '$(Env("SystemRoot"))\Sy$($any)\dr*s\*c\hosts' | Expand-Path -AllVariables `
        | assert -eq 'C:\Windows\System32\drivers\etc\hosts'
}

function Get-PathExistingParent {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $Path
    )
    process {
        $path = Get-Path $Path
        while ( $Path -and -not ( Test-Path $Path ) ) {
            $Path = Split-Path $Path -Parent
        }
        return $Path
    }
}
function Get-PathExistingParent:::Test {
    Get-PathExistingParent -Path C:\Windows\System\XXX\YYY | assert -eq 'C:\Windows\System'
    Get-PathExistingParent -Path SSS:\Windows\System\XXX\YYY | assert -eq ''
}

function Get-FolderWithFiles {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $folder,
        $filter 
    )
    process{
        $folder | Get-ChildItem -Recurse -Filter $filter | 
            ForEach-Object { $_.DirectoryName } | Select-Object -Unique
    }
}

function Get-FolderWithFiles:::Example{
    Get-FolderWithFiles -folder $folder -filter *.configuration.xml
}

function Find-InPath ([String[]]$toFindList, [String[]]$lookin, [switch]$required) {
    if (-not $lookin) {
        $lookin = @( $home, $env:ChocolateyInstall , ${env:ProgramFiles(x86)}, $env:ProgramFiles )
        $paths = $env:Path.Split(';')
        $lookin = $paths + $lookin | Where-Object { $_ } | Select-Object -Unique
    }
    
    $lookin = $lookin | Expand-Path

    foreach($toFind in $toFindList) {
        if ($toFind) {
            $toFind = '\' + $toFind
        }
        $locations = $lookin | ForEach-Object { $_ + $toFind } | 
            Where-Object { test-path $_ } | Select-Object -First 1
        if ($locations) {
            return $locations
        }
    }
    if ($required -and -not $locations){
        throw "Not found [$toFindList] "
    }
}
function Find-InPath:::Test {
    Find-InPath notepad.exe -lookin 'C:\Windows\system32' | assert 'C:\Windows\system32\notepad.exe'
    Find-InPath notepad.exe | assert 'C:\Windows\system32\notepad.exe'
    Find-InPath nothere.exe, notepad.exe | assert 'C:\Windows\system32\notepad.exe'
}

function Get-LastFile_ByName_InFolders {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $folder,
        $filter 
    )
    begin {
        $lastFile = @{}
    }
    process{
        $folder = Get-Path $folder
        $files = Get-ChildItem -Path $folder -Filter $filter -Recurse
        foreach ($file in $files) {
            $lastFile[$file.Name] = $file.Fullname
        }
    }
    end {
        $lastFile.values | Sort-Object
    }
}
function Get-LastFile:::Example {
    $patches = Get-ChildItem -path c:\code\tbc-2\src\Deploy\Patches -Recurse -Filter *.patch -Directory
    $patches | Get-LastFile_ByName_InFolders -filter *.xsn
    $patches | Get-LastFile_ByName_InFolders -filter *.dll
    $patches | Get-LastFile_ByName_InFolders -filter *.wsp
}

function Resolve-Exe($cmd) {
    $cmdInfo = Get-Command $cmd -ErrorAction SilentlyContinue 
    if ( $cmdInfo ) {
        if ( $cmdInfo.Source ) {
            $cmdInfo.Source
        }
        else {
            $candidatePathList = @()
            $candidatePathList += $env:Path.Split(';')
            $candidatePathList += "$env:windir\System32"
            $candidatePathList = $candidatePathList | Select-Object -Unique
            
            foreach ($candidatePath in $candidatePathList) {
                $result = "$candidatePath\$($cmdInfo.Name)"
                if (Test-Path($result)) {
                    return $result
                }
            }
        }
    }
}
function Resolve-Exe:::Test {
    Resolve-Exe notepad | assert -eq "$env:windir\system32\notepad.exe"
    Resolve-Exe ping | assert -eq "$env:windir\system32\ping.exe"
}

function Find-App ([string]$toFind, [switch]$required) {

    $result = Resolve-Exe $toFind
    if ( $result ) {
        return $result
        #return get-item -LiteralPath $result -Force
    }
     
    if (-not $toFind.Contains(".")) {
        $toFind = "$toFind.exe"
    }

    try {
        # $lookin = @( $env:ChocolateyInstall , ${env:ProgramFiles(x86)}, $env:ProgramFiles, "$env:windir\System32" )
        # $result = $lookin | Get-ChildItem -fi $toFind -re -ea SilentlyContinue | Select-Object -First 1
    }
    catch { 
        $err = $_
        Write-Warning $err
    }
    if ($required -and -not $result) {
        throw "Not found [$toFind] "
    }
    return $result
}
function Find-App:::Example {
    Find-App notepad++
    Find-App robocopy
}

function Find-Path_Up ([string]$filename, [string]$folder, [switch]$Required ) {
    if ( -not $folder ) {
        $folder = Split-Path ( Get-PSCallStack )[1].ScriptName -Parent
    }
    $folder = ( Resolve-Path $folder ).Path
    $found = $false
    while ( $folder ) {
        $result = "$folder\$filename"; 
        $folder = Split-Path $folder -Parent;
        if ( Test-Path $result ) { 
            $found = $true
            ( Resolve-Path $result ).Path 
        }
    }
    if ($Required -and -not $found) {
        throw "File '$filename' is required but not found in '$folder' or any of its parents"
    }
}
function Find-Path_Up:::Example {
    $expected = 'C:\Windows\System32\PING.EXE'

    Find-Path_Up 'ping.*' -folder C:\Windows\System32 | assert -eq $expected
    Find-Path_Up 'ping.*' -folder C:\Windows\System32\drivers\etc | assert -eq $expected
    { Find-Path_Up 'ping.*' -folder C:\Windows -Required } | assert -throw "Required"
}

function Add-Path {
    Param(
        [string]$path,
        [Parameter(ValueFromPipeline=$true)]
        [string]$result,
        [string]$separator = ';', 
        [switch]$append 
    )
    if (-not $result) { return $path }
    if (-not $path) { return $result }
    $splitted = $result.Split($separator)
    if ( $path -in $splitted ) {
        return $result
    }
    if ($append) {
        return $result + $separator + $path
    }
    return $path + $separator + $result
}
function Add-Path:::Test {
    "A;B" | Add-Path "x:\"  | assert -eq 'x:\;A;B'
    "X:\;B" | Add-Path "x:\" | assert -eq 'x:\;B'
    "" | Add-Path "x:\"  | assert -eq 'x:\'
}

function Get-ScriptPath ([switch]$parent, [switch]$rootScript ) {
    $allStack = Get-PSCallStack
    $CommonScriptFolder = split-path $allStack[0].ScriptName -Parent
    $stack = $allStack | skip-until { $_.ScriptName -notlike "$CommonScriptFolder*"  }
    $frame = & { if ($rootScript) { 
                    $stack | Where-Object ScriptName | 
                    Select-Object -Last 1 } 
                else { $stack | Select-Object -First 1 } }
    $result = if ( $frame.ScriptName ) { $frame.ScriptName }
              else {
                if ($psISE) {
                    $psISE.CurrentFile.FullPath
                }
                else {
                    ( ( Get-PS_WorkingFolder ) | Join-Path -ChildPath '\.' ) 
                }
              }
    
    return & { if ($parent) { Split-Path $result -Parent } else { $result } }
}

function Find-Path_Up_Extended ([string]$filename, [int]$MaxResults = 1, [int]$MaxLevels = 32, [ref]$testedPaths) {
    begin {
        $levelCountDown = $MaxLevels
        $preTestedPaths = if ($testedPaths) { $testedPaths.Value } else { @() } 
    }
    Process {
        $folder = Get-Path $_
        while ( $folder -and ( $levelCountDown -gt 0 ) ) {
            $levelCountDown--
            $result = "$folder\$filename"
            $folder = Split-Path $folder -Parent

            if ($result -in $preTestedPaths) {
                continue
            }
            if ( Test-Path $result ) { 
                ( Resolve-Path $result ).Path
                $MaxResults--;
                if ( $MaxResults -eq 0 ) {
                    return
                }
            }
            if ($testedPaths) { $testedPaths.Value += $result }
        }
    }
}
# $tested = @()
# 'C:\GIT\abc3\Build.CI\Commit' | Find-Path_Up_Extended '*.ps1' -testedPaths ([ref]$tested) -MaxResuls 10

function Find-WalkUp ([string]$filename, [switch]$Required, [int]$MaxResults = 1, [int]$MaxLevels = 32 `
     ,[string[]]$startLookingIn, [switch]$psWorkingFolder, [switch]$rootScript, [switch]$scriptFolder, [string[]]$finallyLookIn) {

    function CalculateLookIntoLocations {
        [string[]]$lookInto = Coalesce $startLookingIn @()
        if ($psWorkingFolder) {
            $lookInto += Get-PS_WorkingFolder
        }
        if ($rootScript) {
            $lookInto += Get-ScriptPath -parent -rootScript
        }
        if ($scriptFolder) {
            $lookInto += Get-ScriptPath -parent
        }
        if ($finallyLookIn) {
            $lookInto = $lookInto + $finallyLookIn 
        }
        $lookInto = $lookInto | Select-Object -Unique
        return $lookInto
    }

    $lookInto = CalculateLookIntoLocations

    $testedPaths = @()

    $MaxLevels = [Math]::Max(0, $MaxLevels)
    if ( $MaxResults -lt 0 ) {
        return
    } 

    $results = $lookInto | Find-Path_Up_Extended -filename $filename -testedPaths ([ref]$testedPaths) -MaxLevels $MaxLevels

    if (-not $results -and $Required) {
        $msg = "File '$filename' not found`nTested Paths:`n" + ( $testedPaths | Out-String )
        throw [System.IO.FileNotFoundException] $msg
    }

    $results
}
# Find-WalkUp "BuildName.input.ps1" -scriptFolder
# Find-WalkUp "bin" -scriptFolder
# . ( Find-Script 'TFS.ps1' -Required )
# Ensure to enclose in parenthesis

function Remove-FolderAsync {
    Param (
        [Parameter(ValueFromPipeline=$true)]
        $path
    )

    # Is faster to rename the folder and then remove it async
    process {
        $folder = Get-Path $path
        if ( Test-Path $folder ) {
            Write-Host "Removing '$folder'"
            $parentfolder = [System.IO.Path]::GetDirectoryName($folder)
            $foldername = [System.IO.Path]::GetFileName($folder)
            $newName = "$($foldername)_$( [Guid]::NewGuid() ).bak.tmp"
            Get-Item -LiteralPath $folder -Force | Rename-Item -NewName $newName
            $job = start-job -ScriptBlock { Remove-Item $args[0] -Recurse -Force } -ArgumentList "$parentfolder\$newName"
            Write-Verbose $job
        }
    }
}

function Expand-8_3_Path {
    [CmdletBinding()] Param( [Parameter(ValueFromPipeline=$true)] $path )
    process {
        if ( ( -not $path ) -or ( -not $path.contains('~') ) ) {
            return $path
        }
        $result = Invoke-InLocation -path $path -script {
            ( Get-Location ).Path
        }
        return $result
    }
}
function Expand-8_3_Path:::Example {
    Expand-8_3_Path -path 'C:\Progra~1'
}

function New-Folder_EnsureExists {
    [CmdletBinding()]
    Param (
        [Parameter(ValueFromPipeline=$true)]
        $folder,
        [switch]$PassThru, 
        [switch]$New,
        [switch]$Parent
    )
    process {
        if ( $Parent ) {
            $folder = Split-Path $folder -Parent
        }
        if ( $New ) {
            $folder | Remove-FolderAsync
        }
        if ( Test-Path $folder ) {
            if ($PassThru) {
                $result = Expand-8_3_Path -path $folder
                $result = Get-Item -LiteralPath $result -Force
            }
        }
        else {
            Write-Host "Creating '$folder'"
            $result = mkdir $folder
        }
        if ($PassThru) { 
            $result
        }
    }
}
function New-Folder_EnsureExists:::Example {
    $testFolder = "$env:temp\PS\New-Folder_EnsureExists.Example"
    New-Folder_EnsureExists -folder $testFolder
}

function Get-UNC_FileName {
    param (
        [Parameter(ValueFromPipeline=$true)]
        $filename
    )
    process {
        function Get-DefaultSmbShare {
            Get-PSDrive -PSProvider FileSystem | Select-Object Name, Root | 
                ForEach-Object { [PSCustomObject]@{'Path'=$_.Root; 'Name'="$($_.Name)$"} }
        }
    
        if (-not $SharesCache ) {
            try {
                Import-Module SmbShare
                $global:SharesCache = Get-SmbShare | Where-Object { -not [string]::IsNullOrWhiteSpace($_.Path) } | 
                    Select-Object -Property Path, Name
            }
            catch {
                $global:SharesCache = Get-DefaultSmbShare
            }
        }
        $share = $SharesCache | Get-LongestPrefix $filename 'Path'
    
        $uncfilename = $filename | Update-Prefix $share.Path ( $share.Name + '\' )
        $uncfilename = '\\' + $env:COMPUTERNAME + '\' + $uncfilename  
        # $result = "file://$filename"
        return $uncfilename
    }
}
function Get-UNC_FileName:::Test {
    Get-UNC_FileName -filename C:\Users | assert -eq "\\$env:computername\C$\Users"
    'C:\Users' | Get-UNC_FileName | assert -eq "\\$env:computername\C$\Users"
}

function Get-Files_Ignore($path, $ignoreList) {
    $condition = @()
    foreach ($ignore in $ignoreList) {
        $condition += [ScriptBlock]::Create( { $_.FullName -notlike "*\$ignore\*"  }.ToString().Replace('$ignore',$ignore) )
        $condition += [ScriptBlock]::Create( { $_.FullName -notlike "*\$ignore"  }.ToString().Replace('$ignore',$ignore) )
    }
    $predicate = $condition | Join-Script '-and' {$true}

    Get-ChildItem -Path $path -Recurse -Directory | Where-Object $predicate
}

function Update-Path_ReplaceFileSpecialChars($replaceWith = '_') {
    begin {
        $specialChars = @('/', '\', ':', '~', '?', '*', '<', '>', '|')
        # '.' is not included as is valid for extensions and as additional separator
    }
    process {
        $result = $_
        foreach ($char in $specialChars ) {
            $result = $result.Replace($char,$replaceWith)
        } 
        return $result
    }
}        
function Update-Path_ReplaceFileSpecialChars:::Test {
    'c:\abc.txt' | Update-Path_ReplaceFileSpecialChars | assert 'c__abc.txt'
}

function Add-FilenameSuffix ($suffix) {
    begin {
        if ( $null -eq $suffix ) {
            $suffix = ''
        }
    }
    Process {
        $filename = $_
        if ( [string]::IsNullOrWhiteSpace($filename) ) {
            return $filename + $suffix
        }
        $dir = [System.IO.Path]::GetDirectoryName($filename)
        if ($dir) { $dir += '\' }
        $ext = [System.IO.Path]::GetExtension($filename);
        $name = [System.IO.Path]::GetFileNameWithoutExtension($filename);
        return $dir + $name + $suffix + "$ext"
    }
}
function Add-FilenameSuffix:::Test {
 'abc.txt', 'abc', ' ', '','c:\d2\d1\abc.xml'  | Add-FilenameSuffix '.EXT' | 
    assert 'abc.EXT.txt', 'abc.EXT', ' .EXT', '.EXT', 'c:\d2\d1\abc.EXT.xml'
}

function Get-SmbShareMap {
    param (
        [Parameter(ValueFromPipeline=$true)]
        $computerName,
        $CachePath,
        [Switch]$IncludeShortName
    )
    process {
        if ( -not $computerName ) { return }

        $hostDSNName = [System.Net.Dns]::GetHostByName($computerName).HostName
        $hostName = $hostDSNName.Split('.')[0]

        try {
            $cim = New-CimSession -ComputerName $hostDSNName
            try {
                $shares = Get-SmbShare -CimSession $cim | Where-Object Path
            }
            finally {
                $cim | Remove-CimSession
            }
        }
        catch {
            # Try first to obtain shares, and if fails try to get it from the cache
        }

        if ($CachePath -and -not $shares) {
            $CacheFile = "$CachePath\$computerName.FolderShares.Cache.json"
            if ( test-path $CacheFile ) {
                $json = Get-Content -Raw -Path $CacheFile
                $result = ConvertFrom-Json $json
                return $result
            }
        }

        $shares = $shares | Sort-Object Path -Descending | Where-Object ScopeName -eq '*'

        $result = $shares | ForEach-Object { 
            $local = $_.Path | Update-Suffix '\'
            if ($IncludeShortName) {
                [PSCustomObject]@{ 
                    Local = $local
                    Remote = "\\$hostName\" + $_.Name } 
            }
            [PSCustomObject]@{ 
                Local = $local
                Remote = "\\$hostDSNName\" + $_.Name } 
        }

        if ($CacheFile) {
            $json = ConvertTo-Json -InputObject $result
            Set-Content -Path $CacheFile -Value $json
        }

        return $result
    }
}
function Get-SmbShareMap:::Example {
    'VmHostServer3' | Get-SmbShareMap
    'VmHostServer2' | Get-SmbShareMap -IncludeShortName
    'localhost' | Get-SmbShareMap
}

function SmbShareRemotePath {
    begin {
        $shareList = $env:COMPUTERNAME | Get-SmbShareMap -IncludeShortName
    }
    process {
        $path = Get-Path $_
        if ( -not $path ) { return }
        if ( $path.StartsWith('\\' ) ) {
            return $path
        }
        foreach ( $share in $shareList ) {
            $prefixPath = $share.Local
            $sharePath = $prefixPath + '\'
            if ( $path.StartsWith( $sharePath, [System.StringComparison]::InvariantCultureIgnoreCase ) -or ( $path -eq $prefixPath ) ) {
                $path | Update-Prefix -prefix $prefixPath -replace $share.Remote
            }
        }
    }
}
function SmbShareRemotePath:::Example {
     $path = 'C:\Windows\System32'
     $path | SmbShareRemotePath
}

function SmbShareLocalPath {
    Param (
        [Parameter(ValueFromPipeLine=$true)]
        $path,
        $computername = $env:COMPUTERNAME
    )
    begin {
        $shareList = $computername | Get-SmbShareMap -IncludeShortName
    }
    process {
        $path = Get-Path $Path
        if ( -not $path ) { return }
        if ( -not $path.StartsWith('\\' ) ) {
            return $path
        }
        foreach ( $share in $shareList ) {
            $prefixPath = $share.Remote
            $sharePath = $prefixPath + '\'
            if ( $path.StartsWith( $sharePath, [System.StringComparison]::InvariantCultureIgnoreCase ) -or ( $path -eq $prefixPath ) ) {
                $path | Update-Prefix -prefix $prefixPath -replace $share.Local
            }
        }
    }
}
function SmbShareLocalPath:::Example {
     $path = '\\VmHostServer2\Nuget\abc'
     $path | SmbShareLocalPath
}

function Get-ItemUse {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $path
    )
    process {
        $path = Get-Path $path
        if (-not $path) { return }
        $path = Update-Path_EnsureQuoted -path $path
        $handle = Invoke-Exe handle -accepteula -u $path
        $itemUses = $handle | Get-Regex_Match -regex '(?<pname>.+)\s+pid:\s(?<ProcessId>\d+)\s+type:\s(?<type>\w+)\s+(?<user>\S+)\s+(?<handle>\w+):\s(?<file>.+)'
        $itemUses
    }
}

function Get-ProcessUsingItem {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $path
    )
    process {
        $itemUses = Get-ItemUse -path $path
        Get-Process | Where-Object Id -In $itemUses.ProcessId
    }
}
function Get-ProcessUsingItem:::Example {
    $ErrorActionPreference = 'stop'

    $foldername = [Guid]::NewGuid().ToString() + ".txt"
    $file = "$env:TEMP\$foldername"
    
    Set-Content $file 'SOMETHING'

    $job = Start-Job -ScriptBlock {
        Write-Host $pid
        $lockedFile = [System.io.File]::Open($using:file, 'Open', 'Read', 'None')
        Read-Host 'Press ENTER to release file'
        $lockedFile.Close()
        }

    if (-not ( wait-until -condition { $job.State -eq 'Blocked' } ) ) {
        throw 'Failed'
    }

    $processUsingIt = Get-ProcessUsingItem -path $file
    # Expected Exceotion
    { Remove-Item $file -Force -Recurse } | assert -throw '*'

    $processUsingIt | Stop-Process -Force

    if (-not ( wait-until -condition { -not (Get-ProcessUsingItem -path $file) } ) ) {
        throw 'Failed'
    }

    #Should work
    Remove-Item $file -Force -Recurse

    Stop-Job $job
}

function Get-LastWriteTime_Max {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $path
    )
    process {
        $path = Get-Path $path
        $path | Get-ChildItem -Recurse -File | 
            Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1
    }
}

function Copy-File {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true, Mandatory=$true)]
        $filePath,
        [string]$FolderFrom, 
        [Parameter(Mandatory=$true)]
        [string]$FolderTo, 
        [string]$tempSuffix = '.wip',
        [Switch]$PassThru )
    process {
        $filePath = Get-Path $filePath
        if (-not $filePath ) { return }

        if (-not $FolderFrom) {
            $FolderFrom = Split-Path $filePath -Parent
        }

        $destinationFilePath = $filePath | Update-Prefix -prefix $FolderFrom -replace $FolderTo -Required
        $destinationFilePath = $destinationFilePath | Compress-Name
        $destinationFile = if (test-path $destinationFilePath) { 
            get-item -LiteralPath $destinationFilePath -Force
        } else { $null }
        
        $filePath = $filePath | Compress-Name

        $file = Get-Item -LiteralPath $filePath -Force
        if (($null -eq $destinationFile) -or ($destinationFile.LastWriteTime -lt $file.LastWriteTime))
        {
            $destinationFileName = Split-Path $destinationFilePath -Leaf
            $destinationFolderPath = Split-Path $destinationFilePath -Parent
            New-Folder_EnsureExists $destinationFolderPath
            
            $destinationFilePathTemp = $destinationFilePath
            if ($tempSuffix) {
                $destinationFilePathTemp += $tempSuffix
            }
            
            if ( test-path $destinationFilePath ) {
                Remove-Item $destinationFilePath -Force -Verbose
            }
            if ( test-path $destinationFilePathTemp ) {
                Remove-Item $destinationFilePathTemp -Force -Verbose
            }

            $newFile = Copy-item -Path $filePath -Destination $destinationFilePathTemp -Force -Verbose -PassThru
            if ($tempSuffix -and ($newFile | Test-Path)) {
                Rename-Item -Path $destinationFilePathTemp -NewName $destinationFileName -Verbose
            }
            if ($PassThru){
                return $newFile
            }
        }
    }
}
function Copy-File:::Example {
    $srcFolder = "$env:TEMP\copy-file-test-src"
    $destFolder = "$env:TEMP\copy-file-test-dest"
    #Expand 8.3 name
    $srcFolder =  New-Folder_EnsureExists $srcFolder -New -PassThru  | Get-Path
    Set-Content -Path "$srcFolder\a.txt" 'aaaa'
    get-childitem $srcFolder -recurse -File | Copy-File -FolderFrom $srcFolder -FolderTo $destFolder
}

function Copy-Folder {
    param (
        $FolderFrom, 
        $FolderTo, 
        $like = '*', 
        [string]$notlike = [Guid]::NewGuid(), 
        [Switch]$ReturnFiles
    )

    $startTime = [System.Diagnostics.Stopwatch]::StartNew()
    $FolderFrom = Resolve-Path $FolderFrom | get-path
    New-Folder_EnsureExists $FolderTo
    $FolderTo = Resolve-Path $FolderTo | Get-Path
    $copiedFiles = Get-ChildItem $FolderFrom -Recurse -File -Force | 
        Where-Object FullName -like $like |
        Where-Object FullName -notlike $notlike |
        Copy-File -FolderFrom $FolderFrom -FolderTo $FolderTo -PassThru
    $startTime.Stop()
    Write-Host "Copy-Folder Finished in '$($startTime.Elapsed)' from '$FolderFrom' to '$FolderTo' "
    if ($ReturnFiles) {
        $copiedFiles
    }
    else {
        return $FolderTo
    }
}

function Get-Item_Where_Mode {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $path,
        [switch]$Not,
        [switch]$Directory,
        [switch]$Archive,
        [switch]$Hidden,
        [switch]$NotContentIndexed,
        [switch]$ReadOnly,
        [switch]$Encrypted
    )
    process {
        $path = Get-Path $path
        if ( -not $path ) { return }
        if ( -not ( test-path $path ) ) { return $false }
        $item = Get-Item -LiteralPath $path -Force
        $attr = ( $item.Attributes -split ', '  )
        
        $result = ( ( -not ( $Directory.IsPresent ) -or ($attr -contains 'Directory') ) `
            -and ( -not ( $Archive.IsPresent ) -or ($attr -contains 'Archive') ) `
            -and ( -not ( $Hidden.IsPresent ) -or ($attr -contains 'Hidden') ) `
            -and ( -not ( $NotContentIndexed.IsPresent ) -or ($attr -contains 'NotContentIndexed') ) `
            -and ( -not ( $ReadOnly.IsPresent ) -or ($attr -contains 'ReadOnly') ) `
            -and ( -not ( $Encrypted.IsPresent ) -or ($attr -contains 'Encrypted') ) ) 
        if ($Not.IsPresent) { $result = -not $result}
        if ($result) { return $item }
    }
}
function Get-Item_Where_Mode:::Example {
    $i = Get-Item_Where_Mode -path C:\temp\test.txt -Archive
    $i.Mode
    $i.Attributes
}

function Test-File_Access {
    [CmdLetBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeLine=$true)]
        $Path,
        [Switch]$Read, 
        [Switch]$Write
    )
    process {
        $path = Get-Path $path
        $mode = [System.IO.FileMode]::Open
        
        $access = if ($Read) {
                [System.IO.FileAccess]::Read
            } else { if ($Write) {
                [System.IO.FileAccess]::Write
            }
            else { 
                [System.IO.FileAccess]::Read
            }
            }

        try {
            $fileStream = New-Object 'System.IO.FileStream' -ArgumentList $path, $mode, $access
        }
        catch [System.UnauthorizedAccessException] {
            return $false
        }
        finally {
            if ($fileStream) {
                $fileStream.Close()
            }
        }
        return $true
    }
}
function Test-FileInUse:::Example {
    Test-FileInUse -Path 'D:\CCPH-8\Virtual Hard Disks'
}

function Get-FileNamePart_Max {
    [CmdLetBinding()]
    param(
        [Parameter(Mandatory=$true)]
        $filter,
        [Switch]$AsNumber,
        $Default = $null,
        [Parameter(Mandatory=$true, ValueFromPipeLine=$true)]
        $Path
    )
    begin{
        $last = $Default
        if ( $AsNumber -and ( $null -eq $last )) {
            $last = 0
        } 
    }
    process {
        $Path = Get-Path -path $Path
        $name = Split-Path $Path -Leaf
        $nameMatch = $name | Get-Like_Match -like $filter  
        if ( $nameMatch ) {
            $current = $nameMatch.1
            if ($AsNumber) {
                $current = [int]$current
            }
            if ( -not ( $last -gt $current ) ) {
                $last = $current
            }
        }
    }
    end{
        $last
    }
}
function Get-FileNamePart_Max:::Test {
    @() | Get-FileNamePart_Max -filter 'file_(AAA)_(*)' | Assert -eq $null
    @() | Get-FileNamePart_Max -filter 'file_(AAA)_(*)' -AsNumber | Assert -eq 0
    @('file_(AAA)_(001)','file_(AAA)_(003)') | Get-FileNamePart_Max -filter 'file_(AAA)_(*)' | Assert -eq '003'
    @('file_(AAA)_(003)','file_(AAA)_(001)') | Get-FileNamePart_Max -filter 'file_(AAA)_(*)' | Assert -eq '003'
    @('file_(AAA)_(003)','file_(AAA)_(001)') | Get-FileNamePart_Max -filter 'file_(AAA)_(*)' -AsNumber | Assert -eq 3
}

function Compare-ByFolderAndFileName {
    Param(
        $lhs, $rhs
    )

    [string]$lhsPath = Get-Path -path $lhs
    [string]$rhsPath = Get-Path -path $rhs
    [char[]]$separators = @('/','\',':','.')
    $lhsParts = $lhsPath.Split($separators)
    $rhsParts = $rhsPath.Split($separators)

    for($i = 0; $i -lt $lhsParts.Count; $i++) {
        if ( $i -ge $rhsParts.Count ) {
            return 1
        }
        if ( $lhsParts[$i] -gt $rhsParts[$i] ) {
            return 1
        }
        if ( $lhsParts[$i] -lt $rhsParts[$i] ) {
            return -1
        }
    }
    if ( $i -eq $rhsParts.Count ) {
        return 0
    }
    if ( $i -gt $rhsParts.Count ) {
        throw 'Assert : Unexpected condition reached'
    }
    return -1
}
function Compare-ByFolderAndFileName:::Example {
    Compare-ByFolderAndFileName -lhs c:\a\b -rhs c:\a\b | assert -eq 0
    Compare-ByFolderAndFileName -lhs c:\a\b -rhs c:\a\b\c | assert -eq -1
    Compare-ByFolderAndFileName -lhs c:\a\b\c -rhs c:\a\b | assert -eq 1
    Compare-ByFolderAndFileName -lhs c:\a\b\c.xml -rhs c:\a\b\c1.xml | assert -eq -1
}

function Get-Ignore_Files {
    $extList = @('bak','csv','log','txt','tmp','lock','svn','git','Unencrypted.*')
    $ignoreContent = $extList | ForEach-Object { "*.$_" } 
    $ignoreContent += '* - Copy.*'
    $ignoreContent += '* - Copy (*).*'
    $ignoreContent += '*.*_*_*_*_*.sitemap'
    $ignoreContent
}

function Get-Text_Filter {
    $textFilter = @('*.config', '*.txt', '*.md', '*.cs*', '*.*proj', '*.sln', '*.nuspec', '*.bgi','*.sitemap','*.browser', '*.cnf', '*.script*'
        '*.htm*', '*.xml', '*.json', '*.ini', '*.js', '*.css','*.p*1', '*.sql', '*.resx', '*.aspx','*.*html','*.asp','*.svc'
        '*.gitignore', '*.gitconfig', '*.gitattributes','*.gitmodules','*.bat', '*.cmd', '*.master', '*.config','*.asax','*.xml','*.xsd','*.ascx','*.rsd','*.rdl'
            )
    $textFilter
}

function Invoke-InLocation { 
    param (
        [Parameter(ValueFromPipeline=$true)]
        $path, 
        [ScriptBlock]$script
    )
    $path = Get-Path $path
    if (-not $path) {
        . $script
        return
    }

    Push-Location $path 
    $path = Resolve-Path $path | Get-Path
    $currentDirectory = [System.IO.Directory]::GetCurrentDirectory()
    try {
        [System.IO.Directory]::SetCurrentDirectory($path)
        Write-Host "Invoke-InLocation '$path'"
        . $script
    }
    finally {
        [System.IO.Directory]::SetCurrentDirectory($currentDirectory)
        Pop-Location
        Write-Host "Invoke-InLocation Reverted to '$currentDirectory'"
    }
}

function Get-WebFile ([Uri] $url, [string] $folder, [string] $file) {
    if ([string]::IsNullOrWhiteSpace($file)) {
        $file = split-path $url.AbsolutePath -Leaf
    }
    if (![string]::IsNullOrWhiteSpace($folder)) {
        $file = Join-Path $folder $file 
    }
    if ( test-path $file ) { 
        return $file; 
    }

    New-Folder_EnsureExists -folder $folder

# Invoke-WebRequest -Uri $url -OutFile $file -Verbose

    $web = new-object System.Net.WebClient
    $web.DownloadFile($url,$file)
    return $file
}
function Get-WebFile:::Test {
    $src = 'C:\tmp'
    $uri = [Uri]'http://vm-ccpla.civica.root.local:8080/repository/download/OneTrunk512_Product_Build/latest.lastSuccessful/C360UpdatePackage.zip?guest=1'
    $file = Get-WebFile -url $uri -folder $src
    Write-Verbose $file
}

function Expand-ZipFile($file, $destination, $filter) {
    try {
        Add-Type -Assembly System.IO.Compression.FileSystem
        $archive = [System.IO.Compression.ZipFile]::OpenRead($file)
        $toUnzip = $archive.Entries | Where-Object FullName -Like $filter
        $toUnzip | ForEach-Object {
            $folder = Split-Path $_.Fullname -Parent
            $folder = Join-Path $destination $folder
            New-Folder_EnsureExists -folder $folder
            $fileDestination = Join-Path $folder $_.Name
            Write-Host "Extracting to '$fileDestination'"
            [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $fileDestination, $true)
        }
    }
    finally {
        if ($archive) {
            $archive.Dispose()
        }
    }
}
function Expand-ZipFile:::Example {
    Expand-ZipFile -file $file -filter '*.dacpac' -destination 'c:\tmp\Core'
}

function Rename-Item_Replace {
    param (
        [Parameter(ValueFromPipeline=$true)]
        $item,
        $currentValue,
        $newValue
    )
    process {
        $newName = $item.Name.Replace($currentValue,$newValue)
        $newFullName = ( Split-Path $item.FullName -Parent ) + "\$newName"
        if ( ( $newFullName -ne $item.FullName ) -and ( test-path $newFullName ) ) {
            # Remove only when it does not have the same name to rename
            Remove-Item $newFullName -Force -Verbose -Recurse
        }
        if ( $item.Name -cne $newName ) {
            if ($item.Name -eq $newName) {
                $item = $item | Rename-Item -NewName ([Guid]::NewGuid()) -Verbose -Force -PassThru
            }
            $item | Rename-Item -NewName $newName -Verbose -Force
        }
    }
}

Function New-SymLink ($link, $target, [switch]$hard)
{
    Remove-SymLink $link
    $directoryFlag = if (test-path -pathtype container $target) { " /d" }
    $hardlinkFlag = if ($hard) { " /H" }
    invoke-expression "cmd /c mklink $hardlinkFlag $directoryFlag '$link' '$target' "
}

Function Remove-SymLink ($link)
{
    if (-not (test-path $link) ) { return }

    $removeCommand = if (test-path -pathtype container $link) { "rmdir" } else { "del" }
    invoke-expression "cmd /c $removeCommand '$link'"
}