打包在批处理文件中的PowerShell脚本只执行了一半

ygo*_*goe 5 powershell cmd batch-file

我想将 PowerShell 脚本包装到一个批处理文件中,最终使其对任何人都可以执行,并且可以在单个分布式文件中执行。我的想法是使用读取文件本身的 CMD 命令开始一个文件,跳过前几行并将其余行通过管道传输到 powershell,然后让批处理文件结束。在 Bash 中,使用简短的可读命令将是一项简单的任务,但是您可以从众多技巧中看出 Windows 已经在这方面遇到了大麻烦。这就是它的样子:

@echo off
(for /f "skip=4 delims=" %%a in ('type %0') do @echo.%%a) |powershell -noprofile -noninteractive -
goto :EOF
---------- BEGIN POWERSHELL ----------
write-host "Hello PowerShell!"
if ($true)
{
    write-host "TRUE"
}
write-host "Good bye."
Run Code Online (Sandbox Code Playgroud)

问题是,PowerShell 脚本没有完全执行,它在第一行写完后停止。我可以让它工作更多,这取决于脚本本身,但在这里找不到任何模式。

如果您将第 2 行的结果通过管道传输到文件或cat进程(如果您安装了 unix 工具,Windows 甚至无法单独执行此操作),您可以看到文件的完整 PowerShell 部分,因此不会被剪切关什么的。PowerShell 只是不想在这里执行脚本。

复制文件,删除前 4 行并将其重命名为 *.ps1,然后调用:

powershell -ExecutionPolicy unrestricted -File FILENAME.ps1
Run Code Online (Sandbox Code Playgroud)

它会完全执行。所以它也不是 PowerShell 脚本内容。在这种情况下,是什么让 powershell 过早结束?

Ano*_*ard 8

您可以使用注释块从 powershell 中隐藏脚本的批处理文件部分,然后按原样运行脚本,而无需批处理文件对其进行修改:

<# : 
@echo off
powershell -command "Invoke-Expression (Get-Content '%0' -Raw)"
goto :EOF
#>

write-host "Hello PowerShell!"
if ($true)
{
    write-host "TRUE"
}
write-host "Good bye."
Run Code Online (Sandbox Code Playgroud)

  • 不错的把戏。但是,我建议使用 `%~f0` 而不是 `%0`,以确保批处理文件以其完整路径为目标,以便在工作目录时调用也有效。碰巧不是批处理文件所在的那个。一个潜在的挑战是传递参数,尤其是当它们用双引号括起来时。 (2认同)
  • @ygoe,如果您使用 _path_(例如,`.\foo.cmd` 或 `C:\path\to\foo.cmd`)调用批处理文件,则不需要,但如果批处理文件被放置在系统的 `%PATH%` 中并且仅通过名称调用_(例如,`foo`),至少从 `cmd.exe` 调用(PowerShell 实际上在幕后解析了完整路径,因此 `%0` 看到该场景中的完整路径。) (2认同)

mkl*_*nt0 5

不幸的是,将脚本内容传输powershell.exe/ ,即通过stdinpwsh提供 PowerShell 代码-无论是通过-Command( -c) (powershelle.exe默认)还是-File( -f) (pwsh默认) - 都有严重的局限性:它显示伪交互行为,有时需要两个尾随换行符来终止一个声明,并且缺乏对参数传递的支持;请参阅GitHub 问题 #3223

  • 确实,多行if语句与后面缺少额外的换行符(空行)的组合导致脚本的处理过早执行,因为该多行语句的结尾(包括所有后续语句)不被识别。增加的问题是for /f 删除空行,因此在结束后添加空}不起作用;虽然您可以使用非空的全空白行(单个空格即可),但不会被删除,但这种晦涩的解决方法(在每个多行语句之后都需要)不值得麻烦,因此标准输入最好避免基于基于的方法。

基于Anon Coward 的从 PowerShell 隐藏文件的批处理文件部分的出色技巧,让我提供以下改进:

使用附加扩展名的批处理文件的副本.ps1,传递给 PowerShell CLI-file( -f) 参数:

<# ::
@echo off
copy /y "%~f0" "%~dpn0.ps1" > NUL
powershell -executionpolicy bypass -noprofile -file "%~dpn0.ps1" %*
exit /b %ERRORLEVEL%
#>
# ---------- BEGIN POWERSHELL ----------
"Hello PowerShell!"
"Args received:`n$($args.ForEach({ "[$_]" }))"
if ($true)
{
  "TRUE"
}
"Good bye."
Run Code Online (Sandbox Code Playgroud)
  • 调用脚本保留了-file通常的 PowerShell 脚本体验,关于报告其自己的文件名和路径的脚本,也许更重要的是,稳健地支持通过 传递的参数%*

    • 不幸的是,PowerShell 只接受具有扩展名的文件.ps1作为-file参数,因此会创建具有该扩展名的批处理文件的副本。
    • 在上面的代码中,该副本是在与批处理文件相同的目录中创建的,但可以进行调整(例如,您可以%TEMP%在调用 PowerShell 后创建它和/或自动删除副本;请参阅此答案)。
  • exit /b %ERRORLEVEL%用于退出批处理文件,以确保PowerShell的退出代码通过。


扩展 Anon Coward 的无额外文件、Invoke-Expression基于 的解决方案以支持(相当稳健的)参数传递

这种方法避免了对扩展名为 的批处理文件的(临时)副本的需要.ps1;一个小缺点是Invoke-Expression基于 - 的调用将使代码将空字符串报告为脚本自己的文件路径(通过自动$PSCommandPath变量)和目录(通过$PSScriptRoot)。但是,您可以用来$batchFilePath = ([Environment]::CommandLine -split '""')[1]获取批处理文件的完整路径。

<# ::
@echo off & setlocal
set __args=%*
powershell -executionpolicy bypass -noprofile -c Invoke-Expression ('. { ' + (Get-Content -Raw -LiteralPath \"%~f0\") + ' }' + $env:__args)
exit /b %ERRORLEVEL%
#>
# ---------- BEGIN POWERSHELL ----------
"Hello PowerShell!"
"Args received:`n$($args.ForEach({ "[$_]" }))"
if ($true)
{
  "TRUE"
}
"Good bye."
Run Code Online (Sandbox Code Playgroud)
  • %__args%/是一个辅助环境变量,$env:__args用于将所有参数 ( %*) 传递到 PowerShell,其中它的值被合并到传递给 的字符串中Invoke-Expression;请注意文件内容如何包含在 中,即通过脚本块. { ... }进行调用,以便代码支持接收参数。

    • 警告:这意味着最初传递给批处理文件的参数随后将使用 PowerShell 语法进行解释:
      • 会破坏具有嵌入 "字符的参数,例如"3\" of snow"; 对于包含不平衡'字符数的未加引号的参数,同上。(例如,6'

      • 即使调用没有完全中断参数值也可以更改,例如在$批处理文件按字面处理的前缀子字符串的情况下(例如,"amount: $3");还有,'字符。在 PowerShell 中具有语法功能(例如,'aha'将变成 verbatim aha,删除引号)和`字符。被解释为转义字符。

  • %~f0请注意批处理文件的完整文件路径 是如何包含在其中的\"...\"(PowerShell 最终将其视为"...");使用"- 而不是 -'引用稍微更健壮,因为文件路径允许包含'字符。他们自己,但不是"

  • 如果您仍然需要 PowerShell v2 支持,其中Get-Contents-Raw开关不受支持,请替换
    Get-Content -Raw -LiteralPath ""%~f0""
    Get-Content LiteralPath ""%~f0"" | Out-String