By design, double-clicking (opening) *.ps1
files from the Windows [GUI] shell (in this case: Desktop, File Explorer, and the taskbar, via pinned items) does not execute them - instead they're opened for editing in Notepad or in the PowerShell ISE, depending on the Windows / PowerShell version.
However, since at least Windows 7, the shortcut menu for *.ps1
files contains a Run with PowerShell
command, which does invoke the script at hand; this may be enough for your purposes, but this invocation method has limitations - see the bottom section for details.
If you do want to redefine double-clicking / opening so that it executes *.ps1
scripts, you have two options:
Note:
It's best to limit the redefinition to your user account; otherwise, other users on the same machine may not expect the modified behavior, which can lead to unintentional execution of scripts.
For a given script (as opposed to all .ps1
files), you may alternatively create a shortcut file or batch file that launches it, but that isn't a general solution, as you'd have to create a companion file for each and every .ps1
file you want to run by double-clicking. It does, however, give you full control over the invocation. You can create shortcut files interactively, via File Explorer, as described in this answer, or programmatically, as shown in this answer. Similarly, you may create a companion batch file (.cmd
or .bat
) that invokes your script, because batch file are executed when double-clicked; e.g., if you place a batch file with the same base name as your .ps1
script in the same directory (e.g., foo.cmd
next to foo.ps1
), you can call it from your batch file as follows; -NoExit
keeps the session open:
@powershell.exe -NoExit -File "%~dpn0.ps1" %*
- Note: If you want to bypass the effective execution policy for this call only, place a
-ExecutionPolicy Bypass
argument before -File
.
The methods below also enable direct execution of a .ps1
script from a cmd.exe
console window, synchronously, inside the same window. In other words: You can execute, say, script foo.ps1
directly as such, instead of having to use the PowerShell CLI, say, powershell.exe -File foo.ps1
[Not recommended] GUI method:
Use File Explorer to make PowerShell execute .ps1
files by default:
- Right-click on a
.ps1
file and select Properties
.
- Click on
Change...
next to the Opens with:
label.
- Click on
More apps
on the bottom of the list and scroll down to Look for another app on this PC
- Browse to or paste file path
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
and submit.
This method gives you no control over the specifics of the PowerShell invocation and has major limitations; in effect you'll end up with the following behavior:
Major limitations:
Script paths with embedded spaces and '
chars. cannot be invoked this way, because, even though such paths are passed with double quotes, the latter are in effect stripped by PowerShell, because the path is passed to the implied -Command
parameter, which first strips (unescaped) double quotes from the command line before interpreting the result as PowerShell code - in which case paths with spaces are seen as multiple arguments / paths that contain (an odd number of) '
cause a syntax error.
Note that if you were to select pwsh.exe
instead, the CLI of the cross-platform, install-on-demand PowerShell (Core) 7+ edition, that problem would not arise, because it defaults to the -File
parameter - in which case a double-quoted script-file path is properly recognized.
For the difference between PowerShell CLI calls using -Command
vs. those using -File
, see this answer.
Passing arguments is not supported, which matters if you want to invoke .ps1
files directly from cmd.exe
and need to pass arguments.
The redefinition is only in effect for the current user - which is probably a good thing, as other users may not expect this change, which can result in unwanted execution of scripts.
Whatever execution policy is in effect will be honored; e.g., if Restricted
is in effect, invocation will fail altogether.
As with the default Run in PowerShell
command, the window in which the script runs will automatically close when the script ends - thus, unless the script explicitly prompts the user before exiting, you may not be able to examine its output.
To exercise more control over how PowerShell invokes the script including support for paths with spaces and for passing arguments, use the programmatic method shown in the next section.
Programmatic method:
The code below modifies the registry to (re)define the Open
shortcut-menu command for *.ps1
files, using the same registry keys that are created when you use the GUI method described above.
You can run the code as-is to create a user-level file-type definition that:
uses the executable that runs the current PowerShell session, i.e. powershell.exe
in Windows PowerShell, and pwsh.exe
in PowerShell (Core) 7+.
respects the effective execution policy - add an -ExecutionPolicy
argument to override.
loads the profiles first - add -NoProfile
to suppress loading; this is primarily of interest if you're planning to directly invoke .ps1
files from cmd.exe
, not (just) from File Explorer, in combination with not using -NoExit
.
runs in the script in its own directory
keeps the session open after the script exits - remove -NoExit
to exit the session when the script ends; this is primarily of interest if you're planning to directly invoke .ps1
files from cmd.exe
, not (just) from File Explorer.
If you requirements differ - if you need different CLI parameters and/or you want to use pwsh.exe
, i.e. PowerShell (Core) 7+ instead - tweak the code first, by modifying the $cmd = ...
line below; see the comments above it.
# Specify if the change should apply to the CURRENT USER only, or to ALL users.
# NOTE: If you set this to $true - which is NOT ADVISABLE -
# you'll need to run this code ELEVATED (as administrator)
$forAllUsers = $false
# Determine the chosen scope's target registry
$fileTypeName = 'ps1_auto_file'
$targetRegKeys = [ordered] @{
Extension = "$(('HKCU', 'HKLM')[$forAllUsers]):\Software\Classes\.ps1"
FileType = "$(('HKCU', 'HKLM')[$forAllUsers]):\Software\Classes\$fileTypeName\shell\open\command"
}
# Make sure that the target keys exist.
$targetRegKeys.Values |
ForEach-Object {
if (-not (Test-Path -LiteralPath $_)) {
$null = New-Item -Path $_ -Force -ErrorAction Stop
}
}
# Make sure that no Visual Studio Code override is in place.
# (A "VSCode.ps1" entry in HKEY_CLASSES_ROOT\.ps1\OpenWithProgids)
$vsCodeOverrideKey = "$($targetRegKeys.Extension)\OpenWithProgids"
$vsCodeOverrideKey_Abstract = $vsCodeOverrideKey -replace '^.+?:\\Software\\Classes', 'registry::HKEY_CLASSES_ROOT'
if (Test-Path -LiteralPath $vsCodeOverrideKey) {
Write-Verbose -Verbose "Removing Visual Studio Code override..."
Remove-Item -Recurse -Force -ErrorAction Stop -LiteralPath $vsCodeOverrideKey
}
if (Test-Path -LiteralPath $vsCodeOverrideKey_Abstract) {
# Implies $forAllUsers -eq $false
Write-Warning @"
A HKEY_LOCAL_MACHINE version of key $vsCodeOverrideKey_Abstract exists,
which makes Visual Studio Code preempt the default action.
To remove it, re-run this script with `$forAllUsers set to `$true,
which requires ELEVATION, however - and changes the behavior for ALL users.
"@
}
# Specify the command to use when opening / double-clicking *.ps1 scripts:
# As written here:
# * The script runs in the directory in which it resides.
# * The profiles are loaded (add -NoProfile to change).
# * The current execution policy is respected (add -ExecutionPolicy <policy> to override, if possible)
# * The window stays open after the script exits (remove -NoExit to change)
# For help with all parameters, see https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe
$cmd = "`"$((Get-Process -Id $PID).Path)`" -nologo -noexit -file `"%1`" %*"
# Write to the registry.
Set-ItemProperty -ErrorAction Stop -LiteralPath $targetRegKeys.Extension -Name '(default)' -Value $fileTypeName
Set-ItemProperty -ErrorAction Stop -LiteralPath $targetRegKeys.FileType -Name '(default)' -Value $cmd
Write-Verbose -Verbose "$(('User-level', 'Machine-level')[$forAllUsers]) file-type definition for *.ps1 files successfully updated."
# Additionally, make sure that NO USER OVERRIDE preempts the new definition:
# See if a user override established interactively via File Explorer happens to be defined,
# and remove it, if so.
if ($fileExplorerOverrideKey = Get-Item -ErrorAction Ignore -LiteralPath 'registry::HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.ps1\UserChoice') {
Write-Verbose -Verbose 'Removing File Explorer override...'
# Get the parent key path and the key name
$parentKeyPath = $fileExplorerOverrideKey.PSParentPath -replace '^.+?::\w+\\' # Remove the 'Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER\' prefix
$keyName = $fileExplorerOverrideKey.PSChildName
$key = $null
try {
# Open the *parent* key for writing.
$key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubkey($parentKeyPath, $true)
# Delete the subkey.
# !! Due to the specific permissions assigned by File Explorer to the key
# !! (an additional DENY access-control entry for the current user, for the key itself only, for the 'Set Value' permission),
# !! using the .DeleteSubKey*Tree*() method fails (Remove-Item implicitly uses this method and therefore fails too)
# !! However, since there should be no nested subkeys, using .DeleteSubkey() should work fine.
$key.DeleteSubKey($keyName)
}
catch {
throw
}
finally {
if ($key) { $key.Close()}
}
}
Explanation of the predefined Run in PowerShell
shortcut-menu command:
Update:
- The following applies to Windows 10 only.
- In Windows 11, the relevant registry key is now
HKEY_CLASSES_ROOT\SystemFileAssociations\.ps1\Shell\Windows.PowerShell.Run\Command
, and the command is properly defined with the -File
CLI parameter, albeit without any attempt to set the execution policy on demand - however, the command still does not keep the windows of scripts invoked this way open after they terminate; see this answer for a solution.
It is defined in registry key HKEY_CLASSES_ROOT\Microsoft.PowerShellScript.1\shell\0\Command
as follows:
"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" "-Command" "if((Get-ExecutionPolicy ) -ne 'AllSigned') { Set-ExecutionPolicy -Scope Process Bypass }; & '%1'"
This command is flawed in that it breaks with script-file paths that happen to contain '
characters.
Unless execution policy AllSigned
is in effect - in which case only signed scripts can be executed but are executed without prompting - the command attempts to set the execution policy for the invoked process to Bypass
, which means that any script can be executed, but only after the user responds to a confirmation prompt beforehand (irrespective of whether the script is signed or not, and whether it was downloaded from the web or not).
- At least in earlier Windows 7 releases / PowerShell versions, the command was misdefined[1] in a way that effectively ignored the attempt to set the process' execution policy, which meant that whatever execution policy was persistently configured applied - and no confirmation prompt was shown.
Unless the targeted script explicitly pauses to wait for user input before exiting, the window in which the script will close automatically when the script finishes, so you may not get to see its output.
The targeted script executes in the directory in which it is located as the working directory (current location)
[1] The earlier, broken command definition was "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" "-file" "%1" "-Command" "if((Get-ExecutionPolicy ) -ne AllSigned) { Set-ExecutionPolicy -Scope Process Bypass }"
, which meant what anything after -file "%1"
was passed as arguments to file "%1"
instead of the intended execution of the commands following -Command
; additionally - a moot point - the AllSigned
operand would have need to be quoted.
.ps1
files behaves, but instead shows how to execute a given, single.ps1
file via a dedicated shortcut file that must explicitly be created for it. Note that your command will break with file paths that have spaces - unless you prepend-File
(to also support drag and drop, append%*
) – Mcgregor