【问题标题】:How to run a python program from powershell based on python hashbang?如何从基于python hashbang的powershell运行python程序?
【发布时间】:2019-04-09 21:16:32
【问题描述】:

我有几个 python3 脚本使用 hashbang 将其识别为 Python3 脚本。这些脚本可以根据文件扩展名被识别为 python,因为它们没有任何(就像在任何其他 *nix 系统中一样)。

我已经查看了相关问题,但它们并没有解决这个特殊需求,并且似乎依赖于 *.py 扩展来获得认可。

所以我的脚本命名为:myscript,文件的第一行是:

#!/usr/bin/env python3
...

如何让 Windows powershell 识别并使用位于 C:\Python3.7 的 python 解释器运行它?


UPDATE-1

为了澄清,我想从 powershell CLI 运行它,而不是通过单击它。 此外,我刚刚发现(令我震惊)当您将pip install 与本机 Windows Python3 一起使用时,第一行 hashbang 会自动替换为:

#!c:\python37\python.exe

哎哟!


UPDATE-2

感谢@eryksun 的 cmets,我设法获得了一个 PowerShell 脚本来为我做一些基本的检查。但是,它需要修复以支持 Python 以外的更多功能。

test4script.ps1


Param([parameter(Mandatory=$true, HelpMessage="Need a valid filename")] $fileName)
$firstLine = Get-Content -Path $fileName -TotalCount 1
$SHEBANG="^#!"
$shes=@("python3","python2","python","bash","sh","perl","pwsh")

If ($firstLine -match $SHEBANG) {
    Write-Host "DEBUG: checking for common shebangs" -ForegroundColor Green
    foreach ($i in $shes) {
        If ($firstLine -match $i) {
            Write-Host "DEBUG: found shebang for: $i" -ForegroundColor Green
            C:\python37\python.exe $fileName
            break
        }
    }
} else {
    Write-Host "File is not a known script. No shebang fund in file!" -ForegroundColor Red
    return
}
Write-Host "DEBUG: Done" -ForegroundColor Green

结果是:

$ Get-Content -Path nonscript -TotalCount 3
#This aint right
echo "hello"

$ Get-Content -Path pip-describe -TotalCount 3
#!c:\python37\python.exe
# pip-describe - Show full text package description from PyPI
# -*- coding: utf-8 -*-

$ .\test4script.ps1 nonscript
File is not a known script. No shebang fund in file!

$ .\test4script.ps1 pip-describe
DEBUG: checking for common shebangs
DEBUG: found shebang for: python3

 Usage:  pip-describe <package-name>

 This will return the full-text package description (usually the README)
 as found on PyPI, for any given <package-name>.
 ...

现在我们应该能够将此脚本与 . 相关联,使用 CMD.exe 与:

cmd /c assoc .=unknown
cmd /c "ftype unknown=""C:\mybin\test4script.ps1"" %1"

但是使用 PowerShell 独立执行此操作会更好,而不必跳过 CMD。

【问题讨论】:

  • 到目前为止你尝试过什么?我的想法是你会编写一个脚本来内省每个文件,查找那个 hashbang,然后在 Python 中运行该文件(如果匹配)。有没有哪一部分看起来是不可能的?
  • 不多,因为我不知道从哪里开始。我可以通过在 Bash 中运行 powershell 脚本(使用 pwsh hasbang)来做相反的事情,但不是这个。在powershell中检查这个的正确方法是什么?棘手的技巧是让您的 pwsh 解释器在您输入:./myscript 时理解并运行它。
  • 哦,如果这是您的意图,那么在您的帖子中提供的信息是很好的 - 我没有意识到您只是希望能够运行 ./myscript 并让它工作。
  • #!c:\python37\python.exe 是 py.exe 启动器的有效 shebang,默认情况下安装程序与 .py 文件关联。启动器将执行任何完全限定的路径,并且它还支持#!/usr/bin/python3#!python3 等虚拟shebang。您可以编写一个程序来实现“。”的关联。文件扩展名(即没有扩展名的文件名)。如果它是 Python shebang,请通过 py.exe 运行它。让 CMD 运行没有扩展名的文件需要添加“。”到PATHEXT 环境变量。 PowerShell 可能也是一样。
  • “.”文件关联将与定义“打开”模板命令行的 ProgId 关联,该命令行运行处理 shebang 子集的可执行文件或脚本(可能可通过插件架构扩展)。如果它启动一个进程来运行文件,它必须等待子进程并代理退出状态。

标签: python python-3.x windows powershell shebang


【解决方案1】:

eryksun 在 cmets 中提供了有关该问题的重要指针,您基于它们的编辑显示了安装 通用的、机器范围的 shebang-line-aware 启动器的方法,该启动器用于通过添加可执行的无扩展脚本.$env:PATHEXT.

关于这种方法的注意事项:

  • PowerShell 当前(从 PowerShell Core 6.2.0 开始)总是在控制台窗口中执行无扩展名文件,这使得此配置在 PowerShell 中无用 - 不过,它确实按 cmd.exe 的预期工作。

    • PowerShell 的行为应被视为一个错误,并已在this GitHub issue 中报告。
  • 该机制存在潜在的安全风险,因为任何没有扩展名且具有 shebang 行的纯文本文件都可以有效地变为可执行文件,从而可能绕过专注于具有扩展名的文件的安全功能已知可执行。

  • 通过 [PowerShell] 脚本实现文件类型定义的默认操作总是需要使用脚本文件的解释器创建一个 子进程,在本例中,这意味着调用 powershell.exe带有-File 参数。 powershell.exe 的启动性能成本非常高,这会延迟执行

  • 如果您确实想实现这种通用机制,请查看底部的
    Install-ShebangSupport.ps1 脚本。


鉴于上述情况,这里有一个更轻量级、特定于 Python 的方法,它基于为无扩展的 shebang-line 自动创建单独的 *.ps1 包装脚本 Python 脚本

这利用了 PowerShell 仅允许通过文件名执行其自己的 *.ps1 脚本文件这一事实。

限制

  • 您需要至少运行一次 wrapper-script-generation 脚本(打印在下面),并且每次添加新的无扩展 Python 脚本时。

  • 可以想象,可以使用文件系统观察程序来触发生成脚本,但设置它并非易事。

从好的方面来说,包装脚本比基于文件类型的通用解决方案执行得更快,因为不涉及额外的 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.exeexit $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 should generally be avoided


脚本 Install-ShebangSupport.ps1 用于安装通用支持,以便在 Windows 上直接执行无扩展的基于 shebang-line 的脚本:

  • 该脚本支持在 current-user 级别(默认情况下或使用 -Scope CurrentUser)或在 all-users 级别(使用 @987654351 @,需要以管理员身份运行)。

  • 假设当前目录中存在,运行 Get-Help .\Install-ShebangSupport 以获得基本帮助。
    调用不带参数的脚本会打印一个确认提示,其中包含有关系统所需修改的详细信息; Ctrl-C 可用于中止不安装;传递-Force 执行安装而不提示确认。

  • 稍后卸载,传递-Uninstall;请注意,您必须匹配安装期间使用的(隐含的)-Scope 值。

实施说明:通过cmd.exe-internal 命令assocftype 定义无扩展文件类型总是对所有用户生效,因为定义存储在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 em> 从 v6.2.0 开始 - 请参阅 this GitHub issue

【讨论】:

  • 虽然你的方法看起来很不错,因为它是纯 PS 原生的,但我认为我使用 assocftype 的结果的方法更容易解析。至少对于使用 switch 语句时已经安装的解释器。也就是说,我希望找到它们的原生 PS 或 exe 版本,而不必通过 cmd 来获取该信息。这应该是相当直截了当的,因为流行的口译员的数量是有限的。无论哪种方式,真正让我感到惊讶的是(python?)安装程序如何在安装时选择屠宰 shebang 线,这似乎是疯狂且不可移植的行为。
  • @not2qubit:同意,对 shebang 线的悄悄修改听起来很成问题。您无需调用cmd.exe 即可直接访问注册表。无论哪种方式,您都不会获得所需的行为,因为每当您调用无扩展名的 Python 文件时,PowerShell 都会启动一个新的控制台窗口,至少在当前版本中,如答案中所述。
  • @not2qubit:另外,您只需要assocftype 一次,即可安装无扩展处理程序;稍后在通用处理程序中分析 shebang 行时,它们不会帮助您找到合适的解释器。还是我错过了什么?
  • 是的,我想在同一个脚本中将它们用于 2 个不同的目的。 (1) 为. 关联安装testing 脚本(本身),如在OP 中。 (2) 解析 shebang 并将其与 ftype 中已有的内容进行比较。如果存在,请使用 that,如果不存在,请使用我们自己的 exe/path。这将基本上取代原生行为。我还没有使用新的 6.2 控制台,所以这听起来有点令人担忧。为这些脚本启动一个新控制台有什么意义?这对我来说没有任何意义。
  • @not2qubit,是的,对不起 - 我以为你在谈论底部的通用 Install-ShebangSupport.ps1 解决方案(由于链接的错误,目前在 PowerShell 中不起作用,只有在cmd.exe)。因此,您必须使用单独的包装脚本,如在顶部的解决方案中,或者创建类似的 函数(两种解决方案都具有更快的优势,因为不涉及 PowerShell 子进程)。无论哪种情况,每次添加新脚本时都必须运行创建它们的代码,而最底层的解决方案是使用单个脚本进行一次性安装。
猜你喜欢
  • 2010-11-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-08-11
  • 2010-12-04
  • 1970-01-01
  • 2019-12-02
  • 1970-01-01
相关资源
最近更新 更多