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

PowerShell script wrapped in batch file is executed only halfway

我想将一个 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."

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

如果您将第 2 行的结果通过管道传输到文件或 cat 进程(如果您安装了 unix 工具,Windows 甚至无法自行完成),您可以看到文件的完整 PowerShell 部分,因此它没有被截断之类的。 PowerShell 只是不想在这里执行脚本。

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

powershell -ExecutionPolicy unrestricted -File FILENAME.ps1

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

您可以使用注释块从 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."

不幸的是,管道脚本内容到powershell.exe/pwsh,即通过stdin[提供PowerShell代码- - 无论是通过 -Command (-c)(powershelle.exe 默认值)还是 -File-f)(pwsh默认值)- 有严重限制:它显示伪交互行为,在某些情况下需要两个尾随换行符终止一个声明,并且缺乏对参数传递的支持;参见 GitHub issue #3223

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

的基础上,让我提供以下改进:

使用 copy 附加扩展名 .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."
  • 使用 -file 调用脚本保留了通常的 PowerShell 脚本体验,关于脚本报告自己的文件名和路径,也许更重要的是,强大地支持 arguments 通过 %*

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


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

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

<# ::
@echo off
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."
  • 一个辅助环境变量,%__args% / $env:__args 用于将所有参数 (%*) 传递给 PowerShell,它的值被合并到传递给 Invoke-Expression 的字符串;注意文件内容是如何包含在 . { ... } 中的,即通过 script block 的调用,以便代码支持接收参数。

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

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

此方法与 Anon Coward 的方法非常相似,但不依赖 -Raw 选项,该选项在 :

中引入
<# :
@%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile^
 -NoLogo -Command "$input | &{ [ScriptBlock]::Create("^
 "(Get-Content -LiteralPath \"%~f0\") -Join [Char]10).Invoke() }"
@Pause
@GoTo :EOF
#>
Write-Host "Hello PowerShell!"
If ($True)
{
    Write-Host "TRUE"
}
Write-Host "Goodbye."

为了更好地阅读,我将长的 PowerShell 命令行拆分为多个,但这不是必需的:

<# :
@%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -NoLogo -Command "$input | &{ [ScriptBlock]::Create((Get-Content -LiteralPath \"%~f0\") -Join [Char]10).Invoke() }"
@Pause
@GoTo :EOF
#>
Write-Host "Hello PowerShell!"
If ($True)
{
    Write-Host "TRUE"
}
Write-Host "Goodbye."

感谢您的热心解答!仅供参考,这是我从提供的资源中整理出来的:

<# : 
@echo off & setlocal & set __args=%* & %SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -Command Invoke-Expression ('. { ' + (Get-Content -LiteralPath ""%~f0"" -Raw) + ' }' + $env:__args) & exit /b %ERRORLEVEL%
#>
param([string]$name = "PowerShell")

write-host "Hello $name!"
write-host "You said:" $args
if ($args)
{
    write-host "This is true."
}
else
{
    write-host "I don't like that."
    exit 1
}
write-host "Good bye."

批处理部分已经有很长的一行,所以我想我应该把所有的样板代码都放在一行中。

在此编辑中,基于 mklement0 的编辑答案,命令行参数处理非常完整。示例:

ps-script.cmd 'Donald Duck' arg1 arg2
ps-script.cmd arg1 arg2 -name "Donald Duck"

return 代码(错误级别)仅在使用 call 执行批处理文件时可见,如下所示:

call ps-script.cmd some args && echo OK
call ps-script.cmd && echo OK

如果没有 call,return 值始终为 0,并且始终显示“OK”。这是一个经常被遗忘的 CMD 和批处理文件的限制。