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

时间:2010-04-09 18:43:05

标签: windows powershell batch-file cmd scripting

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

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

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

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

我考虑过的一个可能的解决方案是回显PowerShell脚本,然后运行它。 执行此操作的一个很好的理由是,尝试此操作的部分原因是使用PowerShell环境的优势而不会感到痛苦,例如escape characters

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

有什么想法吗?巧妙的评论和转义字符的合适组合是否能够实现这一目标?

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

  • 行末尾的一个克拉^是一个延续 - 就像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"

...除

  • 它不起作用......因为
  • 文件的扩展名不符合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.

10 个答案:

答案 0 :(得分:17)

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

dosps2.cmd

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

regular expression排除以@f开头并包含&的行,并将其他所有内容传递给PowerShell。

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

答案 1 :(得分:11)

听起来你正在寻找有时被称为“多语言剧本”的东西。对于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中可能更简单,但我没看过。

答案 2 :(得分:3)

如果你不介意开头的PowerShell中的一个错误,这似乎有效:

dosps.cmd

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

答案 3 :(得分:3)

Here已讨论过该主题。主要目标是避免使用临时文件来减少缓慢的IO操作并在没有冗余输出的情况下运行脚本。

根据我的说法,这是最好的解决方案:

public static void main(String[] args) {
    Scanner scan = new Scanner(System.in);
    int size = scan.nextInt();
    int sticks [] = new int[size];
    for (int i = 0; i < size; i++){
        sticks[i] = scan.nextInt();
    }    
    Arrays.sort(sticks);
    do {
    int count =0;
    int leastLength = sticks[0];
    for (int j=0; j < sticks.length; j++){
        sticks[j] = sticks[j] - leastLength;
        count++;
    }
    System.out.println(count);
    List<Integer> resizeArray = new LinkedList<Integer>();
    for ( int i=0; i< sticks.length; i++){
        if (sticks[i] != 0){
            resizeArray.add(sticks[i]);
        }
    }
    int temp[] = new int[resizeArray.size()];
      for (int i = 0; i < resizeArray.size(); i ++){
          temp[i] = resizeArray.get(i);
      }
      sticks = temp;
    } while (sticks.length > 0);
}

修改(更好的方式here

<# :
@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将被解析为<# :,因为重定向的prio高于其他命令。 但批处理文件中以:<#开头的行被视为标签 - 即未执行。这仍然是一个有效的PowerShell评论。

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

答案 4 :(得分:2)

还要考虑this "polyglot" wrapper script支持嵌入式PowerShell和/或VBScript / JScript cod ;它改编自作者本人this ingenious original于2013年发布的flabdablet,但由于仅作为链接的答案而在2015年被删除,因此它变得黯然失色。 功能

改进Kyle's excellent answer的解决方案:

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

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

注意:之后清理的临时*.ps1文件将在%TEMP%文件夹中创建;通过使用%*

,这样做可以极大地简化通过(合理)稳健地传递参数
  • <# ::是一个混合行,PowerShell将其视为注释块的开头,但cmd.exe忽略了一种从npocmaka's answer借来的技术。

    < / LI>
  • PowerShell会忽略以@开头的批处理文件命令,但由cmd.exe执行;由于上一个@ - 带前缀的行以exit /b结尾,并在那里退出批处理文件,cmd.exe 忽略文件的其余部分,因此是免费的包含非批处理文件代码,即PowerShell代码。

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

  • 因为整个文件是一个有效的PowerShell文件,所以不需要findstr技巧来提取PowerShell代码;但是,因为PowerShell只执行文件扩展名为.ps1的脚本,所以必须创建批处理文件的(临时)副本; %TEMP%\%~0n.ps1在为批处理文件(%TEMP%)命名的%~0n文件夹中创建临时副本,但扩展名为.ps1;临时文件将在完成时自动删除。

  • 请注意,为了传递PowerShell命令的退出代码,需要3个单独的 cmd.exe语句。
    (使用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有缺陷的论证解析,最终尝试隐藏这些瑕疵毫无意义,因为{ {3}}

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

& | < >

使用上面的例子:

:: # OK: cmd.exe metachars. inside \"...\" are ^^^-escaped.
sample.cmd "A \"rock ^^^& roll\" life style"

答案 5 :(得分:1)

如果没有完全理解你的问题,我的建议将是:

@echo off
set MYSCRIPT="some cool powershell code"
powershell -c %MYSCRIPT%

或更好

@echo off
set MYSCRIPTPATH=c:\work\bin\powershellscript.ps1
powershell %MYSCRIPTPATH%

答案 6 :(得分:1)

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

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

答案 7 :(得分:1)

我目前对此任务的偏好是一个多语言标题,与mklement0's first solution的工作方式非常相似:

<#  :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路径名中使用。

甚至不让我开始Vinyl LP's, 12"出现在CSV文件中。

答案 8 :(得分:0)

另一个样本批处理+ PowerShell脚本...它比其他提议的解决方案更简单,并且具有以下任何一个都无法匹配的特征:

  • 没有创建临时文件=&gt;更好的性能,没有覆盖任何东西的风险。
  • 批处理代码没有特殊的前缀。这只是正常的批次。 PowerShell代码也是如此。
  • 正确地将所有批处理参数传递给PowerShell,甚至引用带有棘手字符的字符串! %&lt; &GT; '$
  • 双引号可以通过加倍来传递。
  • 标准输入可在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注释。 (或者像大多数其他脚本语言实际上一样评论。)

答案 9 :(得分:0)

我非常喜欢Jean-François Larvoire's解决方案,尤其是对于他处理参数并将其直接传递给powershell脚本(添加了+1)而言。

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

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

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

我个人更喜欢将get-content-raw选项一起使用,因为对我来说这更像是powershell'ish:

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

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

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

<# :# 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