Skip to content

Instantly share code, notes, and snippets.

@nohwnd
Last active March 14, 2024 13:57
Show Gist options
  • Save nohwnd/5c07fe62c861ee563f69c9ee1f7c9688 to your computer and use it in GitHub Desktop.
Save nohwnd/5c07fe62c861ee563f69c9ee1f7c9688 to your computer and use it in GitHub Desktop.
Remove built-in version of Pester 3 (or -All) from Windows 10 Program Files and Program Files (x86).
#Requires -RunAsAdministrator
function Uninstall-Pester ([switch]$All) {
if ([IntPtr]::Size * 8 -ne 64) { throw "Run this script from 64bit PowerShell." }
#Requires -RunAsAdministrator
$pesterPaths = foreach ($programFiles in ($env:ProgramFiles, ${env:ProgramFiles(x86)})) {
$path = "$programFiles\WindowsPowerShell\Modules\Pester"
if ($null -ne $programFiles -and (Test-Path $path)) {
if ($All) {
Get-Item $path
}
else {
Get-ChildItem "$path\3.*"
}
}
}
if (-not $pesterPaths) {
"There are no Pester$(if (-not $all) {" 3"}) installations in Program Files and Program Files (x86) doing nothing."
return
}
foreach ($pesterPath in $pesterPaths) {
takeown /F $pesterPath /A /R
icacls $pesterPath /reset
# grant permissions to Administrators group, but use SID to do
# it because it is localized on non-us installations of Windows
icacls $pesterPath /grant "*S-1-5-32-544:F" /inheritance:d /T
Remove-Item -Path $pesterPath -Recurse -Force -Confirm:$false
}
}
Uninstall-Pester
@nohwnd
Copy link
Author

nohwnd commented Dec 8, 2019

Updated to use SID because Windows localizes name of Administrators group in non-english installations.

@jasonrush
Copy link

Might be worth having it run twice, one with the path included and one with the 32-bit path (I just ran into this myself):
C:\Program Files (x86)\WindowsPowerShell\Modules\Pester

@nohwnd
Copy link
Author

nohwnd commented Apr 11, 2020

Thanks for the tip. This also won’t remove installations in your profile, and in everyone elses profiles. Would that be desirable behavior as well? For context, this was originally written just to get rid of the Pester that shipped with Windows and was “locked” by trusted installer.

@jasonrush
Copy link

Note the path I mentioned was not in my profile. It seems like Pester 3.4.0 was installed 32-bit on my (windows 10) system so when I tried running your gist code, it was not catching it. When I ran the same code but added the " (x86)" into the path, it successfully removed the 32-bit copy of Pester.

@nohwnd
Copy link
Author

nohwnd commented Apr 11, 2020

I did notice that. 🙂 I meant , it won't remove in the path you mentioned, and also won't remove installations from your profile, and other users profile.

@nohwnd
Copy link
Author

nohwnd commented Apr 11, 2020

Updated to do that, will only work correctly when executed from x64 powershell, because of how env variables are defined, if anyone wants to fix that be my guest :)

@jasonrush
Copy link

Gotcha! And I like it! Only question I have (might be because I'm on my phone right now) is where the $suffix variable comes from in the path(s)?

@nohwnd
Copy link
Author

nohwnd commented Apr 11, 2020

Thanks for pointing it out, it should not be there.

@SebastianSchuetze
Copy link

Will newer versions be shipped with new windows updates?

@nohwnd
Copy link
Author

nohwnd commented May 19, 2020

Doubt that.

@clemmesserli
Copy link

Not sure if this makes it any 'cleaner' but here's my approach to finding any pre-existing path as we also were finding Pester in multiple folder locations especially if someone had also installed PowerShell v7:

$modules = Get-Module -Name Pester -ListAvailable
$pesterPaths = (get-item $modules.modulebase).parent.Fullname | Sort-Object -Unique
foreach ($pesterPath in $pesterPaths) {
....
}

@jcoryatjr
Copy link

jcoryatjr commented Feb 4, 2021

@clemmesserli wouldn't this also take any version in the users profile directory too?

One thought to make the 64 bit/admin transition smoother, replace the top #Requires -RunAsAdministrator with this:

$isAdmin = (New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)

if ( ( ( $pshome -like "*syswow64*" ) -and ( ( Get-WmiObject Win32_OperatingSystem ).OSArchitecture -like "64*" ) ) -or -not $isAdmin ) {
    write-warning "Restarting script under 64 bit powershell as admin"
 
    # relaunch this script under 64 bit shell as adminstrator
    $CommandLine = "-NoLogo -NoProfile -NoExit -File `"$( $MyInvocation.MyCommand.Path )`" $( $MyInvocation.UnboundArguments )"
    Start-Process -FilePath "$( $pshome -replace "syswow64", "System32" )\powershell.exe" -Verb Runas -ArgumentList $CommandLine
 
    # This will exit the original powershell process. This will only be done in case of an x86 process on a x64 OS.
    exit
}

And delete the second #Requires -RunAsAdministrator

Grabbed this from https://gist.github.com/talatham/ad406d5428ccec641f075a7019cd29a8 and added the -Verb parameter.

@jasonrush
Copy link

@jcoryatjr Note that your version would fail on any 32-bit only systems. While it should be rare to nonexistent, it is a possibility to consider.

@jcoryatjr
Copy link

jcoryatjr commented Feb 10, 2021

@jasonrush good point. I spent some time and refactored into a true cmdlet using advanced function syntax. The module name is Utils and must be installed in a location located within $env:PSModulePath. The file structure is:

  • Utils
    • Utils.psd1
    • Utils.psm1
    • /Public
      • Uninstall-Pester.ps1
    • /Private

The Utils.psd1

@{

ModuleVersion = "1.0.0"

GUID = "e38b14a6-0d85-4be5-869e-38ed7e270126"

Author = "Your Name Here"

CompanyName = "My Company"

Copyright = "(c) 2021 My Company All rights reserved."

RootModule = "Utils.psm1"

}

Utils.ps1 file

Get-ChildItem -Filter *.ps1 -Path "$PSScriptRoot\public","$PSScriptRoot\private" -Recurse | ForEach-Object { . $_.FullName; Write-Verbose "Loaded $_.BaseName" }
Get-ChildItem -Filter *.ps1 -Path "$PSScriptRoot\public" -Recurse | ForEach-Object { Export-ModuleMember $_.BaseName; Write-Verbose "Exported $_.BaseName" }

Uninstall-Pester.ps1 file

function Uninstall-Pester {
    [CmdletBinding()]
    [OutputType([ bool ])]
    param(
        [Parameter()]
        [switch]
        $All = $false
    )
    
    begin {
        try {
            Write-Verbose -Message "[$(( Get-Date ).TimeOfDay )][$( $MyInvocation.MyCommand )]-Begin Enter"
            $result = $true
            $pesterPath = 'WindowsPowerShell\Modules\Pester'

            if ( -not $All ) {
                $pesterPath += '\3.*'
            }

            # See if there is anything to do
            $paths = @()
            foreach ( $path in @(${env:ProgramFiles}, ${env:ProgramFiles(x86)} ) ) {
                if ( $path -and ( Test-Path -Path "$path\$pesterPath" ) ) {
                    $paths += ( Get-ChildItem -Path "$path\$pesterPath" -Directory -ErrorAction SilentlyContinue ).FullName
                }
            }

            if ( -not $paths.Count ) {
                Write-Warning "There are no Pester$( if ( -not $All ) { " 3" } ) installations in Program Files and Program Files (x86)"
                $result = $false
                return
            }
        
            $isAdmin = (New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
            $is32BitProcess = $pshome -like "*syswow64*"
            $is64BitOS = ( Get-WmiObject Win32_OperatingSystem ).OSArchitecture -like "64*"
            $filePath = $null
            
            switch ( $true ) {
                { ( $is32BitProcess -and -not $is64BitOS ) -or ( -not $isAdmin -and $is64BitOS ) } {
                    Write-Warning "Restarting script under 64 bit powershell as admin"
                 
                    # relaunch this script under 64 bit shell as adminstrator
                    $filePath = "$( $pshome -replace "syswow64", "System32" )\powershell.exe"
                    break
                 }
                 { -not $isAdmin } {
                     Write-Warning "Restarting script using admin privs"
                    $filePath = 'powershell.exe'
                 }
            }
            
            if ( $filePath ) {
                Start-Process -FilePath $filePath -Verb RunAs -ArgumentList "-NoLogo -NoExit -Command `" Import-Module -name Utils; Uninstall-Pester $( if( $All ) { "-All" } )  `""
                $result = $false
                return
            }
        }
        catch {
            $result = $false
            Write-Verbose -Message  "[$(( Get-Date ).TimeOfDay )][$( $MyInvocation.MyCommand )] Exception:`r`n$_"
            throw $_
        }
        finally {
            Write-Verbose -Message "[$(( Get-Date ).TimeOfDay )][$( $MyInvocation.MyCommand )]-Begin Exit"
        }
    }

    process {
        try {
            Write-Verbose -Message "[$(( Get-Date ).TimeOfDay )][$( $MyInvocation.MyCommand )]-Process Enter"
            if ( -not $result ) {
                return
            }

            foreach ($path in $paths) {
                takeown /F $path /A /R
                icacls $path /reset
                # grant permissions to Administrators group, but use SID to do
                # it because it is localized on non-us installations of Windows
                icacls $path /grant "*S-1-5-32-544:F" /inheritance:d /T
                Remove-Item -Path $path -Recurse -Force -Confirm:$false -ErrorAction SilentlyContinue
            }
        
        }
        catch {
            $result = $false
            Write-Verbose -Message  "[$(( Get-Date ).TimeOfDay )][$( $MyInvocation.MyCommand )] Exception:`r`n$_"
            throw $_
        }
        finally {
            Write-Verbose -Message "[$(( Get-Date ).TimeOfDay )][$( $MyInvocation.MyCommand )]-Process Exit"
        }
    }
    
    end {
        try {
            Write-Verbose -Message "[$(( Get-Date ).TimeOfDay )][$( $MyInvocation.MyCommand )]-End Enter"
            return $result
        }
        catch {
            Write-Verbose -Message  "[$(( Get-Date ).TimeOfDay )][$( $MyInvocation.MyCommand )] Exception:`r`n$_"
            throw $_
        }
        finally {
            Write-Verbose -Message "[$(( Get-Date ).TimeOfDay )][$( $MyInvocation.MyCommand )]-End Exit"
        }
    }
}

@nohwnd
Copy link
Author

nohwnd commented Mar 4, 2021

@jcoryatjr my version is geared more towards non-interactive use. So I like that it fails when you are not admin. But thanks for sharing, hopefully someone will find it useful.

@Hot12345
Copy link

@jasonrush good point. I spent some time and refactored into a true cmdlet using advanced function syntax. The module name is Utils and must be installed in a location located within $env:PSModulePath. The file structure is:

  • Utils

    • Utils.psd1

    • Utils.psm1

    • /Public

      • Uninstall-Pester.ps1
    • /Private

The Utils.psd1

@{

ModuleVersion = "1.0.0"

GUID = "e38b14a6-0d85-4be5-869e-38ed7e270126"

Author = "Your Name Here"

CompanyName = "My Company"

Copyright = "(c) 2021 My Company All rights reserved."

RootModule = "Utils.psm1"

}

Utils.ps1 file

Get-ChildItem -Filter *.ps1 -Path "$PSScriptRoot\public","$PSScriptRoot\private" -Recurse | ForEach-Object { . $_.FullName; Write-Verbose "Loaded $_.BaseName" }
Get-ChildItem -Filter *.ps1 -Path "$PSScriptRoot\public" -Recurse | ForEach-Object { Export-ModuleMember $_.BaseName; Write-Verbose "Exported $_.BaseName" }

Uninstall-Pester.ps1 file

function Uninstall-Pester {
    [CmdletBinding()]
    [OutputType([ bool ])]
    param(
        [Parameter()]
        [switch]
        $All = $false
    )
    
    begin {
        try {
            Write-Verbose -Message "[$(( Get-Date ).TimeOfDay )][$( $MyInvocation.MyCommand )]-Begin Enter"
            $result = $true
            $pesterPath = 'WindowsPowerShell\Modules\Pester'

            if ( -not $All ) {
                $pesterPath += '\3.*'
            }

            # See if there is anything to do
            $paths = @()
            foreach ( $path in @(${env:ProgramFiles}, ${env:ProgramFiles(x86)} ) ) {
                if ( $path -and ( Test-Path -Path "$path\$pesterPath" ) ) {
                    $paths += ( Get-ChildItem -Path "$path\$pesterPath" -Directory -ErrorAction SilentlyContinue ).FullName
                }
            }

            if ( -not $paths.Count ) {
                Write-Warning "There are no Pester$( if ( -not $All ) { " 3" } ) installations in Program Files and Program Files (x86)"
                $result = $false
                return
            }
        
            $isAdmin = (New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
            $is32BitProcess = $pshome -like "*syswow64*"
            $is64BitOS = ( Get-WmiObject Win32_OperatingSystem ).OSArchitecture -like "64*"
            $filePath = $null
            
            switch ( $true ) {
                { ( $is32BitProcess -and -not $is64BitOS ) -or ( -not $isAdmin -and $is64BitOS ) } {
                    Write-Warning "Restarting script under 64 bit powershell as admin"
                 
                    # relaunch this script under 64 bit shell as adminstrator
                    $filePath = "$( $pshome -replace "syswow64", "System32" )\powershell.exe"
                    break
                 }
                 { -not $isAdmin } {
                     Write-Warning "Restarting script using admin privs"
                    $filePath = 'powershell.exe'
                 }
            }
            
            if ( $filePath ) {
                Start-Process -FilePath $filePath -Verb RunAs -ArgumentList "-NoLogo -NoExit -Command `" Import-Module -name Utils; Uninstall-Pester $( if( $All ) { "-All" } )  `""
                $result = $false
                return
            }
        }
        catch {
            $result = $false
            Write-Verbose -Message  "[$(( Get-Date ).TimeOfDay )][$( $MyInvocation.MyCommand )] Exception:`r`n$_"
            throw $_
        }
        finally {
            Write-Verbose -Message "[$(( Get-Date ).TimeOfDay )][$( $MyInvocation.MyCommand )]-Begin Exit"
        }
    }

    process {
        try {
            Write-Verbose -Message "[$(( Get-Date ).TimeOfDay )][$( $MyInvocation.MyCommand )]-Process Enter"
            if ( -not $result ) {
                return
            }

            foreach ($path in $paths) {
                takeown /F $path /A /R
                icacls $path /reset
                # grant permissions to Administrators group, but use SID to do
                # it because it is localized on non-us installations of Windows
                icacls $path /grant "*S-1-5-32-544:F" /inheritance:d /T
                Remove-Item -Path $path -Recurse -Force -Confirm:$false -ErrorAction SilentlyContinue
            }
        
        }
        catch {
            $result = $false
            Write-Verbose -Message  "[$(( Get-Date ).TimeOfDay )][$( $MyInvocation.MyCommand )] Exception:`r`n$_"
            throw $_
        }
        finally {
            Write-Verbose -Message "[$(( Get-Date ).TimeOfDay )][$( $MyInvocation.MyCommand )]-Process Exit"
        }
    }
    
    end {
        try {
            Write-Verbose -Message "[$(( Get-Date ).TimeOfDay )][$( $MyInvocation.MyCommand )]-End Enter"
            return $result
        }
        catch {
            Write-Verbose -Message  "[$(( Get-Date ).TimeOfDay )][$( $MyInvocation.MyCommand )] Exception:`r`n$_"
            throw $_
        }
        finally {
            Write-Verbose -Message "[$(( Get-Date ).TimeOfDay )][$( $MyInvocation.MyCommand )]-End Exit"
        }
    }
}

Thanks your script worked out for to uninstall the module. And installing manually the newer version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment