可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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.
回答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
回答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.
回答3:
Here the topic has been discussed. Main goals were to avoid the usage of temp files to reduce the slow IO operations and to run the script without redundant output.
And here's the best solution according to me:
param( [string]$str ); $VAR = "Hello, world!"; function F1() { $str; $script:VAR; } F1;
Edit (a better way seen here )
$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.
回答4:
This seems to work, if you don't mind one error in PowerShell at the beginning:
dosps.cmd
:
@powershell -
回答5:
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%
回答6:
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
回答7:
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:
NUL && powershell -NoProfile -ExecutionPolicy Bypass -File "%TEMP%\%~0n.ps1" %* & del "%TEMP%\%~0n.ps1" & goto :eof #> # Paste arbitrary PowerShell code here. # In this example, all arguments are echoed. $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 command that starts with @copy ...
is therefore ignored by PowerShell, but executed by cmd.exe
; since the line ends with goto :eof
, 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.
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:
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"
To solve this problem, a more elaborate solution is required:
On the plus side, this solution doesn't require a temporary file.
=^>%) :: Invoke PowerShell, passing it the full filename of this script and the pass-thru arguments. :: The PowerShell command then reads this file and executes it with Invoke-Expression :: as a script block to which the pass-thru arguments are passed. :: NOTE: This file is assumed to be UTF-8-encoded. powershell -NoProfile -Command "Invoke-Expression ('& { ' + [io.file]::ReadAllText(\""%~f0\"") + '} %args%')" goto :eof * The batch-file code ends with the line above and ignores everything below. * Everything up to the end of this PowerShell comment block is generic code. Paste arbitrary PowerShell below this line. #> # In this example, all arguments are echoed. $Args | % { 'arg #{0}: [{1}]' -f ++$i, $_ }
Now you can use ""
to represent embedded "
chars.:
sample.cmd "A ""rock & roll"" life style" 27
The above yields:
arg #1: [A "rock & roll" life style] arg #2: [27]
回答8:
My current preference for this task is a polyglot header that works much the same way as mklement0's first solution:
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.