eryksun предоставляет отличные указатели в комментариях по этому вопросу, и ваши правки, основанные на них, показывают путь к установке универсального средства запуска с поддержкой shebang-line-линий для сценариев без расширения.сделал исполняемым, добавив .
к $env:PATHEXT
.
Что следует помнить об этом подходе:
PowerShell в настоящее время (начиная с PowerShell Core)6.2.0) неизменно выполняет файлы без расширений в новом окне консоли , что делает эту конфигурацию бесполезной в PowerShell - она работает, как и ожидалось, с cmd.exe
, хотя.
Механизм представляет потенциальную угрозу безопасности , поскольку любой простой текстовый файл без расширения, имеющий строку shebang, эффективно становится исполняемым, потенциально обходя функции безопасности, ориентированные на файлы, имеющиерасширения, которые известны как исполняемые.
Реализация операции определения типа файла по умолчанию с помощью скрипта [PowerShell] неизменно требует создания дочернего процесса с интерпретатором файла скрипта,что в данном случае означает вызов powershell.exe
с параметром -File
.Стоимость запуска powershell.exe
нетривиальна, что задерживает выполнение .
Если вы do хотите реализовать этот универсальныймеханизм, в конце концов, см. скрипт
Install-ShebangSupport.ps1
внизу.
Учитывая вышесказанное, вот более легкий,Специфичный для Python подход , основанный на автоматическом создании отдельных *.ps1
сценариев-оболочек для Python-сценариев без расширения строки :
Это использует тот факт, что PowerShellразрешает выполнение своих собственных *.ps1
файлов сценариев только по имени файла.
Ограничения :
Вам необходимо запустить генерацию сценария-оболочкисценарий (напечатан ниже), по крайней мере, один раз, и каждый раз, когда вы добавляете новые сценарии Python без расширения.
Возможно, наблюдатель файловой системы может использоваться для запуска сценария генерации, ноустановка этого - нетривиальное усилие.
On tС другой стороны, сценарии-оболочки выполняются быстрее, чем универсальное решение на основе типов файлов, так как не задействован дополнительный экземпляр PowerShell (дочерний процесс).
Запустите следующий сценарий из каталога, в котором расширение-меньше скриптов Python расположены [1] :
Get-ChildItem -File | Where-Object Extension -eq '' | % {
if ((Get-Content -LiteralPath $_.fullname -First 1) -match '^#!.*\bpython') {
@'
py.exe ($PSCommandPath -replace '\.ps1$') $Args; exit $LASTEXITCODE
'@ > ($_.FullName + '.ps1')
}
}
Для каждого скрипта Python без расширения somescript
создается сопутствующий файл somescript.ps1
, который передает somescript
вСредство запуска Python py.exe
вместе с любыми аргументами командной строки;exit $LASTEXTICODE
обеспечивает прохождение кода выхода py.exe
.
Как отмечает eryksun, py.exe
должен иметь возможность интерпретировать строку shebang для вызова соответствующего исполняемого файла Python.
Если вы не используетене хотите загромождать вашу систему оболочкой файлов , автоматически генерировать функций в качестве альтернативы , но учтите, что вам придется загружать их в каждый сеансбыть доступным, как правило, через файл $PROFILE
:
Get-ChildItem -File | Where-Object Extension -eq '' | % {
if ((Get-Content -LiteralPath $_.FullName -First 1) -match '^#!.*\bpython') {
Invoke-Expression @"
Function global:$($_.Name) {
py.exe "$($_.FullName)" `$Args
}
"@
}
}
Примечание :
Это сделает текущий каталог без расширенияСценарии Python доступны , как если бы они находились в каталоге, указанном в $env:PATH
- независимо от того, указан ли там текущий каталог.
Каждый целевой сценарий Pythonжестко закодированы в функцию с тем же именем и будут неизменно предназначаться для этого сценария.
- В отличие от этого, подход файла-сценария оболочки
*.ps1
позволяет целенаправленный вызов в данномкаталог, с чем-то вроде.\foo
.
Это конкретное использование Invoke-Expression
безопасно - определять функции на основе расширяемых строк - но, как правило, следует избегать Invoke-Expression
.
Скрипт Install-ShebangSupport.ps1
для установки универсальной поддержки прямого выполнения сценариев на основе shebang-line без расширений в Windows:
Сценарий поддерживает установку на уровне current-user (по умолчанию или с -Scope CurrentUser
) или на уровне all-users (с -Scope AllUsers
, требуется запуск от имени администратора).
Предполагая присутствие в текущем каталоге, запустите Get-Help .\Install-ShebangSupport
для получения базовой помощи.
При вызове сценария без аргументов выводится запрос на подтверждение с подробной информацией о необходимых изменениях в системе; Ctrl-C может использоваться для отмены без установки; передача -Force
выполняет установку без запроса подтверждения.
Чтобы удалить позже, введите -Uninstall
; обратите внимание, что вы должны соответствовать (подразумеваемому) значению -Scope
, используемому во время установки.
Замечание по реализации : определение типа файла без расширения с помощью cmd.exe
внутренних команд assoc
и ftype
неизменно вступает в силу для всех пользователей , потому что определения хранятся в реестре в HKEY_LOCAL_MACHINE\Software\Classes
; поэтому для вызова всегда требуется повышение прав (административные привилегии).
Однако можно создавать определений на уровне пользователя путем прямого манипулирования реестром, что и используется этим сценарием, также для определений на уровне компьютера.
Примечание. Подсветка синтаксиса в коде ниже не работает, но работает.
<#
.SYNOPSIS
Support for direct execution of extension-less script files with shebang lines
on Windows.
.DESCRIPTION
For details, invoke this script without arguments: the confirmation prompt
will show the required modifications to your system. Submit "N" to opt out
of the installation.
Note that extension-less files that do not have a shebang line will open in
the default text editor.
.PARAMETER Scope
Whether to install support for the current user only (the default) or
for all users (requires invocation as admin).
.PARAMETER Uninstall
Uninstalls previously installed support.
Note that the (implied) -Scope value must match the one that was used during
installation.
.PARAMETER Force
Bypasses the confirmation prompt that is shown by default.
.EXAMPLE
Install-ShebangSupport
Installation for the current user that requires answering a confirmation prompt.
.EXAMPLE
Install-ShebangSupport -Scope AllUsers -Force
Installation for all users without confirmation prompt. Requires invocation
as admin.
.EXAMPLE
Install-ShebangSupport -Uninstall
Uninstallation for the current user with confirmation prompt.
#>
[CmdletBinding(PositionalBinding=$false)]
param(
[ValidateSet('CurrentUser', 'AllUsers')]
[string] $Scope = 'CurrentUser'
,
[switch] $Force
,
[switch] $Uninstall
)
$ErrorActionPreference = 'Stop'; Set-StrictMode -Version 1
if ($env:OS -ne 'Windows_NT') { Throw ("This script can only run on Windows.")}
# ---------------------- BEGIN: Internal helper functions
# === INSTALL
function install {
Write-Verbose ('Installing shebang-script support for {0}:' -f ('the current user', 'ALL users')[$forAllUsers])
# NOTE:
# * assoc and ftype only ever operate on HKEY_LOCAL_MACHINE\Software\Classes, not HKEY_CURRENT_USER\Software\Classes - both on reading and writing.
# * *HKEY_CURRENT_USER*-level definitions DO work, but *neither assoc nor ftype report them or can update them*.
# Therefore, we perform direct registry manipulation below.
Write-Verbose 'Creating file type for extension-less file names via the registry...'
# Map the "extension-less extension", "." to the name of the new file type to be created below.
# Caveat: Sadly, New-Item -Force blindly recreates the registry key if it already exists, discarding
# all existing content in the process.
$key = New-Item -Force -Path $regkeyExtensionToFileType
$null = New-ItemProperty -LiteralPath $key.PSPath -Name '(default)' -Value $fileTypeName
# Define the new file type:
$key = New-Item -Force -Path "$regkeyFileType\Shell\Open\Command"
$null = New-ItemProperty -LiteralPath $key.PSPath -Name '(default)' -Value ('powershell.exe -noprofile -file "{0}" "%1" %*' -f $helperScriptFullName)
# Get the current $env:PATHEXT definition from the registry.
$currPathExt = [Environment]::GetEnvironmentVariable('PATHEXT', ('User', 'Machine')[$forAllUsers])
if (-not $forAllUsers -and -not $currPathExt) {
Write-Verbose "Creating a static user-level copy of the machine-level PATHEXT environment variable..."
$currPathExt = [Environment]::GetEnvironmentVariable('PATHEXT', 'Machine')
}
# Add "." as an executable extension to $env:PATHEXT so as to support
# direct execution of extension-less files.
if ($currPathExt -split ';' -notcontains '.') {
Write-Verbose "Appending '.' to PATHEXT..."
[Environment]::SetEnvironmentVariable('PATHEXT', $currPathExt + ';.', ('User', 'Machine')[$forAllUsers])
# Also define it for the running session
$env:PATHEXT += ';.'
} else {
Write-Verbose "'.' is already contained in PATHEXT."
}
# The following here-string is the source code for the
# $helperScriptFullName script to create.
# To debug and/or modify it:
# * Edit and/or debug $helperScriptFullName
# * After applying fixes / enhancements, replace the here-string
# below with the updated source code.
@'
# When invoked by direct execution of a script file via the file-type definition, the arguments are:
# * The full path of the script file being invoked.
# * Arguments passed to the script file on invocation, if any.
# CAVEAT: PowerShell's own parsing of command-line arguments into $args
# breaks unquoted tokens such as >> -true:blue << and >> -true.blue << into *2* arguments
# ('-true:', 'blue' and '-true', '.blue', respectively).
# The only way to avoid that is to pass the argument *quoted*: '-true:blue' and '-true.blue'
# See https://github.com/PowerShell/PowerShell/issues/6360
# Parse the arguments into the script
param(
[Parameter(Mandatory=$true)] [string] $LiteralPath,
[Parameter(ValueFromRemainingArguments=$true)] [array] $passThruArgs
)
$ErrorActionPreference = 'Stop'; Set-StrictMode -Version 1
# Note: When invoked via the file-type definition, $LiteralPath is guaranteed to be a full path.
# To also support interactive use of this script (for debugging), we resolve the script
# argument to a full path.
# Note that if you pass just a script filename (<script>), it'll be interpreted relative
# to the current directory rather than based on an $env:PATH search; to do the latter,
# pass (Get-Command <script>).Source
if ($LiteralPath -notmatch '^(?:[a-z]:)?[\\/]') { $LiteralPath = Convert-Path -LiteralPath $LiteralPath }
# Check the script's first line for a shebang.
$shebangLine = ''
switch -Regex -File $LiteralPath {
'^#!\s*(.*)\s*$' { # Matches a shebang line.
# Save the shebang line and its embedded command.
$shebangLine = $_
$cmdLine = $Matches[1]
Write-Verbose "Shebang line found in '$LiteralPath': $shebangLine"
break # We're done now that we have the shebang line.
}
default { # no shebang line found -> open with default text editor
# Note: We cannot use Invoke-Item or Start-Process, as that would
# reinvoke this handler, resulting in an infinite loop.
# The best we can do is to open the file in the default text editor.
Write-Verbose "No shebang line, opening with default text editor: $LiteralPath"
# Obtain the command line for the default text editor directly from the registry
# at HKEY_CLASSES_ROOT\txtfile\shell\Open\command rather than via `cmd /c ftype`,
# because assoc and ftype only ever report on and update the *machine-level* definitions at
# HKEY_LOCAL_MACHINE\Software\Classes
$cmdLine = [environment]::ExpandEnvironmentVariables((((Get-ItemProperty -EA Ignore registry::HKEY_CLASSES_ROOT\txtfile\shell\Open\command).'(default)') -split '=')[-1])
if (-not $cmdLine) { $cmdLine = 'NOTEPAD.EXE %1' } # Fall back to Notepad.
break # We're done now that we know this file doesn't have a shebang line.
}
}
# Parse the shebang line's embedded command line or the default-text-editor's command line into arguments.
# Note: We use Invoke-Expression and Write-Output so as to support *quoted*
# arguments as well - though presumably rare in practice.
# If supporting quoted tokens isn't necessary, the next line can be replaced
# with a strictly-by-whitespace splitting command:
# $cmdArgs = -split $cmdLine
[array] $cmdArgs = (Invoke-Expression "Write-Output -- $($cmdLine -replace '\$', "`0")") -replace "`0", '$'
if ($shebangLine) {
# Extract the target executable name or path.
# If the first argument is '/usr/bin/env', we skip it, as env (on Unix-like platforms) is merely used
# to locate the true target executable in the Path.
$exeTokenIndex = 0 + ($cmdArgs[0] -eq '/usr/bin/env')
$exeNameOrPath = $cmdArgs[$exeTokenIndex]
$exeFullPath = ''
# Note: We do NOT pass any remaining arguments from the shebang line through.
# (Such arguments are rare anyway.)
# The rationale is that an interpreter that understands shebang lines will
# also respect such arguments when reading the file - this is true of at
# least py.exe, the Python launcher, and ruby.exe
# Python is a special case: the Python launcher, py.exe, is itself
# capable of interpreting shebang lines, so we defer to it.
if ($exeNameOrPath -match '\bpython\d?') {
# Ensure that 'py.exe' is available; if not, we fall back to the same approach
# as for all other executables.
$exeFullPath = (Get-Command -CommandType Application py.exe -ErrorAction Ignore).Source
}
if (-not $exeFullPath) {
# Try the executable spec. as-is first, should it actually contain a *Windows* path name.
$exeFullPath = (Get-Command -CommandType Application $exeNameOrPath -ErrorAction Ignore).Source
if (-not $exeFullPath) {
# If not found, assume it is a Unix path that doesn't apply, and try to locate the hopefully
# appropriate executable by its filename only, in the Path.
$exeFullPath = (Get-Command -CommandType Application (Split-Path -Leaf -LiteralPath $exeNameOrPath) -ErrorAction Ignore).Source
}
}
# Abort, if we can't find a suitable executable.
if (-not $exeFullPath) { Throw "Could not find a suitable executable to run '$LiteralPath' based on its shebang line: $shebangLine" }
# Synthesize the complete list of arguments to pass to the target exectuable.
$passThruArgs = , $LiteralPath + $passThruArgs
} else { # NON-shebang-line files: invocation of default text editor
$exeFullPath, [array] $editorArgs = $cmdArgs -replace '%1', ($LiteralPath -replace '\$', '$$')
# Synthesize the complete list of arguments to pass to the target exectuable.
# Replace the '%1' placeholder with the script's path.
# Note that we don't really expect additional arguments to have been passed in this scenario,
# and such arguments may be interpreted as additional file arguments by the editor.
$passThruArgs = ($editorArgs -replace '"?%1"?', ($LiteralPath -replace '\$', '$$$$')) + $passThruArgs
# If the editor is a GUI application, $LASTEXITCODE won't be set by PowerShell.
# We set it to 0 here, as it has no value by default, and referencing it below with exit
# would cause an error due to Set-StrictMode -Version 1.
$LASTEXITCODE = 0
}
Write-Verbose "Executing: $exeFullPath $passThruArgs"
# Invoke the target executable with all arguments.
# Important:
# * We need to manually \-escape embeded " chars. in arguments
# because PowerShell, regrettably, doesn't do that automatically.
# However, even that may fail in edge cases in Windows PowerShell (fixed in PS Core),
# namely when an unbalanced " char. is part of the first word - see https://stackoverflow.com/a/55604316/45375
& $exeFullPath ($passThruArgs -replace '"', '\"')
# Pass the target executable's exit code through.
# (In the case of invoking the default editor for non-shebang-line files, it
# won't have been set, if the editor is a GUI application.)
exit $LASTEXITCODE
'@ |
Set-Content -Encoding Utf8 -LiteralPath $helperScriptFullName
}
# === UNINSTALL
function uninstall {
Write-Verbose ('Uninstalling shebang-script support for {0}:' -f ('the current user', 'ALL users')[$forAllUsers])
Write-Verbose 'Removing file type information from the registry...'
foreach ($regKey in $regkeyExtensionToFileType, $regkeyFileType) {
if (Test-Path -LiteralPath $regKey) {
Remove-Item -Force -Recurse -LiteralPath $regkey
}
}
# Get the current $env:PATHEXT definition from the registry.
$currPathExt = [Environment]::GetEnvironmentVariable('PATHEXT', ('User', 'Machine')[$forAllUsers])
# Remove the "." entry from $env:PATHEXT
$newPathExt = ($currPathExt -split ';' -ne '.') -join ';'
if ($newPathExt -eq $currPathExt) {
Write-Verbose "'.' is not contained in PATHEXT; nothing to do."
} else {
# For user-level uninstallations: as a courtesy, we compare the new PATHEXT value
# to the machine-level one, and, if they're now the same, simply REMOVE the user-level definition.
Write-Verbose "Removing '.' from PATHEXT..."
if (-not $forAllUsers) {
$machineLevelPathExt = [Environment]::GetEnvironmentVariable('PATHEXT', 'Machine')
if ($newPathExt -eq $machineLevelPathExt) { $newPathExt = $null }
Write-Verbose "User-level PATHEXT no longer needed, removing..."
}
[Environment]::SetEnvironmentVariable('PATHEXT', $newPathExt, ('User', 'Machine')[$forAllUsers])
# Also update for the running session
$env:PATHEXT = if ($newPathExt) { $newPathExt } else { $machineLevelPathExt }
}
Write-Verbose "Removing helper PowerShell script..."
if (Test-Path -LiteralPath $helperScriptFullName) {
Remove-Item -Force -LiteralPath $helperScriptFullName
}
}
# ---------------------- END: Internal helper functions
$forAllUsers = $Scope -eq 'AllUsers'
$verb = ('install', 'uninstall')[$Uninstall.IsPresent]
$operation = $verb + 'ation'
# If -Scope AllUsers was passed, ensure that the session is elevated.
$mustElevate = $forAllUsers -and -not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole('BUILTIN\Administrators')
if ($mustElevate) {
Throw "In order to $verb for ALL users, you must run this script WITH ELEVATED PRIVILEGES (Run As Administrator)."
}
# --- Define names, registry and file locations.
# The path of the generic shebang runner script that we'll create below.
$helperScriptFullName = Join-Path ($HOME, $env:ALLUSERSPROFILE)[$forAllUsers] 'Invoke-ShebangScript.ps1'
# The name of the file type to create for extension-less files.
$fileTypeName = 'ShebangScript'
# Registry keys that need to be modified.
# "." represents extension-less files
$regkeyExtensionToFileType = 'registry::{0}\SOFTWARE\Classes\.' -f ('HKEY_CURRENT_USER', 'HKEY_LOCAL_MACHINE')[$forAllUsers]
$regkeyFileType = 'registry::{0}\SOFTWARE\Classes\{1}' -f ('HKEY_CURRENT_USER', 'HKEY_LOCAL_MACHINE')[$forAllUsers], $fileTypeName
# ---
# Prompt for confirmation, unless -Force was passsed.
if ($Uninstall) { # UNINSTALL
if (-not $Force -and -not $PSCmdlet.ShouldContinue(@"
You are about to UNINSTALL support for direct execution of extension-less
script files that have shebang lines.
Uninstallation will be performed for $(("the CURRENT USER only`n(invoke as admin with -Scope AllUsers to change that)", 'ALL USERS')[$forAllUsers]).
IMPORTANT: Uninstallation will only be effective if it is performed in the same
(implied) -Scope as the original installation.
The following modifications to your system will be performed:
* "." will be persistently REMOVED from your `$env:PATHEXT variable.
* The following registry keys will be REMOVED:
$($regkeyExtensionToFileType -replace '^registry::')
$($regkeyFileType -replace '^registry::')
* The following helper PowerShell script will be REMOVED:
$helperScriptFullName
Press ENTER to proceed, or Ctrl-C to abort.
"@, "Shebang-Script Direct-Execution Support - Uninstallation")) { # , $true, [ref] $null, [ref] $null)) {
exit 1
}
# Call the uninstallation helper function
uninstall
} else { # INSTALL
if (-not $Force -and -not $PSCmdlet.ShouldContinue(@"
You are about to install support for direct execution of Unix-style scripts
that do not have a filename extension and instead define the interpreter to run
them with via shebangline ("#!/path/to/interpreter").
Support will be installed for $(("the CURRENT USER only`n(invoke as admin with -Scope AllUsers to change that)", 'ALL USERS')[$forAllUsers]).
Once installed, you will be able to run such scripts by direct invocation,
via a helper PowerShell script that analyzes the shebang line and calls the
appropriate interpreter.
CAVEATS:
* ENABLING THIS INVOCATION MECHANISM IS A SECURITY RISK, because any
plain-text file without an extension that has a shebang line
effectively becomes executable, potentially bypassing security features
that focus on files that have extensions known to be executable.
* AS OF POWERSHELL CORE 6.2.0, direct execution of such extension-less files
from PowerShell INVARIABLY RUNS IN A NEW CONSOLE WINDOW, WHICH MAKES USE
FROM POWERSHELL VIRTUALLY USELESS.
However, this is a BUG that should be fixed; see:
https://github.com/PowerShell/PowerShell/issues/7769
The following modifications to your system will be performed:
* "." will be added persistently to your `$env:PATHEXT variable, to enable
direct execution of filenames without extension.
NOTE: If you install with -Scope CurrentUser (the default), a static
user-level copy of the machine-level PATHEXT environment variable is
created, unless already present.
* The following registry locations will be created or replaced to define a
new file type for extension-less filenames:
$($regkeyExtensionToFileType -replace '^registry::')
$($regkeyFileType -replace '^registry::')
* The helper PowerShell script will be created in:
$helperScriptFullName
NOTE: Any existing registry definitions or helper script will be REPLACED.
Press ENTER to proceed, or CTRL-C to abort.
"@, "Shebang-Script Direct-Execution Support - Installation")) {
# !! The prompt defaults to *Yes* (!)
# !! Sadly, if we wanted the prompt to be default to *No*, we'de be forced
# !! to also present pointless 'Yes/No to *All*' prompts, which would be confusing.
# !! See https://github.com/PowerShell/PowerShell/issues/9428
exit 1
}
# Call the installation helper function
install
}
Write-Verbose "Shebang-support ${operation} completed."
if (-not $Force) {
Write-Host "Shebang-support ${operation} completed."
}
exit 0
[1] В Windows PowerShell вы можете использовать Get-ChildItem -File -Filter *.
для более удобного и эффективного поиска файлов без расширений, но эта функция не работает в PowerShell Core начиная с версии 6.2.0 - см. этот выпуск GitHub .