How to run a PowerShell script within a Windows batch file

前端 未结 10 1898
半阙折子戏
半阙折子戏 2020-11-27 16:22

How do I have a PowerShell script embedded within the same file as a Windows batch script?

I know this kind of thing is possible in other scenarios:

  • Em
相关标签:
10条回答
  • 2020-11-27 16:41

    Here the topic has been discussed. The main goals were to avoid the usage of temporary files to reduce the slow I/O operations and to run the script without redundant output.

    And here's the best solution according to me:

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

    An even better way (seen 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
    

    where POWERSHELL_BAT_ARGS are command line arguments first set as variable in the batch part.

    The trick is in the batch redirection priority - this line <# : will be parsed like :<#, because redirection is with higher priority than the other commands.

    But the lines starting with : in batch files are taken as labels - i.e., not executed. Still this remains a valid PowerShell comment.

    The only thing left is to find a proper way for PowerShell to read and execute %~f0 which is the full path to the script executed by cmd.exe.

    0 讨论(0)
  • 2020-11-27 16:42

    This seems to work, if you don't mind one error in PowerShell at the beginning:

    dosps.cmd:

    @powershell -<%~f0&goto:eof
    Write-Output "Hello World" 
    Write-Output "Hello World again" 
    
    0 讨论(0)
  • 2020-11-27 16:42

    Also consider this "polyglot" wrapper script, which supports embedded PowerShell and/or VBScript/JScript code; it was adapted from this ingenious original, which the author himself, flabdablet, had posted in 2013, but it languished due to being a link-only answer, which was deleted in 2015.

    A solution that improves on 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, $_ }
    

    Note: A temporary *.ps1 file that is cleaned up afterwards is created in the %TEMP% folder; doing so greatly simplifies passing arguments through (reasonably) robustly, simply by using %*

    • Line <# :: is a hybrid line that PowerShell sees as the start of a comment block, but cmd.exe ignores, a technique borrowed from npocmaka's answer.

    • The batch-file commands that start with @ are therefore ignored by PowerShell, but executed by cmd.exe; since the last @-prefixed line ends with exit /b, which exits the batch file right there, cmd.exe ignores the rest of the file, which is therefore free to contain non-batch-file code, i.e., PowerShell code.

    • The #> line ends the PowerShell comment block that encloses the batch-file code.

    • Because the file as a whole is therefore a valid PowerShell file, no findstr trickery is needed to extract the PowerShell code; however, because PowerShell only executes scripts that have filename extension .ps1, a (temporary) copy of the batch file must be created; %TEMP%\%~0n.ps1 creates the temporary copy in the %TEMP% folder named for the batch file (%~0n), but with extension .ps1 instead; the temporarily file is automatically removed on completion.

    • Note that 3 separate lines of cmd.exe statements are needed in order to pass the PowerShell command's exit code through.
      (Using setlocal enabledelayedexpansion hypothetically allows doing it as a single line, but that can result in unwanted interpretation of ! chars. in arguments.)


    To demonstrate the robustness of the argument passing:

    Assuming the code above has been saved as sample.cmd, invoking it as:

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

    yields something like the following:

    Args:
    arg #1: [val. w/ spaces & special chars. (\|<>'), on Windows_NT]
    arg #2: [666]
    arg #3: [Lisa "Left Eye" Lopez]
    

    Note how embedded " chars. were passed as \".
    However, there are edge cases related to embedded " chars.:

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

    These difficulties are owed to cmd.exe's flawed argument parsing, and ultimately it is pointless to try to hide these flaws, as flabdablet points out in his excellent answer.

    As he explains, escaping the following cmd.exe metacharacters with ^^^ (sic) inside the \"...\" sequence solves the problem:

    & | < >
    

    Using the example above:

    :: # OK: cmd.exe metachars. inside \"...\" are ^^^-escaped.
    sample.cmd "A \"rock ^^^& roll\" life style"
    
    0 讨论(0)
  • 2020-11-27 16:45

    This one only passes the right lines to PowerShell:

    dosps2.cmd:

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

    The regular expression excludes the lines starting with @f and including an & and passes everything else to PowerShell.

    C:\tmp>dosps2
    Hello World
    Hello some@com & again
    
    0 讨论(0)
  • 2020-11-27 16:50

    This supports arguments unlike the solution posted by Carlos and doesn't break multi-line commands or the use of param like the solution posted by Jay. Only downside is that this solution creates a temporary file. For my use case that is acceptable.

    @@echo off
    @@findstr/v "^@@.*" "%~f0" > "%~f0.ps1" & powershell -ExecutionPolicy ByPass "%~f0.ps1" %* & del "%~f0.ps1" & goto:eof
    
    0 讨论(0)
  • 2020-11-27 16:55

    It sounds like you're looking for what is sometimes called a "polyglot script". For 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
    

    If you don't need to support quoted arguments, you can even make it a one-liner:

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

    Taken from http://blogs.msdn.com/jaybaz_ms/archive/2007/04/26/powershell-polyglot.aspx. That was PowerShell v1; it may be simpler in v2, but I haven't looked.

    0 讨论(0)
提交回复
热议问题