I followed this procedure in order to permanently add a path to SumatraPDF using powershell. The last few commands from the link are meant to check that the path has indeed been added.
When I access the path using the following command,
(get-itemproperty -path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH).Path.split(';')
the result includes the path to SumatraPDF
C:\Windows\system32
C:\Windows
C:\Windows\System32\Wbem
C:\Windows\System32\WindowsPowerShell\v1.0\
C:\Windows\System32\OpenSSH\
C:\ProgramData\chocolatey\bin
C:\texlive\2021\bin\win32
C:\Users\921479\AppData\Local\SumatraPDF
However when I access it using the following command,
($env:path).split(';')
the result does not contain the path to SumatraPDF:
C:\Windows\system32
C:\Windows
C:\Windows\System32\Wbem
C:\Windows\System32\WindowsPowerShell\v1.0\
C:\Windows\System32\OpenSSH\
C:\ProgramData\chocolatey\bin
C:\texlive\2021\bin\win32
C:\Users\921479\AppData\Local\Microsoft\WindowsApps
Finally, actually passing sumatrapdf does not works, which indicates to me that the real path is the one accessed using the get-itemproperty command.
Why does the path set in the registry not correspond to the one set in $env:path? Is there a mistake in the procedure shown in the link I followed? How can I correct it?
I should mention I have already tried restarting the shell but it doesn't help.
Note:
See the middle section for helper function Add-Path
See the bottom section for why use of setx.exe should be avoided for updating the Path environment variable.
The procedure in the linked blog post is effective in principle, but is missing a crucial piece of information / additional step:
If you modify environment variables directly via the registry - which, unfortunately, is the right way to do it for REG_EXPAND_SZ-based environment variables such as Path - you need to broadcast a WM_SETTINGCHANGE message so that the Windows (GUI) shell (and its components, File Explorer, the taskbar, the desktop, the Start Menu, all provided via explorer.exe processes) is notified of the environment change and reloads its environment variables from the registry. Applications launched afterwards then inherit the updated environment.
If this message is not sent, future PowerShell sessions (and other applications) won't see the modification until the next logon / reboot.
Unfortunately, there's no direct way to do this from PowerShell, but there are workarounds:
Brute-force workaround - simple, but visually disruptive and closes all open File Explorer windows:
# Kills all explorer.exe processes, which restarts the Windows shell
# components, forcing a reload of the environment from the registry.
Stop-Process -Name explorer
Workaround via .NET APIs:
# Create a random name assumed to be unique
$dummyName = [guid]::NewGuid().ToString()
# Set an environment variable by that name, which makes .NET
# send a WM_SETTINGCHANGE broadcast
[Environment]::SetEnvironmentVariable($dummyName, 'foo', 'User')
# Now that the dummy variable has served its purpose, remove it again.
# (This will trigger another broadcast, but its performance impact is negligible.)
[Environment]::SetEnvironmentVariable($dummyName, [NullString]::value, 'User')
Workaround by calling the Windows API via an ad hoc-compiled P/Invoke call to SendMessageTimeout() in C#, via Add-Type:
While this is a proper solution, it invariably incurs a noticeable performance penalty due to the ad hoc-compilation the first time it is run in a session.
For details, see this blog post.
The approach in the blog post has another problematic aspect:
It retrieves the expanded environment-variable value from the registry, because that is what Get-ItemProperty and Get-ItemPropertyValue invariably do. That is, if directories in the value are defined in terms of other environment variables (e.g., %SystemRoot% or %JAVADIR%), the returned value no longer contains these variables, but their current values. See the bottom section for why this can be problematic.
The helper function discussed in the next section addresses all issues, while also ensuring that the modification takes effect for the current session too.
The following Add-Path helper function:
Adds (appends) a given, single directory path to the persistent user-level Path environment variable by default; use -Scope Machine to target the machine-level definition, which requires elevation (run as admin).
If the directory is already present in the target variable, no action is taken.
The relevant registry value is updated, which preserves its REG_EXPAND_SZ data type, based on the existing unexpanded value - that is, references to other environment variables are preserved as such (e.g., %SystemRoot%), and may also be used in the new entry being added.
Triggers a WM_SETTINGCHANGE message broadcast to inform the Windows shell of the change.
Also updates the current session's $env:Path variable value.
Note: By definition (due to use of the registry), this function is Windows-only.
With the function below defined, your desired Path addition could be performed as follows, modifying the current user's persistent Path definition:
Add-Path C:\Users\921479\AppData\Local\SumatraPDF
If you really want to update the machine-level definition (in the HKEY_LOCAL_MACHINE registry hive, which doesn't make sense with a user-specific path), add -Scope Machine, but not that you must then run with elevation (as admin).
Add-Path source code:
function Add-Path {
param(
[Parameter(Mandatory, Position=0)]
[string] $LiteralPath,
[ValidateSet('User', 'CurrentUser', 'Machine', 'LocalMachine')]
[string] $Scope
)
Set-StrictMode -Version 1; $ErrorActionPreference = 'Stop'
$isMachineLevel = $Scope -in 'Machine', 'LocalMachine'
if ($isMachineLevel -and -not $($ErrorActionPreference = 'Continue'; net session 2>$null)) { throw "You must run AS ADMIN to update the machine-level Path environment variable." }
$regPath = 'registry::' + ('HKEY_CURRENT_USER\Environment', 'HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment')[$isMachineLevel]
# Note the use of the .GetValue() method to ensure that the *unexpanded* value is returned.
$currDirs = (Get-Item -LiteralPath $regPath).GetValue('Path', '', 'DoNotExpandEnvironmentNames') -split ';' -ne ''
if ($LiteralPath -in $currDirs) {
Write-Verbose "Already present in the persistent $(('user', 'machine')[$isMachineLevel])-level Path: $LiteralPath"
return
}
$newValue = ($currDirs + $LiteralPath) -join ';'
# Update the registry.
Set-ItemProperty -Type ExpandString -LiteralPath $regPath Path $newValue
# Broadcast WM_SETTINGCHANGE to get the Windows shell to reload the
# updated environment, via a dummy [Environment]::SetEnvironmentVariable() operation.
$dummyName = [guid]::NewGuid().ToString()
[Environment]::SetEnvironmentVariable($dummyName, 'foo', 'User')
[Environment]::SetEnvironmentVariable($dummyName, [NullString]::value, 'User')
# Finally, also update the current session's `$env:Path` definition.
# Note: For simplicity, we always append to the in-process *composite* value,
# even though for a -Scope Machine update this isn't strictly the same.
$env:Path = ($env:Path -replace ';$') + ';' + $LiteralPath
Write-Verbose "`"$LiteralPath`" successfully appended to the persistent $(('user', 'machine')[$isMachineLevel])-level Path and also the current-process value."
}
The limitations of setx.exe and why it shouldn't be used to update the Path environment variable:
setx.exe has fundamental limitations that make it problematic, particularly for updating environment variables that are based on REG_EXPAND_SZ-typed registry values, such as Path:
Values are limited to 1024 characters, with additional ones getting truncated, albeit with a warning (as of at least Windows 10).
The environment variable that is (re)created is invariably of type REG_SZ, whereas Path is originally of type REG_EXPAND_SZ and contains directory paths based on other environment variables, such as %SystemRoot% and %JAVADIR%.
If the replacement value contains only literal paths (no environment-variable references) that may have no immediate ill effects, but, for an instance, an entry that originally depended on %JAVADIR% will stop working if the value of %JAVADIR% is later changed.
Additionally, if you base the updated value on the current session's $env:Path value, you'll end up duplicating entries, because the process-level $env:Path value is a composite of the machine-level and current-user-level values.
This increases the risk of running into the 1024-character limit, especially if the technique is used repeatedly. It also bears the risk of duplicate values lingering after the original entry is removed from the original scope.
While you can avoid this particular problem by retrieving the scope-specific value either directly from the registry or - invariably in expanded form - via [Environment]::GetEnvironmentVariable('Path', 'User') or [Environment]::GetEnvironmentVariable('Path', 'Machine'), that still doesn't solve the REG_EXPAND_SZ problem discussed above.
Use setx to permanently update an environment variable. Don't hack the registry.
After you invoke setx, just update the Path environment manually in the current session. Powershell: Reload the path in PowerShell
I am looking for a way to change this with a PowerShell script:
The default setting is Quick Access and I want This PC. I looked at some posts but nobody wants something like this.
How can I use PowerShell to change the Folder Options settings?
You need to set the LaunchTo property under the registry key HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced.
To do this, we can use the Set-ItemProperty cmdlet:
# Set up the parameters for Set-ItemProperty
$sipParams = #{
Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced'
Name = 'LaunchTo'
Value = 1 # Set the LaunchTo value for "This PC"
}
# Run Set-ItemProperty with the parameters we set above
Set-ItemProperty #sipParams
There are three possible values this will take, so if you want to open Quick Access or Downloads, set the value to either of those numbers instead:
This PC
Quick Access
Downloads
The value for Downloads seems to be undocumented, but it does work. Any other values will result in an error when opening File Explorer about file associations.
Following the documentation, variables must be set:
[mariadb]
...
ssl_cert = /etc/my.cnf.d/certificates/server-cert.pem
ssl_key = /etc/my.cnf.d/certificates/server-key.pem
ssl_ca = /etc/my.cnf.d/certificates/ca.pem
Translating to windows, as seen in other answers taking precautions for \s and /s escape.
[mariadb]
ssl_cert="C://Users//myUser//MariaDB//SSL_Certs//server-cert.pem"
ssl_key="C://Users//myUser//MariaDB//SSL_Certs//server-key.pem"
ssl_ca="C://Users//myUser//MariaDB//SSL_Certs//ca.pem"
ssl=true
However, after connecting:
show variables like 'have_ssl'; is DISABLED instead of YES.
show session status like 'ssl_cipher'; is empty.
Open services.msc and find the MariaDB service, and take note of the "Log On As" username. Give Read and List folder contents permissions to the "C://Users//myUser//MariaDB//SSL_Certs// directory, to the user name in the "Log On As" column (in my case NETWORK SERVICE).
After this, show variables like 'have_ssl'; was set to YES.
I am working on a project that is buried deep beneath the folders and directories and because of that my powershell's line is half filled with the path to the directory. I want to know if it is possible to get rid of the long string of directory path that is constantly showing.
Add this to your script, then call the function after starting your script.
function prompt {
$p = Split-Path -leaf -path (Get-Location)
"$p> "
}
This way only the "leaf" folder will be shown for the prompt placeholder.
Building on the last answer I usually put the current path in the title bar of the console window and use the history ID for the prompt. Something like:
Function Prompt
{
$cwd = (get-location).Path
$LastHist = ( Get-History)[-1].Id + 1
$Host.UI.RawUI.WindowTitle = "SPoSh_$($PSVersionTable.PSVersion.Major) - $cwd | User: $($env:USERNAME)"
$Host.UI.Write("Green", $Host.UI.RawUI.BackGroundColor, "SPoSh_$($PSVersionTable.PSVersion.Major)")
" #$LastHist : "
}
So this would look like:
"SPoSh_5 #1 : "
I leave a space at the end which makes it easier to double click select a previous command without capturing any of the prompt itself.
Note: If you are working in the regular console the title bar additions let you know where you are without crowding the prompt.
The title bar stuff doesn't show in some of the other non-colsole PowerShell hosts, like VSCode's integrated console, but during script dev the location is usually fairly static, so it's not too much trouble.
How can I create a protocol handler for a powershell script and make the target powershell script receive command line arguments?
And what are the security concerns in doing so?
I thought I write up a decent guide on doing so since the information I found online was lacking some details.
https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85)
First off the security concerns
Any program, website, script etc. that is running on your computer can set off the protocol. There are no authorization checks.
You should NOT create a universal protocol handler. That would be a massive security issue concern. I mean that would enable a program, website, script etc. to run any powershell script or command on your computer.
Creating the protocol handler in Windows registry
The protocol must be registered in Windows Registry. It's a simple task.
I'm calling my powershell protocol handler for pwsh
Step 1: Open the Registry Editor and navigate to
Computer\HKEY_CLASSES_ROOT
For inspiration you can look at Computer\HKEY_CLASSES_ROOT\http to look at how that protocol handler is made.
Step 2: Create the following hierarchy:
Create the key pwsh: [Computer\HKEY_CLASSES_ROOT\pwsh]
Edit the default value of (Default) to URL:pwsh. Remember I call my protocol handler for pwsh, write whatever your is called.
Add a string value with the name URL Protocol and empty data.
It should look like this now:
Create a new key under pwsh, DefaultIcon: Computer\HKEY_CLASSES_ROOT\pwsh\DefaultIcon.
Set the (Default) data field to a filepath that leads to an icon or image. I used the powershell icon for Powershell 7 C:\Program Files (x86)\PowerShell\7-preview\assets\ps_black_32x32.ico.
Then create the keys shell -> open -> command like shown on the image above.
In the key command change the (Default) data value to where powershell is installed and then the powershell script to be run.
When testing I do this: "C:\Program Files\PowerShell\6\pwsh.exe" -noexit -executionpolicy bypass -Command {Write-Host %1}
Note I am using powershell core 6 and your path to powershell is probably different.
You can test to check if it works by opening the run program in Windows(Windows+R).
Expected behavior is the powershell window to open with the text pwsh:Hello Stackoverflow printed.
Step 3: Create a powershell script to handle incoming actions on the protocol.
The production ready data value for the command key: "C:\Program Files\PowerShell\6\pwsh.exe" -noexit -File C:\handleActions.ps1 %1
Param($Argument="") # If the protocol is ran you always at least get the protocol name as an argument. (if using the %1)
[String]
$Argument
function Handle-Actions { # The cmdlet 'Handle-Actions' uses an unapproved verb.
[cmdletBinding()]
param(
[Parameter(Mandatory=$false, Position=0)]
[String]
$Argument
)
$Argumuments = $Argument.Split([Char]0x003F) # Splits by `?`
#Argumnets is now in an array, do whatever you need to next.
$Argumuments | %{
Write-Host $_ # Writes each argument that was seperated by ? to a line
}
}
Handle-Actions -Argument $Argument
Given the run command pwsh:?firstArgument?SecondArgument the script will output:
pwsh:
firstArgument
SecondArgument
To complement your helpful guide with sample code that automates creation of a custom protocol handler:
The following:
Creates a custom URI protocol custom: (rather than pwsh:, given that PowerShell is simply used to implement the protocol) to which an open-ended number of arguments can be passed.
Does so for the current user only (HKEY_CURRENT_USER\Software\Classes) by default; however, it's easy to tweak the code to implement the custom protocol for all users instead (HKEY_LOCAL_MACHINE\Software\Classes), though you'll need to run the code with elevation (as administrator) then.
A handler *.ps1 script is automatically created:
At $env:USERPROFILE\customUriHandler.ps1 in the current-user scenario.
At $env:ALLUSERPROFILE\customUriHandler.ps1 in the all-users scenario.
The handler script simply echoes the arguments passed to it, and it is invoked in a PowerShell script window that is kept open after script execution (-NoExit); tweak the PowerShell command as needed.
The protocol expects its arguments as if it were a shell command, i.e., as a space-separated list of arguments, with argument-individual "..." quoting, if necessary.
The sample command at the end uses Start-Process to invoke the following URI, which you could also submit from the Run dialog (WinKey-R), which passes arguments one, two & three, four:
URI: custom:one "two & three" four
Invocation via Start-Process: Start-Process 'custom:one "two & three" four'
Caveat: If you submit this URI via a web browser's address bar (note: doesn't seem to work with Microsoft Edge), it is URI-escaped, and a single one%20%22two%20&%20three%22%20four argument is passed instead, which would require custom parsing; similarly, submitting from File Explorer's address bar passes one%20two%20&%20three%20four, though note that the " chars. are - curiously - lost in the process.
# Determine the scope:
# Set to $false to install machine-wide (for all users)
# Note: Doing so then requires running with ELEVATION.
$currentUserOnly = $true
if (-not $currentUserOnly) {
net session *>$null
if ($LASTEXITCODE) { Throw "You must run this script as administrator (elevated)." }
}
$ErrorActionPreference = 'Stop'
# The name of the new protocol scheme
$schemeName = 'custom'
$pwshPathEscaped = (Get-Process -Id $PID).Path -replace '\\', '\\'
$handlerScript = ($env:ALLUSERSPROFILE, $env:USERPROFILE)[$currentUserOnly] + "\${schemeName}UriHandler.ps1"
$handlerScriptEscaped = $handlerScript -replace '\\', '\\'
# Create the protocol handler script.
#'
# Remove the protocol scheme name from the 1st argument.
$argArray = $args.Clone()
$argArray[0] = $argArray[0] -replace '^[^:]+:'
# If the 1st argument is now empty, remove it.
if ('' -eq $argArray[0]) { $argArray = $argArray[1..($argArray.Count-1)] }
"Received $($argArray.Count) argument(s)."
$i = 0
foreach ($arg in $argArray) {
"#$((++$i)): [$arg]"
}
'# > $handlerScript
# Construct a temp. *.reg file.
# Target the scope-appropriate root registrykey.
$rootKey = ('HKEY_LOCAL_MACHINE\Software\Classes', 'HKEY_CURRENT_USER\Software\Classes')[$currentUserOnly]
# Determine a temp. file path.
$tempFile = [IO.Path]::GetTempPath() + [IO.Path]::GetRandomFileName() + '.reg'
#"
Windows Registry Editor Version 5.00
[$rootKey\$schemeName]
#="URL:$schemeName"
"URL Protocol"=""
[$rootKey\$schemeName\DefaultIcon]
#="$pwshPathEscaped"
[$rootKey\$schemeName\shell]
#="open"
[$rootKey\$schemeName\shell\open\command]
; === Tweak the PowerShell command line here: ===
#="\"$pwshPathEscaped\" -ExecutionPolicy Bypass -NoProfile -NoExit -File \"$handlerScriptEscaped\" %1"
"# > $tempFile
# Import the *.reg file into the registry.
& {
$ErrorActionPreference = 'Continue'
reg.exe import $tempFile 2>$null
if ($LASTEXITCODE) { Throw "Importing with reg.exe failed: $tempFile"}
}
# Remove the temp. *.reg file.
Remove-Item -ErrorAction Ignore -LiteralPath $tempFile
# ---
# Sample invocation of the new protocol with 3 arguments:
$uri = "$schemeName`:one `"two & three`" four"
Write-Verbose -Verbose "Invoking the following URI: $uri"
Start-Process $uri