How to run a PowerShell script within a Windows batch file

29,161

Solution 1

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

Solution 2

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.

Solution 3

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.

Solution 4

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" 

Solution 5

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:

Create a batch file (e.g. sample.cmd) with the following content:

<# ::
@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, $_ }

Note:

  • When the batch file runs, 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 %*
  • The above invokes Windows PowerShell. To call the cross-platform PowerShell (Core) v7+ edition, replace powershell with pwsh in the code above.

Explanation of the technique:

  • 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%\%~n0.ps1 creates the temporary copy in the %TEMP% folder named for the batch file (%~n0), 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"
Share:
29,161

Related videos on Youtube

Don Vince
Author by

Don Vince

↖️ check it out, you can use a transparent png for your avatar and have a dark mode capable profile 🥳 I'm loving working at skyscanner (travel search: flights/hotels/car-hire), you should join us! Give me a shout on linkedin if you'd like me to pass your CV to our recruitment team.

Updated on March 18, 2022

Comments

  • Don Vince
    Don Vince about 2 years

    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:

    • Embedding SQL in a batch script using sqlcmd and a clever arrangements of goto's and comments at the beginning of the file
    • In a *nix environment having the name of the program you wish to run the script with on the first line of the script commented out, for example, #!/usr/local/bin/python.

    There may not be a way to do this - in which case I will have to call the separate PowerShell script from the launching script.

    One possible solution I've considered is to echo out the PowerShell script, and then run it. A good reason to not do this is that part of the reason to attempt this is to be using the advantages of the PowerShell environment without the pain of, for example, escape characters

    I have some unusual constraints and would like to find an elegant solution. I suspect this question may be baiting responses of the variety: "Why don't you try and solve this different problem instead." Suffice to say these are my constraints, sorry about that.

    Any ideas? Is there a suitable combination of clever comments and escape characters that will enable me to achieve this?

    Some thoughts on how to achieve this:

    • A carat ^ at the end of a line is a continuation - like an underscore in Visual Basic
    • An ampersand & typically is used to separate commands echo Hello & echo World results in two echos on separate lines
    • %0 will give you the script that's currently running

    So something like this (if I could make it work) would be good:

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

    Except...

    • It doesn't work... because
    • the extension of the file isn't as per PowerShell's liking: Windows PowerShell console file "insideout.bat" extension is not psc1. Windows PowerShell console file extension must be psc1.
    • CMD isn't really altogether happy with the situation either - although it does stumble on '#', it is not recognized as an internal or external command, operable program or batch file.
  • Don Vince
    Don Vince about 14 years
    Many thanks - it's not quite what I'm after though: Like the first part of your answer I want to incorporate the powershell script directly into the batch file, but by passing the script itself to powershell to run. I'll perhaps update my question with an example of how this might look-ish
  • Don Vince
    Don Vince about 14 years
    Nice work - got past my issue of having it actually run the powershell script! I'm still in the business for more answers though if we can get rid of the powershell errors
  • Don Vince
    Don Vince about 14 years
    That's a perfect answer - clean - no errors, works! Gold star to you sir. Only amend I had to make to get it working: to work with spaces in the filename/path I needed to put "quotes" around the %~f0: @findstr/v "^@.*&" "%~f0"|powershell -&goto:eof Thanks again
  • Jay Bazuzi
    Jay Bazuzi about 14 years
    I made two changes. 1) Use @@ as the prefix to look for, so it doesn't get confused by a here-string. 2) use spaces to separate elements of the command line, for readability
  • Jay Bazuzi
    Jay Bazuzi about 14 years
    This answer doesn't support arguments at all. I bet you'll want that feature soon.
  • Qwertie
    Qwertie about 13 years
    Why does it create the POWERSHELL_BAT_ARGS variable and then check whether it is defined? What does the :"=\"% part do?
  • Jay Bazuzi
    Jay Bazuzi about 13 years
    @Qwertie: First it's set to %*, which is a list of all arguments. If none were passed, then the variable remains unset, so the next line does nothing. The %FOO:X=Y% will replace all instances of X in %FOO% with Y. In this case, we are escaping quote characters with a backslash. Make sense?
  • Qwertie
    Qwertie about 13 years
    Thanks, that's really neat! So the DOS shell has a built-in find-and-replace, but only for environment variables. Presumably certain chars like % and = cannot be involved in the replacement... pretty lucky that neither quotation mark nor backslash are treated as special.
  • Rynant
    Rynant about 12 years
    Joining with ; causes problems with multi-line commands (using the backtick, pipeline on multiple lines, contents of parens on multiple lines, etc.). Joining with [char]10 fixed the problem for me.
  • jsxt
    jsxt over 9 years
    Googling the question brought me to this thread. I have used this solution (with few minor changes) in the batch script producing hybrid powershell-in-cmd (others are supported as well: js, vbs, wsf, hta, pl). Follow this thread: dostips.com/forum/viewtopic.php?p=37780#p37780
  • holtavolt
    holtavolt about 8 years
    This is a nice solution. Powershell multi-line format, plus cmd line args passed in, with no reliance on findstr. Thanks for this.
  • mojo
    mojo almost 7 years
    I would also change reading the file to (Get-Content '%~sf0') (note: added s: short file name) so that the path to the file doesn't contain spaces or other badness that will confuse the double-parsing of data by both PowerShell and cmd.
  • mklement0
    mklement0 about 6 years
    +1 for elegance; if you place -File before "%~f0.ps1", your solution will handle arguments such as "a b" and "a & b" correctly too (Windows PowerShell defaults to -Command, which parses arguments differently). A slight improvement would be to create the temporary file in the %TEMP% folder: "%TEMP%\%~0n.ps1", to use literal start-of-line matching with findstr /v /b /l "@@", and to eliminate the @echo off line: @@findstr /v /b /l "@@" "%~f0" > "%TEMP%\%~0n.ps1" & powershell -NoProfile -ExecutionPolicy Bypass -File "%TEMP%\%~0n.ps1" %* & del "%TEMP%\%~0n.ps1" & goto :eof
  • mklement0
    mklement0 about 6 years
    I've come around to not trying to hide cmd.exe's flawed parsing - thanks for your ^^^ workaround - and I've removed my 2nd solution (and I now link to your answer (you already had my +1)). I've also modified my 1st solution to pass PowerShell's exit code through (which necessitated splitting my single-line approach into 3 lines).
  • Jean-François Larvoire
    Jean-François Larvoire over 3 years
    Hi, My version indeed fails if the script pathname contains a dollar, thanks reporting it... ... But your version will fail if the script pathname contains a single quote! Both are unlikely, but possible :-) To get the best of both worlds, we'd need to start from your solution, but preprocess the pathname exactly as I do for the arguments.
  • Barleyman
    Barleyman over 2 years
    Adding executionpolicy might be prudent to allow running powershell bits on default windows installation.