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
Categories: