如何在Windows批处理文件中运行PowerShell脚本

25
如何将PowerShell脚本嵌入到与Windows批处理脚本相同的文件中?
我知道在其他情况下这种事情是可能的:
- 使用`sqlcmd`和文件开头的精心安排的goto和注释将SQL嵌入批处理脚本中 - 在*nix环境中,将希望运行脚本的程序名称作为脚本第一行的注释,例如`#!/usr/local/bin/python`。
也许没有办法做到这一点 - 如果是这样,我将不得不从启动脚本中调用单独的PowerShell脚本。
我考虑过的一个可能的解决方案是回显PowerShell脚本,然后运行它。不这样做的一个很好的理由是,尝试这样做的部分原因是使用PowerShell环境的优势,而不需要例如转义字符的麻烦。
我有一些不寻常的限制,并希望找到一个优雅的解决方案。我怀疑这个问题可能会引起各种回应:“为什么你不尝试解决不同的问题。”可以说这些是我的限制,对此感到抱歉。
有什么想法吗?是否有适当的巧妙注释和转义字符组合,可以使我实现这一点?
关于如何实现这一点的一些想法:
  • 行末的克拉符号^表示续行,类似于Visual Basic中的下划线
  • 和号&通常用于分隔命令,echo Hello & echo World会在两行上分别输出两个echo
  • %0将给出当前运行的脚本

所以如果我能让它正常工作,就像这样:

# & call powershell -psconsolefile %0
# & goto :EOF
/* From here on in we're running nice juicy powershell code */
Write-Output "Hello World"

除非...

  • 它不起作用...因为
  • 文件的扩展名不符合PowerShell的喜好:Windows PowerShell控制台文件"insideout.bat"的扩展名不是psc1。Windows PowerShell控制台文件扩展名必须是psc1。
  • CMD对此情况也不太满意 - 尽管它会在'#'上绊倒,但它不被识别为内部或外部命令、可操作程序或批处理文件。
18个回答

26

这个只会将正确的行传递给 PowerShell:

dosps2.cmd:

@findstr/v "^@f.*&" "%~f0"|powershell -&goto:eof
Write-Output "Hello World" 
Write-Output "Hello some@com & again" 

这个 正则表达式 排除以 @f 开头且包含一个 & 的行,并将其余所有内容传递给 PowerShell。

C:\tmp>dosps2
Hello World
Hello some@com & again

这是一个完美的答案 - 干净 - 没有错误,可以运行!先生,您获得了金星。我需要做的唯一修改是:为了处理文件名/路径中的空格,我需要在%~f0周围加上“引号”:@findstr/v "^@.*&" "%~f0"|powershell -&goto:eof再次感谢。 - Don Vince
我做了两个更改。1)使用@@作为前缀进行查找,以避免与here-string混淆。2)使用空格分隔命令行的元素,以提高可读性。 - Jay Bazuzi
2
这个答案根本不支持参数。我敢打赌你很快就会需要那个功能。 - Jay Bazuzi
我不喜欢这个不通过返回代码的方式。 - xjcl
也不支持暂停/Read-Host :/ - xjcl

15

听起来你正在寻找被称为“多语言脚本”的东西。对于CMD->PowerShell,

@@:: This prolog allows a PowerShell script to be embedded in a .CMD file.
@@:: Any non-PowerShell content must be preceeded by "@@"
@@setlocal
@@set POWERSHELL_BAT_ARGS=%*
@@if defined POWERSHELL_BAT_ARGS set POWERSHELL_BAT_ARGS=%POWERSHELL_BAT_ARGS:"=\"%
@@PowerShell -Command Invoke-Expression $('$args=@(^&{$args} %POWERSHELL_BAT_ARGS%);'+[String]::Join([char]10,$((Get-Content '%~f0') -notmatch '^^@@'))) & goto :EOF

如果您不需要支持带引号的参数,则甚至可以将其做成单行:

@PowerShell -Command Invoke-Expression $('$args=@(^&{$args} %*);'+[String]::Join([char]10,(Get-Content '%~f0') -notmatch '^^@PowerShell.*EOF$')) & goto :EOF

这段内容摘自http://blogs.msdn.com/jaybaz_ms/archive/2007/04/26/powershell-polyglot.aspx。这是PowerShell v1的版本,可能在v2中更简单,但我没有查看过。


为什么要创建POWERSHELL_BAT_ARGS变量,然后检查它是否已定义?:“\ =%”部分是什么意思? - Qwertie
@Qwertie:首先它被设置为 %* ,这是所有参数的列表。如果没有传递任何参数,则变量保持未设置状态,因此下一行不执行任何操作。%FOO:X=Y% 将替换 %FOO% 中所有的 X 为 Y。在这种情况下,我们使用反斜杠来转义引号字符。听起来合理吗? - Jay Bazuzi
谢谢,这真的很好!因此,DOS shell具有内置的查找和替换功能,但仅适用于环境变量。大概某些字符,如%和=不能参与替换……很幸运引号和反斜杠都没有被视为特殊字符。 - Qwertie
6
使用分号连接会导致多行命令的问题(使用反引号、多行管道、括号内多行内容等)。改用[char]10进行连接,可以解决这个问题。 - Rynant
通过谷歌搜索,我找到了这个帖子。我在批处理脚本中使用了这个解决方案(稍作修改),以生成混合的PowerShell-in-cmd(也支持其他语言:js、vbs、wsf、hta、pl)。请参考此帖子:http://www.dostips.com/forum/viewtopic.php?p=37780#p37780 - jsxt
我还会将读取文件更改为(Get-Content '%~sf0')(注意:添加了s:短文件名),以便文件路径不包含空格或其他会混淆PowerShell和cmd双重解析数据的不良内容。 - mojo

8

这里讨论了相关话题。主要目标是避免使用临时文件以减少缓慢的I/O操作,并在不产生冗余输出的情况下运行脚本。

以下是我认为最佳解决方案:

<# :
@echo off
setlocal
set "POWERSHELL_BAT_ARGS=%*"
if defined POWERSHELL_BAT_ARGS set "POWERSHELL_BAT_ARGS=%POWERSHELL_BAT_ARGS:"=\"%"
endlocal & powershell -NoLogo -NoProfile -Command "$input | &{ [ScriptBlock]::Create( ( Get-Content \"%~f0\" ) -join [char]10 ).Invoke( @( &{ $args } %POWERSHELL_BAT_ARGS% ) ) }"
goto :EOF
#>

param(
    [string]$str
);

$VAR = "Hello, world!";

function F1() {
    $str;
    $script:VAR;
}

F1;

甚至更好的方法(见这里):

<# : batch portion (begins PowerShell multi-line comment block)


@echo off & setlocal
set "POWERSHELL_BAT_ARGS=%*"

echo ---- FROM BATCH
powershell -noprofile -NoLogo "iex (${%~f0} | out-string)"
exit /b %errorlevel%

: end batch / begin PowerShell chimera #>

$VAR = "---- FROM POWERSHELL";
$VAR;
$POWERSHELL_BAT_ARGS=$env:POWERSHELL_BAT_ARGS
$POWERSHELL_BAT_ARGS

在这里,POWERSHELL_BAT_ARGS是首先在批处理部分作为变量设置的命令行参数。

关键是批量重定向优先级-此行<# :将被解析为:<#,因为重定向优先级高于其他命令。

但是,在批处理文件中以:开头的行被视为标签-即未执行的行。尽管如此,这仍然是有效的PowerShell注释。

唯一剩下的事情就是找到PowerShell正确的方法来读取和执行% ~ f0,它是由cmd.exe执行的脚本的完整路径。


这是一个不错的解决方案。使用Powershell多行格式,加上传入的命令行参数,而不依赖于findstr。感谢您提供这个方案。 - holtavolt
添加执行策略可能是明智的,以允许在默认的Windows安装上运行PowerShell位。 - Barleyman
如果您收到“此脚本包含恶意内容,已被您的防病毒软件阻止。”的提示,则该脚本由于“<# :”片段而被Defender阻止。 - Stefano
1
@Stefano - 这是新的东西。目前我正在使用Linux机器工作,稍后我将能够测试这个。 - npocmaka

5

还要考虑这个"polyglot"包装脚本,它支持嵌入式PowerShell和/或VBScript/JScript代码;它是从这个巧妙的原始版本改编而来的,作者自己flabdablet在2013年发布了它,但由于只是一个链接答案,因此在2015年被删除。

Kyle的优秀答案基础上改进的解决方案:

创建一个批处理文件(例如sample.cmd),其内容如下:

<# ::
@echo off & setlocal
copy /y "%~f0" "%TEMP%\%~n0.ps1" >NUL && powershell -NoProfile -ExecutionPolicy Bypass -File "%TEMP%\%~n0.ps1" %*
set ec=%ERRORLEVEL% & del "%TEMP%\%~n0.ps1"
exit /b %ec%
#>

# Paste arbitrary PowerShell code here.
# In this example, all arguments are echoed.
'Args:'
$Args | % { 'arg #{0}: [{1}]' -f ++$i, $_ }

注意:
当批处理文件运行时,会在%TEMP%文件夹中创建一个临时的*.ps1文件,并在之后进行清理;这样做极大地简化了通过(相对)稳健地使用%*传递参数,仅仅是因为。
上述代码调用Windows PowerShell。要调用跨平台的PowerShell(Core) v7+版本,请在上面的代码中用pwsh替换powershell。
技术解释:
  • <# ::是一种混合行,PowerShell将其视为注释块的开头,但cmd.exe会忽略它,这是从npocmaka's answer中借鉴的技术。

  • @开头的批处理命令被PowerShell忽略,但由cmd.exe执行;由于最后一个以@开头的行以exit /b结束,该命令在那里退出批处理文件,因此cmd.exe忽略了文件的剩余部分,因此可以自由地包含非批处理文件代码,即PowerShell代码。

  • #>行结束了包含批处理文件代码的PowerShell注释块。

  • 因为整个文件都是有效的PowerShell文件,所以不需要使用findstr技巧来提取PowerShell代码;但是,由于PowerShell仅执行具有文件名扩展名.ps1的脚本,因此必须创建批处理文件的(临时)副本%TEMP%\%~n0.ps1%TEMP%文件夹中创建以批处理文件(%~n0)命名的临时副本,但扩展名为.ps1;完成后会自动删除临时文件。

  • 请注意,需要3个单独的cmd.exe语句,以便通过PowerShell命令的退出代码。
    (使用setlocal enabledelayedexpansion假设允许将其作为单个行执行,但这可能导致参数中不需要的!字符的解释。)


为了展示参数传递的强大性:
假设上述代码已保存为sample.cmd,调用方式为:
sample.cmd "val. w/ spaces & special chars. (\|<>'), on %OS%" 666 "Lisa \"Left Eye\" Lopez"

产生类似以下的结果:
Args:
arg #1: [val. w/ spaces & special chars. (\|<>'), on Windows_NT]
arg #2: [666]
arg #3: [Lisa "Left Eye" Lopez]

请注意嵌入的"字符是如何被传递的,
然而,与嵌入的"字符相关的边缘情况存在。
:: # BREAKS, due to the `&` inside \"...\"
sample.cmd "A \"rock & roll\" life style"

:: # Doesn't break, but DOESN'T PRESERVE ARGUMENT BOUNDARIES.
sample.cmd "A \""rock & roll\"" life style"

这些困难是由于cmd.exe的有缺陷的参数解析引起的,最终试图隐藏这些缺陷是没有意义的,正如 flabdablet在他出色的答案中指出的那样
正如他所解释的那样,在"..."序列内使用^^^转义以下cmd.exe元字符可以解决问题:
& | < >

使用上面的示例:
:: # OK: cmd.exe metachars. inside \"...\" are ^^^-escaped.
sample.cmd "A \"rock ^^^& roll\" life style"

mklink也可以在这里使用,欺骗PowerShell以ps1的方式运行cmd,而不是使用临时文件:mklink“%dpn0.ps1”“%f0”>nul(要删除链接,只需删除创建的符号链接。即test.cmd将在0字节处生成test.ps1符号链接) - undefined
1
@PlayLORD-SysOp,创建符号链接默认情况下需要提升权限,至少在 Windows 11 上是如此,因此使用它们并不是一个普遍可行的选项(要求不需要提升权限需要事先打开“开发者模式”,通过“Start-Process ms-setting:developers”)。 - undefined

5

如果您不介意PowerShell开头出现一个错误,那么这似乎是有效的:

dosps.cmd

@powershell -<%~f0&goto:eof
Write-Output "Hello World" 
Write-Output "Hello World again" 

干得好 - 成功解决了运行PowerShell脚本的问题!不过,如果我们能消除PowerShell错误,我仍然需要更多答案。 - Don Vince

5

我非常喜欢Jean-François Larvoire所提供的解决方案,尤其是他处理参数并将其直接传递给powershell脚本的方式(+1添加)。

但它有一个缺陷。由于我没有评论的声誉,所以我将更正作为新的解决方案发布。

使用双引号在Invoke-Expression中作为参数的脚本名称,当脚本名称包含$字符时将不起作用,因为这将在加载文件内容之前被评估。最简单的解决方法是替换双引号:

PowerShell -c ^"Invoke-Expression ('^& {' + [io.file]::ReadAllText('%~f0') + '} %ARGS%')"

个人而言,我更喜欢使用带有 -raw 选项的 get-content,因为这对于我来说更符合 PowerShell 的风格:

PowerShell -c ^"Invoke-Expression ('^& {' + (get-content -raw '%~f0') + '} %ARGS%')"

当然,这只是我的个人意见。`ReadAllText` 工作得非常完美。
为了完整起见,更正后的脚本如下:
<# :# PowerShell comment protecting the Batch section
@echo off
:# Disabling argument expansion avoids issues with ! in arguments.
setlocal EnableExtensions DisableDelayedExpansion

:# Prepare the batch arguments, so that PowerShell parses them correctly
set ARGS=%*
if defined ARGS set ARGS=%ARGS:"=\"%
if defined ARGS set ARGS=%ARGS:'=''%

:# The ^ before the first " ensures that the Batch parser does not enter quoted mode
:# there, but that it enters and exits quoted mode for every subsequent pair of ".
:# This in turn protects the possible special chars & | < > within quoted arguments.
:# Then the \ before each pair of " ensures that PowerShell's C command line parser 
:# considers these pairs as part of the first and only argument following -c.
:# Cherry on the cake, it's possible to pass a " to PS by entering two "" in the bat args.
echo In Batch
PowerShell -c ^"Invoke-Expression ('^& {' + (get-content -raw '%~f0') + '} %ARGS%')"
echo Back in Batch. PowerShell exit code = %ERRORLEVEL%
exit /b

###############################################################################
End of the PS comment around the Batch section; Begin the PowerShell section #>

echo "In PowerShell"
$Args | % { "PowerShell Args[{0}] = '$_'" -f $i++ }
exit 0

嗨,如果脚本路径名包含一个美元符号,我的版本确实会失败,感谢报告……但是如果脚本路径名包含单引号,则您的版本也会失败!这两种情况都不太可能,但有可能 :-)为了兼顾两者的优点,我们需要从您的解决方案开始,但要像我对参数进行预处理一样精确地预处理路径名。 - Jean-François Larvoire

3

与Carlos发布的解决方案不同,此方法支持参数,并且不会破坏多行命令或像Jay发布的解决方案中使用param。唯一的缺点是此方法会创建一个临时文件。对于我的用例来说,这是可以接受的。

@@echo off
@@findstr/v "^@@.*" "%~f0" > "%~f0.ps1" & powershell -ExecutionPolicy ByPass "%~f0.ps1" %* & del "%~f0.ps1" & goto:eof

1
如果在 "%~f0.ps1" 前面加上 -File,你的解决方案将正确处理诸如 "a b""a & b" 等参数(Windows PowerShell 默认为 -Command,其解析参数方式不同)。稍微改进一下是在 %TEMP% 文件夹中创建临时文件:"%TEMP%\%~0n.ps1",使用 findstr /v /b /l "@@" 进行字面意义的行首匹配,并消除 @echo off 行:@@findstr /v /b /l "@@" "%~f0" > "%TEMP%\%~0n.ps1" & powershell -NoProfile -ExecutionPolicy Bypass -File "%TEMP%\%~0n.ps1" %* & del "%TEMP%\%~0n.ps1" & goto :eof - mklement0

2

以下是另一个批处理+PowerShell脚本的示例...它比其他提出的解决方案更简单,并具有其它方案无法匹配的特点:

  • 不创建临时文件 => 性能更好,并且没有覆盖任何文件的风险。
  • 没有特殊前缀的批处理代码。这只是正常的批处理。PowerShell代码也是如此。
  • 正确地将所有批处理参数传递给PowerShell,即使是带有! % < > ' $等特殊字符的引用字符串。
  • 可以通过重复引号来传递双引号。
  • PowerShell中可使用标准输入。(与将批处理本身管道传递到PowerShell的所有版本相反。)

此示例显示语言转换,并且PowerShell端显示从批处理端接收到的参数列表。

<# :# PowerShell comment protecting the Batch section
@echo off
:# Disabling argument expansion avoids issues with ! in arguments.
setlocal EnableExtensions DisableDelayedExpansion

:# Prepare the batch arguments, so that PowerShell parses them correctly
set ARGS=%*
if defined ARGS set ARGS=%ARGS:"=\"%
if defined ARGS set ARGS=%ARGS:'=''%

:# The ^ before the first " ensures that the Batch parser does not enter quoted mode
:# there, but that it enters and exits quoted mode for every subsequent pair of ".
:# This in turn protects the possible special chars & | < > within quoted arguments.
:# Then the \ before each pair of " ensures that PowerShell's C command line parser 
:# considers these pairs as part of the first and only argument following -c.
:# Cherry on the cake, it's possible to pass a " to PS by entering two "" in the bat args.
echo In Batch
PowerShell -c ^"Invoke-Expression ('^& {' + [io.file]::ReadAllText(\"%~f0\") + '} %ARGS%')"
echo Back in Batch. PowerShell exit code = %ERRORLEVEL%
exit /b

###############################################################################
End of the PS comment around the Batch section; Begin the PowerShell section #>

echo "In PowerShell"
$Args | % { "PowerShell Args[{0}] = '$_'" -f $i++ }
exit 0

请注意,我使用:#来进行批注,而不是像其他大多数人一样使用::,因为这使它们看起来更像PowerShell的注释。(或者实际上更像大多数其他脚本语言的注释。)

2

我目前对这个任务的偏好是一种多语言头文件,其工作方式与mklement0的第一个解决方案类似:

<#  :cmd header for PowerShell script
@   set dir=%~dp0
@   set ps1="%TMP%\%~n0-%RANDOM%-%RANDOM%-%RANDOM%-%RANDOM%.ps1"
@   copy /b /y "%~f0" %ps1% >nul
@   powershell -NoProfile -ExecutionPolicy Bypass -File %ps1% %*
@   del /f %ps1%
@   goto :eof
#>

# Paste arbitrary PowerShell code here.
# In this example, all arguments are echoed.
$Args | % { 'arg #{0}: [{1}]' -f ++$i, $_ }

我更喜欢将cmd头部分成多行,每行只放一个命令,原因如下。首先,我认为这样更容易看出正在发生的事情:命令行不会太长而导致超出编辑窗口右侧,左侧的标点符号列可以视觉上标记为标题块,第一行上滥用的标签也表明了它是什么。其次,delgoto 命令位于它们自己的行上,因此即使作为脚本参数传递了非常奇怪的东西,它们仍然会运行。
我已经开始更喜欢制作临时的.ps1文件的解决方案,而不是依赖于Invoke-Expression,纯粹是因为PowerShell晦涩的错误消息至少包括有意义的行号。
制作临时文件所需的时间通常完全被PowerShell本身启动所需的时间淹没了,而在临时文件名中嵌入128位%RANDOM%几乎保证了多个并发脚本永远不会互相覆盖它们的临时文件。唯一真正的缺点是可能会丢失有关从哪个目录调用原始cmd脚本的信息,这就是第二行创建的dir环境变量的理由。
显然,PowerShell不会对其接受的脚本文件扩展名如此严格要求会更少让人恼火,但你必须使用你拥有的shell而不是你希望拥有的shell。
说到这里:正如mklement0所观察到的,
# BREAKS, due to the `&` inside \"...\"
sample.cmd "A \"rock & roll\" life style"

这确实会出现问题,因为cmd.exe的参数解析非常糟糕。我通常发现,我尽量不去“隐藏”cmd的许多限制,就能在后续避免更少的意外错误(例如,我肯定可以想出包含括号的参数,从而打破mklement0否则完美的&转义逻辑)。在我看来,使用类似于...的东西会减轻痛苦。

sample.cmd "A \"rock ^^^& roll\" life style"

第一个和第三个 ^ 转义字符在命令行初始解析时被过滤掉了;第二个转义字符则保留下来,用于转义嵌入到传递给 powershell.exe 的命令行中的 & 符号。是的,这很丑陋。是的,这确实使得假装 cmd.exe 不是首先处理脚本变得更加困难。不必担心它。如果有必要,请记录它。
在大多数实际应用程序中,& 问题都无关紧要。像这样的脚本将作为参数传递的大多数内容都是通过拖放方式获取的路径名。Windows 会对其进行引号转义,这足以保护空格、& 和实际上任何除引号之外的字符,因为在 Windows 路径名中不允许使用引号。
甚至不要让我开始谈论在 CSV 文件中出现的 Vinyl LP's, 12"

我已经认识到不要试图隐藏 cmd.exe 的缺陷解析 - 感谢你的 ^^^ 解决方案 - 并且我已经移除了我的第二个解决方案(我现在链接到你的答案(你已经有我的+1))。我还修改了我的第一个解决方案以通过 PowerShell 的退出码(必须将我的单行方法拆分成3行)传递。 - mklement0

2
使用Invoke-Command(简称为icm),我们可以在ps1文件中添加以下4行头部信息,使其成为有效的cmd批处理文件:
<# : batch portion
@powershell -noprofile "& {icm -ScriptBlock ([Scriptblock]::Create((cat -Raw '%~f0'))) -NoNewScope -ArgumentList $args}" %*
@exit /b %errorlevel%
: end batch / begin powershell #>

"Result:"
$args | %{ "`$args[{0}]: $_" -f $i++ }

如果想让args[0]指向脚本路径,将%*替换为"'%~f0'" %*即可。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接