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

2jcobegt  于 2023-02-16  发布在  Shell
关注(0)|答案(4)|浏览(196)

我想把PowerShell脚本打包成一个批处理文件,最终使它在一个单独的分布式文件中对任何人都可执行。我的想法是用CMD命令开始一个文件,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部分,所以它不会被切断或其他什么。
复制该文件,删除前4行并将其重命名为 *.ps1,然后调用以下命令:

powershell -ExecutionPolicy unrestricted -File FILENAME.ps1

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

5q4ezhmt

5q4ezhmt1#

您可以使用注解块在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."
lokaqttq

lokaqttq2#

遗憾的是,将 * 脚本内容传送到powershell.exe/pwsh,即通过 * stdin-提供PowerShell代码-无论是通过-Command-c)(powershelle.exe默认值)还是-File-f)(pwsh默认值)-都有严重的限制:它表现出伪交互行为,需要 * 两个 * 尾随换行符来终止语句,并且缺乏对参数传递的支持;参见GitHub issue #3223

  • 它实际上是多行if语句与缺少额外换行符的组合(空行),这会导致脚本处理提前执行,因为该多行语句的结尾(包括所有后续语句)无法识别。增加的问题是for /f * 删除空行 *,所以在}结束后加1 * 不 * 起作用;虽然您可以使用 * non-empty,all-whitespace * 行(一个空格就可以了),它不会被删除,但是这种晦涩难懂的变通方法(每个多行语句之后都需要)不值得这么麻烦,所以最好避免使用基于stdin的方法。

Anon Coward's excellent trick for hiding the batch-file portion of the file from 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."
  • 使用-file调用脚本保留了通常的PowerShell脚本体验,因为脚本报告自己的文件名和路径,而且可能更重要的是,它可靠地支持使用%*传递的 * 参数
  • 不幸的是,PowerShell只接受扩展名为.ps1的文件作为-file参数,因此会创建具有该扩展名的批处理文件的 * 副本 *。
  • 在上面的代码中,该副本创建在与批处理文件相同的目录中,但可以进行调整(例如,您可以在%TEMP%中创建该副本和/或在调用PowerShell后自动删除该副本;参见this answer)。
  • exit /b %ERRORLEVEL%用于退出批处理文件,以确保PowerShell的退出代码通过。
    • 扩展Anon Coward的无额外文件、基于Invoke-Expression的解决方案以支持 (相当健壮)参数传递**:

这种方法避免了对扩展名为.ps1的批处理文件的(临时)副本的需要;一个小缺点是基于Invoke-Expression的调用会使代码报告 * 空字符串 * 作为脚本自己的文件路径(通过自动变量$PSCommandPath)和目录(通过$PSScriptRoot)。2然而,您可以使用$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."
  • 辅助环境变量%__args%/$env:__args用于将所有参数(%*)传递到PowerShell,在PowerShell中,其值合并到传递给Invoke-Expression的字符串中;注意文件内容如何被封装在. { ... }中,即经由script block的调用,使得代码支持接收自变量。
      • 注意事项**:这意味着最初传递给批处理文件的参数随后将使用PowerShell语法 * 进行解释:
  • 这将 * 中断 * 具有 * 嵌入 * "字符的参数,例如"3\" of snow";对于包含不平衡的'字符数的无引号参数,同上。(例如,6'
  • 即使调用没有完全 * break 参数值也可以改变 *,例如在批处理文件将按字面意思处理的带$前缀的子字符串的情况下(例如"amount: $3");此外,'字符在PowerShell中具有语法功能(例如,'aha'将变成逐字的aha,其中引号被移除),并且```字符被解释为 * escape * 字符。
  • 注意批处理文件的完整文件路径%~f0是如何包含在\"...\"中的(PowerShell最终将其视为"...");使用"而不是'加引号稍微更健壮,因为允许文件路径包含'字符本身,但不允许包含"
  • 如果您仍然需要PowerShell v2支持,而Get-Content-Raw交换机不受支持,请更换

Get-Content -Raw -LiteralPath ""%~f0""
Get-Content LiteralPath ""%~f0"" | Out-String

iugsix8n

iugsix8n3#

这种方法与Anon Coward的方法非常相似,但是不依赖于-Raw选项,该选项是在powershell-3.0中引入的:

<# :
@%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."
thtygnil

thtygnil4#

谢谢你的回答!仅供参考,这是我从提供的来源整理出来的:

<# : 
@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."

批处理部分已经有一个很长的行,所以我想我只是把所有的样板代码塞在一行中。
在这次编辑中,基于mklement 0编辑后的答案,命令行参数处理相当完整。

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

返回代码(errorlevel)仅在批处理文件使用call执行时可见,如下所示:

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

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

相关问题