PowerShell: Replacing Notepad with Notepad Plus Plus

Update 12/04/2020: there’s a simpler solution – install notepad2!

choco install notepad2 -y

The command above will automatically substitute notepad with the open source version, notepad2.exe. Moreover, the notepad.exe will still show as original. When triggered, it will call the new notepad software instead.

This also addresses the concern we have with a modified notepad.exe being reverted by Windows repairs. The original notepad.exe is actually unmodified.

# replaceNotepadWithNotepadPlus.ps1
# 
# Use case:
# Certain Windows version earlier than Windows 10 or Windows Server 2019 version 1709
# cannot handle Unix newline character 'LF'; thus, the default notepad.exe from those
# computers would render text incorrectly. Since notepad++ has better support for
# Unix file encoding, replacing notepad.exe with notepad++ would help users view
# UTF-8 encoded files as generated from Linux sources.
#
# what this script does:
# 1. Connects to target computer(s)
# 2. Installs Notepad++ if necessary
# 3. Renames C:\windows\system32\notepad.exe to notepad.exe.bak
# 4. Copies and renames notepad++.exe as notepad.exe
# 5. Updates related registry keys
#
# Note: this code is a refactored version originally contributed by Bob Hodges
# Forked from: https://gist.github.com/hodgesb/7226a970240d415e5b3e

$computerNames="$env:computername"
function connectWinRm($computer,$adminCredential=$false,$winRmPort=5985){
    if(!$computer){
        write-warning "Computer name must be specified to initiate a WinRM connection."
        return $false
    }
    # Legacy equivalent to Test-Netconnection
    function checkNetConnection($computername,$port,$timeout=1000,$verbose=$false) {
        $tcp = New-Object System.Net.Sockets.TcpClient;
        try {
            $connect=$tcp.BeginConnect($computername,$port,$null,$null)
            $wait = $connect.AsyncWaitHandle.WaitOne($timeout,$false)
            if(!$wait){
                $null=$tcp.EndConnect($connect)
                $tcp.Close()
                if($verbose){
                    Write-Host "Connection Timeout" -ForegroundColor Red
                    }
                Return $false
            }else{
                $error.Clear()
                $null=$tcp.EndConnect($connect) # Dispose of the connection to release memory
                if(!$?){
                    if($verbose){
                        write-host $error[0].Exception.Message -ForegroundColor Red
                        }
                    $tcp.Close()
                    return $false
                    }
                $tcp.Close()
                Return $true
            }
        } catch {
            return $false
        }
    }
    function enableWinRmRemotely($remoteComputer,$winRmPort,$adminCredential){
        function Check-NetConnection($computername,$port,$timeout=1000,$verbose=$false) {
                $tcp = New-Object System.Net.Sockets.TcpClient;
                try {
                    $connect=$tcp.BeginConnect($computername,$port,$null,$null)
                    $wait = $connect.AsyncWaitHandle.WaitOne($timeout,$false)
                    if(!$wait){
                        $null=$tcp.EndConnect($connect)
                        $tcp.Close()
                        if($verbose){
                            Write-Host "Connection Timeout" -ForegroundColor Red
                            }
                        Return $false
                    }else{
                        $error.Clear()
                        $null=$tcp.EndConnect($connect) # Dispose of the connection to release memory
                        if(!$?){
                            if($verbose){
                                write-host $error[0].Exception.Message -ForegroundColor Red
                                }
                            $tcp.Close()
                            return $false
                            }
                        $tcp.Close()
                        Return $true
                    }
                } catch {
                    return $false
                }
        }
        if (!(get-command psexec -ea SilentlyContinue)){
            # Install Chocolatey
            if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
                [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;
                Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
                }
            choco install sysinternals -y;  
            }            
        write-host 'Attempting to use psexec to enable WinRM remotely...'
        if(!$adminCredential){ # Enable WinRM Remotely
            $null=psexec.exe \\$remoteComputer -s C:\Windows\system32\winrm.cmd qc -quiet; 
        }else{
            $username=$adminCredential.Username
            $password=$adminCredential.GetNetworkCredential().Password
            $null=psexec.exe \\$remoteComputer -u $username -p $password -s C:\Windows\system32\winrm.cmd qc -quiet
            }
        return check-netconnection $remoteComputer $winRmPort
    }

    # If machine is not pingable, wait five minutes
    $fiveMinuteTimer=[System.Diagnostics.Stopwatch]::StartNew()
    do{
        $ping = Test-Connection $computer -quiet
        if($ping -eq $false){sleep 1}
        $pastFiveMinutes=$fiveMinuteTimer.Elapsed.TotalMinutes -ge 5
    }until ($ping -eq $true -or $pastFiveMinutes)
    $fiveMinuteTimer.stop()

    $winRmAvailable=checkNetConnection $computer $winRmPort
    if(!$winRmAvailable){
        write-host "Attempting to enable WinRM on $computer" -ForegroundColor Yellow
        $enableWinRmSuccessful=enableWinRmRemotely $computer
        if($enableWinRmSuccessful){
            write-host "WinRM enabled: $enableWinRmSuccessful"
        }else{
            write-warning "WinRM could not be enabled remotely. WinRM connection aborted."
            return $false
            }
    }
    # Wait for WinRm session prior to proceeding
    #if($session.state -eq 'Opened'){remove-pssession $session}
    do{
        $session=if($adminCredential){
            try{
                New-PSSession -ComputerName $computer -Credential $adminCredential -ea Stop
            }catch{
                New-PSSession -ComputerName $computer -Credential $adminCredential -SessionOption $(new-pssessionoption -IncludePortInSPN)
            }
        }else{
            try{
                New-PSSession -ComputerName $computer -ea Stop
            }catch{
                New-PSSession -ComputerName $computer -SessionOption $(new-pssessionoption -IncludePortInSPN)
            }
        }
        #sleep -seconds 1
        if ($session){
            write-host "Connected to $computer."
            return $session
        }
    } until ($session.state -match "Opened")    
}

function replaceNotepadWithNotepadPlus{    
    function includeApp($appName,$appExe=$False,$version){
        if(!$appExe){$appExe=$appName}
        try{
            $existingExe=get-command $appExe -ea SilentlyContinue
            if(!$existingExe){
                # Include Chocolatey
                if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
                    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
                    Set-ExecutionPolicy Bypass -Scope Process -Force;
                    iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
                }
                choco install $appName -y
                $installed=$null -ne (get-command $appExe -ea SilentlyContinue)
                return $installed
            }else{
                $existingVersion=$existingExe.Version
                write-host "$appName version $existingVersion already exists." -ForegroundColor Green
                return $true
            }
        }catch{
            write-warning $_
            return $false
        }
    }
    
    function renameSystemFile($file,$newName){    
        #write-host "This function currently doesn't deal with UNC paths. Hence, local paths are assumed."
        if(!(test-path $file)){
            write-warning "$file is not accessible."
            return $False
        }
        if(!$newName){
            $newName="$(split-path $file -leaf).bak"
        }
        $newFile="$(split-path $file -parent)\$newName"
        try{
            if(test-path $newFile){
                $newFileInfo=(get-item $newFile).VersionInfo
                $originalFileName=$newFileInfo.OriginalFileName
                $originalVersion=$newFileInfo.ProductVersion
                rename-item $newFile "$originalFileName`_$originalVersion.bak" -force
            }
            if(!(get-command takeown -ea Ignore)){
                write-warning "C:\Windows\system32\takeown.exe is missing"
            }else{
                try {
                    & takeown /f $file
                }catch{
                    write-warning $_
                }
            }
            write-host "Granting $env:username full access to $file..."
            $userAccess = New-Object System.Security.AccessControl.FileSystemAccessRule($env:username,"FullControl","Allow")
            $administratorsAccess=New-Object System.Security.AccessControl.FileSystemAccessRule('Administrators',"FullControl","Allow")
            $acl=Get-ACL $file
            $acl.AddAccessRule($userAccess)
            $acl.AddAccessRule($administratorsAccess)
            Set-Acl $file $acl
            write-host "Renaming $file to $newName..."
            rename-item $file $newName -force
        }catch{
            write-warning $_
        }finally{
            if(!(Test-Path $file) -and (Test-Path $newFile)){
                $success=$true
                write-host "$file has been successfully renamed to $newName" -ForegroundColor Green
            }else{
                $success=$false
                write-warning "$file has NOT been renamed to $newName"
            }
        }
        return $success
    }
    
    # Include notepadPlusPlus
    $appInstalled=includeApp notepadplusplus notepad++
    if(!$appInstalled){
        write-warning "NotepadPlusPlus has not been installed on $env:computername."
        return $false
    }
    # Close Notepad++ or Notepad if either are running.
    Get-process notepad,notepad++ -ErrorAction Ignore | Stop-Process -Force
    
    # Setting paths to default notepad++.exe and SciLexer.dll.
    Try {
        $notepadPlus=Resolve-Path "$($env:systemdrive)\Program Files*\Notepad++\notepad++.exe" -ea 0
        [version]$notepadPlusVersion=(Get-Item $notepadplus).versioninfo.fileversion
        $notepadPlusDLL=Resolve-Path "$($env:systemdrive)\Program Files*\Notepad++\SciLexer.dll" -ea 0
        write-host "Notepad++.exe version $notepadPlusVersion is currently located at '$notepadPlus'"
    }Catch{
        Write-Output "NotePad++.exe is not found in the environmental paths."
        return $false
    }
    $notepadPlusAvailable=(Test-Path $notepadPlus) -and (Test-Path $notepadPlusDLL)
    if(!$notepadPlusAvailable){
        write-warning "Notepad++.exe is not available on $env:computername"
        return $false
    }
    # Replace all notepad.exe instances with its notepadPlus variants
    # Perform this task progressively - roll-back when any item fails
    $expectedLocations="$env:systemroot\","$($env:systemroot)\System32\","$($env:systemroot)\SysWOW64\"
    $notepadFiles=$expectedLocations|%{$_+'notepad.exe'}
    $validNotepadFiles=$notepadFiles|?{test-path $_}
    foreach ($notepadFile in $validNotepadFiles){
        $fileInfo=(get-item $notepadFile).VersionInfo
        $fileAlreadyReplaced=($fileInfo.OriginalFilename -eq 'Notepad++.exe') -and ($fileInfo.ProductVersion -ge $notepadPlusVersion)
        if(!$fileAlreadyReplaced){
            $fileRenameSuccessful=renameSystemFile $notepadFile 'notepad.exe.bak'
            if($fileRenameSuccessful){
                # Copies the NotePad++ file and its dependant DLL file to the current path.
                Copy-Item -Path $notepadPlus -Destination $notepadFile
                Copy-Item -Path $notepadPlusDLL -Destination $(Split-Path $notepadFile -Parent)  
            }else{
                write-host "Rolling back file replacement changes as file rename has failed on $notepadFile..."
                $validNotepadFiles|%{$null=renameSystemFile $(split-path $_ -parent)+'notepad.exe.bak' 'notepad.exe'}
                return $false
            }
        }else{
            write-host "$notepadFile is already a product of Notepad++ version $($fileInfo.ProductVersion)"
        }        
    }
    
    if ($notepadPlusVersion -ge '7.59'){
        # Registry keys for Notepad++ 7.5.9 and above
        write-host "Patching registry for notepad++ version $notepadPlusVersion..."
        $registryNotepad = "REGISTRY::HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\notepad.exe"
        $RegName = "Debugger"
        $RegValue = "`"$notepadPlus`" -notepadStyleCmdline -z"    
        Write-host "NotePad++ version $notepadPlusVersion is above 7.58. Registry patch must be applied..."
        Try{ 
            # Checks to see if the registry key exists before updating values.
            if (Test-Path $registryNotepad){ 
                # The registry key exists, so we create a registry property and value.
                $regValueMatched=(Get-itemproperty $registryNotepad)."$regName" -eq $RegValue
                if(!$regValueMatched){
                    $null=New-ItemProperty -Path $registryNotepad -Name $regName -Value $RegValue -PropertyType String -Force
                }
            }else{
                # The registry key doesn't exist, so we create the registry key prior to creating the property.
                $null=New-Item -Path $registryNotepad -Force
                $null=New-ItemProperty -Path $registryNotepad -Name $regName -Value $RegValue -PropertyType String -Force
            }
        }catch{
            Write-Warning "Failed to apply registry patch. Reverting file renames..."
            $validNotepadFiles|%{$null=renameSystemFile $(split-path $_ -parent)+'notepad.exe.bak' 'notepad.exe'}
            return $false
        }finally{
            write-host "Registry patching completed."
        }
    }else{
        # Since Notepad++ is below 7.59, the registry patch should not exist. 
        # This checks to see if the registry patch exists, and if so it removes it. 
        $regPatchInstallStatus = Get-ItemProperty -Path $registryNotepad -Name $RegName -ErrorAction 0
        if ($regPatchInstallStatus){
            Write-host "Removing incompatible registry patch `r`n$registryNotepad."
            Try{
                $null=Remove-ItemProperty -Path $registryNotepad -Name $regName -Force
            }Catch{
                Write-warning $_
                return $false
            }
        }
    }
    # Run Notepad++ once to preempt an XML error
    $null=& $notepadPlus
    while(!(Get-process notepad++ -ErrorAction Ignore)){
        # Wait until initial trigger has started
        sleep 1
    }
    $null=Stop-Process -name 'notepad++' -Force
    return $true
}

$results=[hashtable]@{}
foreach ($computername in $computernames){
    $session=connectWinRm $computername
    if($session.State -eq 'Opened' -and $session.ComputerName -eq $computerName){
        $result=invoke-command -Session $session -ScriptBlock{
                param($importFunc)
                write-host "Executing function on $env:computername..."
                [scriptblock]::create($importFunc).invoke()
            } -Args ${function:replaceNotepadWithNotepadPlus}
        write-host "$computername`: $result"
        $results+=@{$computername=$result}
        Remove-PSSession $session
    }else{
        write-warning "Unable to connect to $computername"
        $results+=@{$computername=$false}
    }    
}
Write-Output $results

Sample Output:

Connected to TESTWINDOWS1.
Executing function on TESTWINDOWS1...
notepadplusplus version 7.91.0.0 already exists.
Notepad++.exe version 7.91 is currently located at 'C:\Program Files\Notepad++\notepad++.exe'
C:\Windows\System32\notepad.exe is already a product of Notepad++ version 7.91
C:\Windows\SysWOW64\notepad.exe is already a product of Notepad++ version 7.91
Patching registry for notepad++ version 7.91...
NotePad++ version 7.91 is above 7.58. Registry patch must be applied...
Registry patching completed.
TESTWINDOWS1: True

Name                           Value
----                           -----
TESTWINDOWS1                   True

Leave a Reply

Your email address will not be published. Required fields are marked *