代码之家  ›  专栏  ›  技术社区  ›  Don Vince

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

  •  17
  • Don Vince  · 技术社区  · 16 年前

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

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

    • 使用在批处理脚本中嵌入SQL sqlcmd 在文件开头巧妙地安排了Goto和评论
    • 例如,在一个*nix环境中,您希望在脚本的第一行用注释掉的程序名运行脚本, #!/usr/local/bin/python .

    可能没有办法做到这一点——在这种情况下,我必须从启动脚本调用单独的PowerShell脚本。

    我考虑的一个可能的解决方案是回送PowerShell脚本,然后运行它。一个很好的理由 不 这样做的部分原因是,尝试这样做的部分原因是使用PowerShell环境的优势,而不需要 escape characters

    我有一些不寻常的限制,希望找到一个优雅的解决方案。我怀疑这个问题可能是各种各样的回答:“你为什么不试着去解决这个不同的问题呢?”我只想说这些是我的限制,对此很抱歉。

    有什么想法吗?是否有一个聪明的评论和转义字符的适当组合,使我能够做到这一点?

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

    • 克拉 ^ 行尾是一个延续符-就像VisualBasic中的下划线
    • 吟游诗人 & 通常用于分隔命令 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.
    9 回复  |  直到 8 年前
        1
  •  17
  •   Peter Mortensen Pieter Jan Bonestroo    11 年前

    这一个只将正确的行传递给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
    
        2
  •  10
  •   mxxk    9 年前

    听起来你在寻找一个有时被称为“多语脚本”的东西。对于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 . 那是PowerShellv1;在v2中可能更简单,但我还没有找到。

        3
  •  3
  •   Peter Mortensen Pieter Jan Bonestroo    11 年前

    如果一开始不介意PowerShell中的一个错误,这似乎可以工作:

    dosps.cmd :

    @powershell -<%~f0&goto:eof
    Write-Output "Hello World" 
    Write-Output "Hello World again" 
    
        4
  •  3
  •   npocmaka    8 年前

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

    我认为最好的解决方案是:

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

    编辑 (更好的方式 here )

    <# : 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读取和执行 %~f0 这是由cmd.exe执行的脚本的完整路径。

        5
  •  2
  •   mklement0    8 年前

    也考虑 this "polyglot" wrapper script 哪一个 支持嵌入式PowerShell和/或vbscript/jscript-cod 它改编自 this ingenious original 作者本人, flabdablet ,于2013年发布,但由于是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 .

    • 批处理文件命令 @ 因此被PowerShell忽略,但由执行 命令提示符 ;从上一次 @ -带前缀的行尾为 exit /b ,退出批处理文件, 命令提示符 忽视 文件的其余部分,因此可以自由包含非批处理文件代码,即PowerShell代码。

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

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

    • 注意3个分开 线 属于 命令提示符 需要语句才能通过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"
    

    这些困难是由于 命令提示符 有缺陷的参数解析 最后,尝试 隐藏 这些缺点,如 flabdablet points out in his excellent answer .

    正如他解释的那样, 逃避以下 命令提示符 元字符 ^^^ (sic)内部 \"...\" 顺序解决问题:

    & | < >
    

    使用上面的示例:

    :: # OK: cmd.exe metachars. inside \"...\" are ^^^-escaped.
    sample.cmd "A \"rock ^^^& roll\" life style"
    
        6
  •  1
  •   John Weldon user3678248    16 年前

    如果不完全理解您的问题,我的建议如下:

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

    还是更好

    @echo off
    set MYSCRIPTPATH=c:\work\bin\powershellscript.ps1
    powershell %MYSCRIPTPATH%
    
        7
  •  1
  •   Kyle    13 年前

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

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

    我当前对此任务的首选项是polyglot头,其工作方式与 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头放在多行中,每行只有一个命令。首先,我认为更容易看到发生了什么:命令行足够短,不会从我的编辑窗口的右边跑掉,左边的标点栏将它直观地标记为标题栏,第一行被严重滥用的标签上说它是标题栏。第二, del 和 goto 命令在它们自己的行上,所以即使一些真正的古怪的东西作为脚本参数传递,它们仍然可以运行。

    我更喜欢临时的解决方案 .ps1 向那些依赖 Invoke-Expression ,这纯粹是因为PowerShell的不可破解错误消息将至少包含有意义的行号。

    生成临时文件所需的时间通常完全被PowerShell自己投入运行所占用的时间所淹没,价值128位 %RANDOM% 嵌入在临时文件的名称中,几乎可以保证多个并发脚本不会互相叠加临时文件。临时文件方法的唯一真正缺点是可能丢失有关调用原始命令脚本的目录的信息,这是 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"
    

    第一和第三 ^ 当命令行最初被解析时,escape会被吃掉;第二个命令行幸存下来以逃脱 & 嵌入在传递给的命令行中 powershell.exe . 是的,这很难看。是的,这确实让你很难假装 命令提示符 这不是剧本的第一个亮点。别担心。如果重要,记录下来。

    在大多数实际应用中, & 无论如何,这个问题是没有意义的。将作为参数传递给类似这样的脚本的大部分内容将是通过拖放到达的路径名。Windows将引用这些内容,这些内容足以保护空格和符号,事实上,除了引号以外的任何内容,在Windows路径名中都不允许使用这些内容。

    别让我开始 Vinyl LP's, 12" 在csv文件中显示。

        9
  •  0
  •   Jean-François Larvoire    8 年前

    另一个示例批处理+PowerShell脚本…它比其他建议的解决方案简单,并且具有任何一个都无法匹配的特性:

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