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

Don*_*nce 19 windows powershell scripting cmd batch-file

如何将PowerShell脚本嵌入到与Windows批处理脚本相同的文件中?

我知道在其他情况下这种事情是可能的:

  • 在批处理脚本中嵌入SQL,sqlcmd并在文件开头巧妙地安排goto和注释
  • 在具有程序名称的*nix环境中,您希望在脚本的第一行运行脚本,例如,#!/usr/local/bin/python.

可能没有办法做到这一点 - 在这种情况下,我将不得不从启动脚本调用单独的PowerShell脚本.

我考虑过的一个可能的解决方案是回显PowerShell脚本,然后运行它.这样做的一个很好的理由是,尝试这个的部分原因是使用PowerShell环境的优点而没有例如转义字符的痛苦

我有一些不寻常的限制,并希望找到一个优雅的解决方案.我怀疑这个问题可能是各种各样的反应:"你为什么不尝试解决这个不同的问题." 我只想说这些是我的约束,对此感到抱歉.

有任何想法吗?是否有一个巧妙的评论和转义字符的合适组合,这将使我能够实现这一目标?

关于如何实现这一点的一些想法:

  • ^一行中的克拉是一个延续 - 就像Visual Basic中的下划线一样
  • &符号&通常用于将命令echo Hello & echo World结果分成两个回声在不同的行上
  • %0将为您提供当前正在运行的脚本

所以这样的事情(如果我可以使它工作)会很好:

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

除了...

  • 它不起作用......因为
  • 文件的扩展名不符合PowerShell的喜好: Windows PowerShell console file "insideout.bat" extension is not psc1. Windows PowerShell console file extension must be psc1.
  • CMD对这种情况并不是完全满意 - 尽管它偶然发现了 '#', it is not recognized as an internal or external command, operable program or batch file.

Car*_*rez 18

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

dosps2.cmd:

@findstr/v "^@f.*&" "%~f0"|powershell -&goto:eof
Write-Output "Hello World" 
Write-Output "Hello some@com & again" 
Run Code Online (Sandbox Code Playgroud)

正则表达式排除了线路开始@f和包括&并通过一切来的PowerShell.

C:\tmp>dosps2
Hello World
Hello some@com & again
Run Code Online (Sandbox Code Playgroud)

  • 这个答案根本不支持论据.我打赌你很快会想要这个功能. (2认同)

Jay*_*uzi 12

听起来你正在寻找有时被称为"多语言脚本"的东西.对于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
Run Code Online (Sandbox Code Playgroud)

如果你不需要支持引用的参数,你甚至可以使它成为一个单行:

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

取自http://blogs.msdn.com/jaybaz_ms/archive/2007/04/26/powershell-polyglot.aspx.那是PowerShell v1; 它在v2中可能更简单,但我没看过.

  • 使用`;`连接会导致多行命令出现问题(使用反引号,多行上的管道,多行上的parens内容等).加入`[char] 10`为我解决了问题. (6认同)

Car*_*rez 5

如果您不介意 PowerShell 一开始出现一个错误,这似乎可行:

dosps.cmd:

@powershell -<%~f0&goto:eof
Write-Output "Hello World" 
Write-Output "Hello World again" 
Run Code Online (Sandbox Code Playgroud)


npo*_*aka 5

这里讨论了这个话题。主要目标是避免使用临时文件以减少缓慢的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;
Run Code Online (Sandbox Code Playgroud)

更好的方法(在此处看到):

<# : 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
Run Code Online (Sandbox Code Playgroud)

其中POWERSHELL_BAT_ARGS是命令行参数首先设置为在间歇部变量。

技巧是在批处理重定向优先级中-该行将<# :像一样进行解析:<#,因为重定向的优先级高于其他命令。

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

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


mkl*_*nt0 5

还要考虑这个“多语言”包装脚本,它支持嵌入式 PowerShell 和/或 VBScript/JScript 代码;它改编自作者本人flabdablet于 2013 年发布的巧妙原创文章,但由于仅提供链接答案,该答案已于 2015 年被删除。

一个改进凯尔优秀答案的解决方案:

sample.cmd创建一个包含以下内容的批处理文件(例如):

<# ::
@echo off & setlocal
set "__thisBatchFile=%~f0"
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.
# Use $env:__thisBatchFile to determine this batch file's full path.
'Args:'
$Args | % { 'arg #{0}: [{1}]' -f ++$i, $_ }
Run Code Online (Sandbox Code Playgroud)

笔记:

  • 批处理文件运行时,*.ps1会在文件夹中创建一个临时文件,稍后清理%TEMP%;这样做大大简化了(合理地)稳健地传递参数,只需使用%*
  • 以上调用Windows PowerShell。要调用跨平台PowerShell(Core)v7+版本,请在上面的代码中替换powershell为。pwsh

该技术的解释

  • Line<# ::是一个混合行,PowerShell 将其视为注释块的开始,但会忽略它,这是一种从npocmaka 的答案cmd.exe借用的技术。

  • 因此,以 开头的批处理文件命令@将被 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允许将其作为单行执行,但这可能会导致对!参数中的 chars. 进行不必要的解释。)


为了证明参数传递的稳健性

假设上面的代码已保存为sample.cmd,调用它为:

sample.cmd "val. w/ spaces & special chars. (\|<>'), on %OS%" 666 "Lisa \"Left Eye\" Lopez"
Run Code Online (Sandbox Code Playgroud)

产生类似以下内容:

<# ::
@echo off & setlocal
set "__thisBatchFile=%~f0"
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.
# Use $env:__thisBatchFile to determine this batch file's full path.
'Args:'
$Args | % { 'arg #{0}: [{1}]' -f ++$i, $_ }
Run Code Online (Sandbox Code Playgroud)

注意如何嵌入"字符。被传递为\".
但是,存在与嵌入字符相关的边缘情况"

:: # 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"
Run Code Online (Sandbox Code Playgroud)

这些困难归因于cmd.exe有缺陷的参数解析,最终,试图隐藏这些缺陷是毫无意义的,正如flabdablet 在他的出色回答中指出的那样

正如他所解释的,在序列中使用 (sic)转义以下cmd.exe元字符可以解决问题:^^^\"...\"

& | < >
Run Code Online (Sandbox Code Playgroud)

使用上面的例子:

:: # OK: cmd.exe metachars. inside \"...\" are ^^^-escaped.
sample.cmd "A \"rock ^^^& roll\" life style"
Run Code Online (Sandbox Code Playgroud)


小智 5

我非常喜欢Jean-Fran\xc3\xa7ois Larvoire\ 的解决方案,特别是他对参数的处理并将它们直接传递给 powershell 脚本(添加了+1)。

\n\n

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

\n\n

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

\n\n
PowerShell -c ^"Invoke-Expression (\'^& {\' + [io.file]::ReadAllText(\'%~f0\') + \'} %ARGS%\')"\n
Run Code Online (Sandbox Code Playgroud)\n\n

就我个人而言,我更喜欢使用get-content-raw选项,因为对我来说,这更像是 powershell:

\n\n
PowerShell -c ^"Invoke-Expression (\'^& {\' + (get-content -raw \'%~f0\') + \'} %ARGS%\')"\n
Run Code Online (Sandbox Code Playgroud)\n\n

但这当然只是我个人的意见。ReadAllText works非常完美。

\n\n

为了完整起见,更正后的脚本:

\n\n
<# :# PowerShell comment protecting the Batch section\n@echo off\n:# Disabling argument expansion avoids issues with ! in arguments.\nsetlocal EnableExtensions DisableDelayedExpansion\n\n:# Prepare the batch arguments, so that PowerShell parses them correctly\nset ARGS=%*\nif defined ARGS set ARGS=%ARGS:"=\\"%\nif defined ARGS set ARGS=%ARGS:\'=\'\'%\n\n:# The ^ before the first " ensures that the Batch parser does not enter quoted mode\n:# there, but that it enters and exits quoted mode for every subsequent pair of ".\n:# This in turn protects the possible special chars & | < > within quoted arguments.\n:# Then the \\ before each pair of " ensures that PowerShell\'s C command line parser \n:# considers these pairs as part of the first and only argument following -c.\n:# Cherry on the cake, it\'s possible to pass a " to PS by entering two "" in the bat args.\necho In Batch\nPowerShell -c ^"Invoke-Expression (\'^& {\' + (get-content -raw \'%~f0\') + \'} %ARGS%\')"\necho Back in Batch. PowerShell exit code = %ERRORLEVEL%\nexit /b\n\n###############################################################################\nEnd of the PS comment around the Batch section; Begin the PowerShell section #>\n\necho "In PowerShell"\n$Args | % { "PowerShell Args[{0}] = \'$_\'" -f $i++ }\nexit 0\n
Run Code Online (Sandbox Code Playgroud)\n