25

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.
Ross Ridge
  • 38,414
  • 7
  • 81
  • 112
Don Vince
  • 1,282
  • 3
  • 18
  • 28

18 Answers18

26

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
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Carlos Gutiérrez
  • 13,972
  • 5
  • 37
  • 47
  • 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 – Don Vince Apr 10 '10 at 09:38
  • 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 Apr 10 '10 at 17:20
  • 2
    This answer doesn't support arguments at all. I bet you'll want that feature soon. – Jay Bazuzi Apr 10 '10 at 17:21
  • I don't like how this doesn't pass through return codes – xjcl Jun 20 '23 at 13:28
  • Also doesn't support pause/Read-Host :/ – xjcl Jun 21 '23 at 09:21
15

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.

mxxk
  • 9,514
  • 5
  • 38
  • 46
Jay Bazuzi
  • 45,157
  • 15
  • 111
  • 168
  • Why does it create the POWERSHELL_BAT_ARGS variable and then check whether it is defined? What does the :"=\"% part do? – Qwertie Apr 12 '11 at 23:54
  • @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? – Jay Bazuzi Apr 13 '11 at 01:21
  • 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. – Qwertie Apr 13 '11 at 15:52
  • 6
    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. – Rynant Feb 22 '12 at 16:07
  • 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: http://www.dostips.com/forum/viewtopic.php?p=37780#p37780 – jsxt Nov 09 '14 at 15:31
  • 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. – mojo Jun 27 '17 at 15:25
8

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.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
npocmaka
  • 55,367
  • 18
  • 148
  • 187
  • This is a nice solution. Powershell multi-line format, plus cmd line args passed in, with no reliance on findstr. Thanks for this. – holtavolt Apr 01 '16 at 19:24
  • Adding executionpolicy might be prudent to allow running powershell bits on default windows installation. – Barleyman Dec 15 '21 at 19:01
  • if you get "This script contains malicious content and has been blocked by your antivirus software." then the script got blocked by Defender caused by this fragment "<# :" – Stefano Aug 05 '22 at 07:55
  • 1
    @Stefano - this is something new. At the moment I'm working on a linux machine and I'll be able to test this later. – npocmaka Aug 05 '22 at 10:24
5

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" 
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Carlos Gutiérrez
  • 13,972
  • 5
  • 37
  • 47
  • 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 Apr 09 '10 at 20:52
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"
mklement0
  • 382,024
  • 64
  • 607
  • 775
5

I like Jean-François Larvoire's solution very much, especially for his handling of Arguments and passing them to the powershell-script diredtly (+1 added).

But it has one flaw. AS I do npt have the reputatioin to comment, I post the correction as a new solution.

The script name as argument for Invoke-Expression in double-quotes will not work when the script-name contains a $-character, as this will be evaluated before the file contents is loaded. The simplest remedy is to replace the double quotes:

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

Personally, I rather prefer using get-content with the -raw option, as to me this is more powershell'ish:

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

But that is, of course just my personal opinion. ReadAllText works just perfectly.

For completeness, the corrected script:

<# :# 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
Xanius
  • 71
  • 1
  • 4
  • 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. – Jean-François Larvoire Dec 19 '20 at 16:15
3

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
Kyle
  • 3,549
  • 1
  • 19
  • 25
  • 1
    +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 Mar 02 '18 at 05:57
2

My current preference for this task is a polyglot header that works much the same way as 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, $_ }

I prefer to lay the cmd header out as multiple lines with a single command on each one, for a number of reasons. First, I think it's easier to see what's going on: the command lines are short enough not to run off the right of my edit windows, and the column of punctuation on the left marks it visually as the header block that the horribly abused label on the first line says it is. Second, the del and goto commands are on their own lines, so they will still run even if something really funky gets passed as a script argument.

I have come to prefer solutions that make a temporary .ps1 file to those that rely on Invoke-Expression, purely because PowerShell's inscrutable error messages will then at least include meaningful line numbers.

The time it takes to make the temp file is usually completely swamped by the time it takes PowerShell itself to lumber into action, and 128 bits worth of %RANDOM% embedded in the temp file's name pretty much guarantees that multiple concurrent scripts won't ever stomp each other's temp files. The only real downside to the temp file approach is possible loss of information about the directory the original cmd script was invoked from, which is the rationale for the dir environment variable created on the second line.

Obviously it would be far less annoying for PowerShell not to be so anal about the filename extensions it will accept on script files, but you go to war with the shell you have, not the shell you wish you had.

Speaking of which: as mklement0 observes,

# BREAKS, due to the `&` inside \"...\"
sample.cmd "A \"rock & roll\" life style"

This does indeed break, due to cmd.exe's completely worthless argument parsing. I've generally found that the less work I do to try to hide cmd's many limitations, the fewer unanticipated bugs I cause myself down the line (I am sure I could come up with arguments containing parentheses that would break mklement0's otherwise impeccable ampersand escaping logic, for example). Less painful, in my view, just to bite the bullet and use something like

sample.cmd "A \"rock ^^^& roll\" life style"

The first and third ^ escapes get eaten when that command line is initially parsed; the second one survives to escape the & embedded in the command line passed to powershell.exe. Yes, this is ugly. Yes, it does make it harder to pretend that cmd.exe isn't what gets first crack at the script. Don't worry about it. Document it if it matters.

In most real-world applications, the & issue is moot anyway. Most of what's going to get passed as arguments to a script like this will be pathnames that arrive via drag and drop. Windows will quote those, which is enough to protect spaces and ampersands and in fact anything other than quotes, which aren't allowed in Windows pathnames anyway.

Don't even get me started on Vinyl LP's, 12" turning up in a CSV file.

flabdablet
  • 3,565
  • 3
  • 22
  • 15
  • 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). – mklement0 May 01 '18 at 22:45
2

Another sample batch+PowerShell script... It's simpler than the other proposed solution, and has characteristics that none of them can match:

  • No creation of a temporary file => Better performance, and no risk of overwriting anything.
  • No special prefixing of the batch code. This is just normal batch. And same thing for the PowerShell code.
  • Passes all batch arguments to PowerShell correctly, even quoted strings with tricky characters like ! % < > ' $
  • Double quotes can be passed by doubling them.
  • Standard input is usable in PowerShell. (Contrary to all versions that pipe the batch itself to PowerShell.)

This sample displays the language transitions, and the PowerShell side displays the list of arguments it received from the batch side.

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

Note that I use :# for batch comments, instead of :: as most other people do, as this actually makes them look like PowerShell comments. (Or like most other scripting languages comments actually.)

2

Use Invoke-Command (icm for short), we can prepend the following 4 line header to a ps1 file, make it a valid cmd batch:

<# : batch portion
@powershell -noprofile "& {icm -ScriptBlock ([Scriptblock]::Create((cat -Raw '%~f0'))) -NoNewScope -ArgumentList $args}" %*
@exit /b %errorlevel%
: end batch / begin powershell #>

"Result:"
$args | %{ "`$args[{0}]: $_" -f $i++ }

if want to make args[0] point to script path, change %* to "'%~f0'" %*

James Z.M. Gao
  • 516
  • 1
  • 8
  • 13
1

Without fully understanding your question, my suggestion would be something like:

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

or better yet

@echo off
set MYSCRIPTPATH=c:\work\bin\powershellscript.ps1
powershell %MYSCRIPTPATH%
John Weldon
  • 39,849
  • 11
  • 94
  • 127
  • 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 Apr 09 '10 at 19:53
1

You can add three lines before your Powershell script, use block comments only and then save it as a batch file. Then, you can have a batch file to run the Powershell script. Example:

psscript.bat

@echo off
@powershell -command "(Get-Content -Encoding UTF8 '%0' | select-string -pattern '^[^@]')" | @powershell -NoProfile -ExecutionPolicy ByPass
@goto:eof
<# Must use block comment; Powershell script starts below #>
while($True) {
    Write-Host "wait for 3s"
    Start-Sleep -Seconds 3
}
Timespace
  • 5,101
  • 7
  • 23
  • 32
1

bringing a few ideas together

<# :
@powershell -<%~f0&goto:eof
#>

Write-Output "Hello World" 
Write-Output "Hello World again" 
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jan 27 '23 at 09:12
  • 2 important things for other people trying this one, if you remove the empty line, this will not work. using pause at the end will display the message but actually it will not pause. – Stefano Feb 02 '23 at 01:43
1
@powershell -noninteractive "& ([Scriptblock]::Create((gc '%~df0' | select -Skip 1 | Out-String))) %*" & goto :eof
Red Riding Hood
  • 1,932
  • 1
  • 17
  • 36
0

my offering is as follows:

  • uses certutil.exe to hash the input file to form a file name in temp
  • uses more.exe to strip off the header, and create the file in temp
  • passes all -arg xxx style arguments to powershell
  • keeps ps1 file in temp so you can view it.

example usage


.\runner.bat
.\runner.bat -scope town
.\runner.bat -scope "whole universe"

runner.bat

@echo off & (For /F Delims^= %%a In ('CertUtil -HashFile %0 SHA1^|FindStr /VRC:"[^a-f 0-9]"') Do Set "PS1=%TEMP%\%%a.ps1" )
(if not exist %PS1% more +3 %0 > %PS1%) & (PowerShell.exe -ExecutionPolicy bypass -file %PS1% %* & goto :EOF)
@@@@@@[ PowerShell Starts Here ]@@@@@@

Param(
    [String]$scope = "world"
)
write-host "hello $scope"


unsynchronized
  • 4,828
  • 2
  • 31
  • 43
0

A modified version of Red Riding Hood's answer. [ because the Stackoverflow edit queue is full :D ]

For completeness:

  • Added missing useful PowerShell arguments
  • Returns ErrorLevel for improved error handling.
  • Supports single and double quoted arguments
  • Eliminates this nasty bug
  • Ensures compatibility with Batch and PowerShell syntax highlighting, preventing syntax errors.

Code Snippet:

<# :
@PowerShell -ExecutionPolicy Bypass -NoLogo -NoProfile "& ([Scriptblock]::Create((Get-Content '%~df0' | Select-Object -Skip 4 | Out-String))) %*" & goto :eof
exit /b %errorlevel%
#>

Full Demo Script:

<# :
@PowerShell -ExecutionPolicy Bypass -NoLogo -NoProfile "& ([Scriptblock]::Create((Get-Content '%~df0' | Select-Object -Skip 4 | Out-String))) %*" & goto :eof
exit /b %errorlevel%
#>
Write-Output $PSVersionTable
Write-Output $args
$host.ui.RawUI.WindowTitle = "PowerShell Chimera"
[System.Security.Principal.WindowsIdentity]::GetCurrent().Name
Start-Sleep -Seconds 5
Exit 0x539

Usage Example:

# demo.bat var1="1" var2='2' & echo %errorlevel%
# demo.bat "var1=1" 'var2=2' & echo %errorlevel%

Output:

Name                           Value
----                           -----
PSVersion                      5.1.19041.868
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.19041.868
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
var1=1
var2=2
WINDOWS-PC\User
1337

Side note: If you are not sure about the -NonInteractive argument used in Red Riding Hood's original answer, i quoted a explanation below.

I think you are misunderstanding the use of the -NonInteractive switch; you can still run powershell -noninteractive and get an interactive prompt. The NonInteractive switch is intended for automated scripting scenarios, where you don't want powershell to send a prompt to the user and wait for a response. For example, in a non-interactive PowerShell window, if you run Get-Credential without any parameters it will immediately fail instead of prompting for a username and password. Noninteractive will NOT act as a security mechanism. A better method is to secure what you're trying to protect, not the tools a user might use to access it.

Source: jbsmith's answer to question "How to force PowerShell to not allow an interactive command window"


PS: This topic transformed into a PowerShell hacking contest :D

Stefano
  • 224
  • 4
  • 7
  • I'm not sure -ExecutionPolicy Bypass does anything in this situation. I also had it initially until I realized it wasn't needed :) Scriptblock bypasses it anyway. – Red Riding Hood Jun 09 '23 at 17:57
  • Good point! I included the -ExecutionPolicy Bypass argument in the script to handle situations where I needed to import external modules or execute scripts without encountering execution policy restrictions. This argument allows seamless importing without triggering any errors related to the execution policy. It's worth noting that the bypass argument might not be necessary in all scenarios, especially in recent versions of PowerShell. The scriptblock bypasses the execution policy anyway. Feel free to correct me if my understanding is outdated, as this knowledge is before the great virus :) – Stefano Jun 10 '23 at 22:04
0

The top-voted findstr answer lacks these features which my solution has:

  • Read-Host/pause now works
  • argument pass-through (including quoted arguments)
  • return code pass-through
<# :
  @set batch_args=%*
  @powershell "iex ('$args = @(iex \"^& {`$args} $env:batch_args\");' + (cat -Raw '%~f0'))"
  @exit /b %ERRORLEVEL%
#>

Write-Host Your PowerShell code goes here. First arg: $args[0] -fore Green
Read-Host Waiting for user to press Enter
Exit 42

If you do not need arguments, you can just do @powershell "iex (cat -Raw '%~f0')"


Explanation:

  • <# ... #> is a comment block in PowerShell and thus skipped, but <# : is interpreted as a null redirection in Batch and thus the comment is only run as batch code
    • exit /b %ERRORLEVEL% makes it return the PowerShell exit code
  • PowerShell will only run files which have a .ps1 ending, so instead of executing the current file directly with PowerShell, we call Invoke-Expression/iex instead to execute it
    • %~f0 in Batch is the name of the current code file
    • $args is normally an automatic PowerShell variable containing the command line arguments, but here we place a line on top of the loaded file which manually sets it
  • @ in Batch prevents the command from being echoed back on the command line
  • %* in Batch refers to all arguments passed to the script.
    • The & {$args} %* snippet you see in some other answers creates a code block which just returns its arguments and executes it with the Batch arguments placed after it
    • Quoted arguments do not work in that snippet because PowerShell is suspicious enter image description here
    • If I do $args = (echo %*), then echo waits for user interaction if no args are passed :/
    • Thus my super-special workaround of setting an env variable and parsing it with Invoke-Expression twice, this addresses the bad-quoting issue and avoids the no-argument issue!
    • @ ensures we always get an array, even if we have 0 arguments

The ScriptBlock-based answers have the problem that Write-Host statements are immediately displayed while Write-Output/echo-based answers are only shown when the block completes, which is fatal for my use case (use Read-Host to keep the cmd-window open in case of an error)

xjcl
  • 12,848
  • 6
  • 67
  • 89
0

Okay, so I take 1% credit for this, but I wanted to contribute to a thread I have come back to time and time again. This is a matured culmination of all of the snippets above, effectively addressing all of the different bugs I am aware of. I built this to make launching PowerShell scripts easier within our organization.

<# PowerShell comment protecting the Batch section
@echo off

:# Clear the console to suppress any errors.
cls

:# Disabling argument expansion avoids issues with ! in arguments.
SetLocal EnableExtensions DisableDelayedExpansion

:# Checking for Administrator Privileges.
net session >nul 2>&1
IF NOT %ErrorLevel% == 0 (
    ECHO. Failure: Current permissions are inadequate.
    EXIT /B 1
)

:# Prepare the batch arguments so that PowerShell parses them correctly
SET ARGS=%*
IF defined ARGS set ARGS=%ARGS:"=\"%
IF defined ARGS set ARGS=%ARGS:'=''%

:# Ensure FilePath is utilizing a lettered drive path.
SET "FilePath=%~f0"
IF "%FilePath:~0,2%" == "\\" PUSHD "%~dp0"
IF "%FilePath:~0,2%" == "\\" SET "FilePath=%CD%\%~nx0"
IF NOT "%FilePath:~0,2%" == "\\" CD "%~dp0"

:# Escape the file path for all possible invalid characters.
SET "FilePath=%FilePath:'=''%"
SET "FilePath=%FilePath:^=^^%"
SET "FilePath=%FilePath:[=`[%"
SET "FilePath=%FilePath:]=`]%"
SET "FilePath=%FilePath:&=^&%"

:# ============================================================================================================ #:
:# 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; Entering PowerShell.
"%WinDir%\System32\WindowsPowerShell\v1.0\powershell.exe" -NonInteractive -NoLogo -NoProfile -Command ^
    ^"Invoke-Expression ('^& {' + (get-content -raw '%FilePath%') + '} %ARGS%')"
ECHO Exited PowerShell; Back in BATCH.

pause
POPD
exit /b

###############################################################################
End of the PS comment around the Batch section; Begin the PowerShell section #>
Nick W.
  • 1,536
  • 3
  • 24
  • 40