How to escape poison character in this mixed cmd / powershell code? - windows

I asked for a first explanation "here" and "here" but going to try a more complex situation I was unable (after two hours of trying) to understand how to solve. I read how the regular expression works but nothing, I went into the ball.
The modified code is this:
(Fsutil Dirty Query %SystemDrive%>Nul)||(powershell.exe -c "[Environment]::CommandLine; Start -Verb RunAs cmd /k, ("^""%~f0"^"" -replace '[;,()= &^]', '^$&')" & echo exit)
and the folder with the poison characters is this:
C:\Users\fposc\Desktop\Pie & tea % # ' $^
I have tried to escape the ^ in the regular expression with \^ but don't work. I have escaped also ( and ) with \( and \). But nothing work:
(Fsutil Dirty Query %SystemDrive%>Nul)||(powershell.exe -c "[Environment]::CommandLine; Start -Verb RunAs cmd /c, ("^""%~f0"^"" -replace '[;,\(\)= &\^]', '^$&')" & exit)
I added the round brackets because I wanted to put all possible characters to make the code as generic as possible.
I don't know if I was right to open another question. Maybe I should change the original question? Since other combinations are possible and not having understood the mechanism I could open many other similar questions. What do you advise me to do?

The problem is the presence of $ in your folder name, which causes the PowerShell command to interpret it as the start of a variable reference.
The workaround is to use an aux. environment variable to store the batch file's full path and let PowerShell perform its escaping based on this variable's value:
:: Unless already elevated, re-invoke this batch file with elevation,
:: via PowerShell.
set "__THISFILE=%~f0"
Fsutil Dirty Query %SystemDrive% >Nul || (powershell.exe -c "Start-Process -Verb RunAs cmd /k, ($env:__THISFILE -replace '[ &%%^]', '^$&')" & exit)
I have updated the answer to your original question to incorporate this approach, which now shows a - hopefully - robust approach to on-demand re-invocation of a batch file with elevation, including support for arguments.

Related

How to escape schtasks /tr arguments

I need to schedule a PowerShell task like following:
powershell.exe -file ..\Execute\execute.ps1
And I have tried assigning it to an argument $Argument then pass it to schtasks like following:
$Argument = "powershell.exe -file '..\Execute\execute.ps1'"
schtasks /create /tn "SOE_Checks" /tr $Argument /sc DAILY /st 05:00 /ru "System" /rl HIGHEST /f
but after running above code, nothing happened - while the task is created successfully, it appears not to run.
I have also tried assigning it to $Argument without the quotes, it worked but I got the following warnings:
ERROR: Invalid syntax. Value expected for '/tr'.
Type "SCHTASKS /CREATE /?" for usage.
Can anyone please let me know what I have done wrong here? (I am aware that I can accomplish this using PowerShell's New-ScheduledTaskAction but I want it to work this way)
Just want to add that if I change the file path to a specific location in $Argument like this, $Argument = "powershell.exe -file 'C:\SOE\Execute\execute.ps1'", it works fine without any warnings but this is not ideal.
I have read this but it does not work for me
Scheduled tasks created with schtasks.exe execute with the working directory set to $env:windir\system32[1], so unless your script happens to be located in ..\Execute\execute.ps1 relative to there, your command won't work as intended.
If you don't want to hard-code the script path directly into the command, construct the command dynamically, by resolving the relative path to an absolute one when you assign to $Argument:
$Argument = 'powershell.exe -file \"{0}\"' -f (Convert-Path ..\Execute\execute.ps1)
Note the - unfortunate - need to escape the embedded " as \", which is longstanding bug that hasn't been fixed for the sake of backward compatibility - see this GitHub docs issue for background.
Convert-Path resolves a relative path to an absolute one.
Note that the relative path must refer to an existing file (or directory).
Similarly, relative paths inside your script will be relative to $env:windir\system32 too; to make them relative to the script's directory, change to your script's directory first by executing Set-Location $PSScriptRoot at the start of your script.
Optional reading: How to quote commands that run from a scheduled task:
Note: Virtually the same rules apply as when running a command from the Windows Run dialog (press WinKey+R), which you can use to test-drive a command (the command to pass to schtasks /tr, without outer quoting, not the whole schtasks command line) - though note that the working directory will be the user's home directory, and that you won't be able to use '...'-quoting around the PowerShell CLI's -File argument - see below):
cmd.exe is NOT involved during execution, which means:
You needn't worry about non-double-quoted use of cmd.exe metacharacters such as &, for instance, so you can use these characters even in single-quoted strings passed to the PowerShell CLI powershell.exe as (part of) the -Command argument(s).
Conversely, output redirections (e.g., > c:\path\to\log.txt) are not directly supported.
In the context of invoking the PowerShell CLI, this means:
With -File, you cannot use them on the command line and must instead perform them from within your script.
With -Command, however, you can use them, because it is then PowerShell that applies them (but note that Windows PowerShell's > operator creates UTF-16LE files).
(Even though cmd.exe isn't involved) references to environment variables using the same syntax form as in cmd.exe are expanded (e.g., %USERNAME%)
Caveat: You cannot escape such references:
%% doesn't work - the additional % is simply treated as a literal, and expansion still occurs; e.g., %%OS%% results in %Windows_NT%.
^% (accidentally) prevents expansion, but retains the ^ - the ^ doesn't escape; rather, it "disrupts" the variable name, in which case the token is left as-is; e.g., ^%OS^% results in ^%OS^%, i.e., is retained as-is.
The above applies to the commands as they must end up defined inside a scheduled task, as you would see or define them interactively in Task Scheduler (taskschd.msc).
Additionally, for creating a scheduled task from the command line / a PowerShell script / a batch file:
you must quote the command as a whole and
comply with the syntax rules of the calling shell regarding escaping and up-front string interpolation.
(You can only get away without quoting if the command consists of only a single word that needs no escaping, such the path to an executable that contains no spaces or special characters and to which no arguments are passed.)
When calling schtasks.exe[2], quote the /tr argument as a whole as follows:
from PowerShell, use "...", if you need to expand (string-interpolate) the command string up front; otherwise, use '...'.
Important: The need to escape nested " as \" applies in both cases, which in the case of outer "..." quoting means that nested " must be escaped as \`" (sic).
Surprisingly, schtasks.exe recognizes embedded '...' quoting and automatically translates it to "..." quoting - that is why your original command, "powershell.exe -file '..\Execute\execute.ps1'", worked, even though in direct invocation the PowerShell CLI does not support the use of '...' in combination with -File.
from cmd.exe (whether directly or from a batch file), you must use "...".
PowerShell examples:
The following PowerShell commands create and execute two run-once
scheduled tasks, named test1 and test2, that run when the next calendar minute starts, in the context of the calling user, visibly. (You'll have to remove these tasks manually afterwards.)
You may have to wait for up to 1 minute to see the invocation kick in, at which point a new console window pops up for each task.
# Create sample script test.ps1 in the current dir. that
# echoes its arguments and then waits for a keypress.
'"Hi, $Args."; Read-Host "Press ENTER to exit"' > test.ps1
# Find the start of the next calendar minute.
$nextFullMinute = ([datetime]::Now.AddMinutes(1).TimeOfDay.ToString('hh\:mm'))
# -File example:
# Invoke test.ps1 and pass it 'foo' as an argument.
# Note the escaped embedded "..." quoting around the script path
# and that with -File you can only pass literal arguments at
# invocation time).
schtasks.exe /create /f /tn test1 /sc once /st $nextFullMinute `
/tr "powershell -File \`"$PWD/test.ps1\`" foo" #`# (dummy comment to fix broken syntax highlighting)
# -Command example:
# Invoke test.ps1 and pass it $env:USERNAME as an argument.
# Note the '...' around the script path and the need to invoke it with
# &, as well as the ` before $env:USERNAME to prevent its premature expansion.
schtasks.exe /create /f /tn test2 /sc once /st $nextFullMinute `
/tr "powershell -Command & '$PWD/test.ps1' `$env:USERNAME"
"Tasks will execute at ${nextFullMinute}:00"
[1] Note that the Task Scheduler GUI allows you to configure a working directory, but this feature isn't available via the schtasks.exe utility.
[2] The same applies to values passed to the -Argument parameter of the New-ScheduledTaskAction PowerShell cmdlet, though note that the executable name/path is specified separately there, via the -Execute parameter.
By contrast, the Register-ScheduledJob cmdlet for creating scheduled PowerShell jobs accepts a script block as the command to run, which eliminates the quoting headaches.

How to escape spaces the parameters to a PS script running in VS post-build event

Update 1:
After looking carefully at the output again, I sort of figured it out.
By adding a trailing space between the closing parenthesis and the quotes it works:
powershell -ExecutionPolicy ByPass -File "$(SolutionDir)\BuildScripts\InjectGitVersion.ps1" "$(ProjectDir) " "$(TargetDir) "
I am suspecting that PowerShell somehow interprets the )".
Is there a more elegant way around this issue?
Update 2:
This is weird. I have another script that does the clean up and works with out the space between ) and " like this:
powershell -ExecutionPolicy ByPass -File "$(SolutionDir)\BuildScripts\InjectGitVersionCleanup.ps1" "$(ProjectDir)"
However adding trailing spaces will cause it to fail, because internally it appends a file-name to the path, so that the path will be incorrect.
If any one understand this, I would be happy to accept the explanation as the correct answer!
Original:
I have the following prebuild-command in VisualStudio, which I want to use to inject the version from a Git-tag:
powershell -ExecutionPolicy ByPass -File "$(SolutionDir)\BuildScripts\InjectGitVersion.ps1" $(ProjectDir) $(TargetDir)
This works fine, if the path $(SolutionDir) does not contain spaces (which will then also present in $(ProjectDir) or $(TargetDir)).
When the path $(SolutionDir) does contain spaces, it appears the script starts as expected, but the arguments are not passed correctly and I am unable to figure out how to escape them in the arguments to the PS-script.
I have tried adding sing ", triple """ and also ', which gives the following (each PS-command tries a different method for escaping the spaces):
powershell -ExecutionPolicy ByPass -File "$(SolutionDir)\BuildScripts\InjectGitVersion.ps1" $(ProjectDir) $(TargetDir)
args[0]:
D:\VisualStudio
args[1]:
Projects\software_git_repo\ProgramEditor\
powershell -ExecutionPolicy ByPass -File "$(SolutionDir)\BuildScripts\InjectGitVersion.ps1" "$(ProjectDir)" "$(TargetDir)"
BS: args[0]:
D:\VisualStudio Projects\software_git_repo\ProgramEditor" D:\VisualStudio
BS: args[1]:
Projects\software_git_repo\ProgramEditor\bin\Debug"
powershell -ExecutionPolicy ByPass -File "$(SolutionDir)\BuildScripts\InjectGitVersion.ps1" """$(ProjectDir)""" """$(TargetDir)"""
BS: args[0]:
"D:\VisualStudio
BS: args[1]:
Projects\software_git_repo\ProgramEditor"
powershell -ExecutionPolicy ByPass -File "$(SolutionDir)\BuildScripts\InjectGitVersion.ps1" '$(ProjectDir)' '$(TargetDir)'
BS: args[0]:
'D:\VisualStudio
BS: args[1]:
Projects\software_git_repo\ProgramEditor\'
Doing:
echo ProjectDir:
echo $(ProjectDir)
echo TargetDir:
echo $(TargetDir)
I get:
ProjectDir:
D:\VisualStudio Projects\software_git_repo\ProgramEditor\
TargetDir:
D:\VisualStudio Projects\software_git_repo\ProgramEditor\bin\Debug\
Beware unexpected escaped quotes when a VS directory macro is expanded in a quoted parameter.
A script parameter containing a path should be wrapped in quotes to avoid being interpreted as multiple parameters if the path includes spaces. The following would therefore seem reasonable in Visual Studio's pre/post build event command line:-
powershell.exe -file "$(SolutionDir)script.ps1" -projectDirectory "$(ProjectDir)"
But the macro values that specify directories always include a trailing backslash; so when they're expanded the line becomes, for example:-
powershell.exe -file "C:\sln path\script.ps1" -projectDirectory "C:\sln path\project\"
...where the trailing \" is an escaped character that is interpreted as part of the parameter value, which is passed to the script as:-
C:\sln path\project"
Solution
The fix in my case was to insert an additional backslash after the directory macro to be escaped instead of the quotes, which restores the expected parameter value and prevents the mangling of any subsequent params:-
powershell.exe -file "$(SolutionDir)script.ps1" -projectDirectory "$(ProjectDir)\"
Not very intuitive though. Let me know if I'm missing something more obvious...
One thing I noticed is an extra backslash after $(SolutionDir). I'm using VS2017 and I think consistently $(SolutionDir) has always had a backslash. Not sure though.
I have a .bat in my solution folder that creates a json config file. My Post-Build event looks like this.
call "$(SolutionDir)CreateConfigurationJson.bat" "$(TargetDir)mySettings.json"
{"ProjectName":"$(ProjectName)"}
"$(TargetDir)mySettings.json" is the first parameter of the .bat file
{"ProjectName":"$(ProjectName)"} is the second parameter.
No escape characters necessary. Hope this helps.
The code for the bat file is:
echo off
set filePath=%1
break>%filePath%
set json=%2
echo %json% >>%filePath%
thank you #tranquil_tarn for your great answer!
I struggled with the BATCH + Powershell escaping hell for multiple hours.
Posting final code here to hopefully help someone out.
Solution
#ECHO OFF
cls
echo .SYNOPSIS
echo Allows prebuild to be Powershell, instead of BATCH. (You'll thank me later)
echo Specifically this example shows passing in VisualStudio (ProjectDir) variable AND correctly handles spaces in the path.
echo .DESCRIPTION
echo Call prebuild.ps1 and pass in built-in VS Variables: https://learn.microsoft.com/en-us/visualstudio/ide/reference/pre-build-event-post-build-event-command-line-dialog-box?view=vs-2022
echo Use powershell best-practices named parameters rather than positional to avoid positional-unmatched errors.
echo If Username '$(User)' is blank DO NOT include the Switch at all to avoid powershell errors
REM TASK1: Check a variable for blank/empty and only include the Param if not empty
SET "user=$(User)"
IF [%user%]==[] (
echo "userParam is blank"
) ELSE (
SET "userParam=-User:%user%"
)
echo "userParam: %userParam%"
REM TASK2: Warning: (ProjectDir) in VisualStudio ends in a slash, which when expanded, results in escaped quote \' at end of line
REM if you DO NOT include the quotes, then spaces in path will break it. if you DO include quotes then it gets escaped.
REM solution is to include an extra slash at the end, resulting in double backslash \\ which escapes down to single backslash '\'... whew!
REM 'see https://stackoverflow.com/questions/52205117/how-to-escape-spaces-the-parameters-to-a-ps-script-running-in-vs-post-build-even/64146880#64146880'
cd "$(ProjectDir)\"
SET cmd1=powershell -NoProfile -ExecutionPolicy Unrestricted -Command ^
".\prebuild.ps1 -ProjectDir:'$(ProjectDir)\' -Conf:'$(Configuration)' -ProjectName:'$(ProjectName)' %userParam% "
REM For debugging, view the command (including escaped strings) before calling it.
echo %cmd1%
call %cmd1%

Puppet escaping my variables when creating a windows script

I'll keep it brief, I'm trying to run the following code to install vmware, the variables are defined and fully functional, however when this is interperated into the batch file, they aren't escaped and show exactly as they're defined in the code (displayed below).
$cmd = 'cd "Single Sign On" & start /wait VMware-SSO-Server.exe /S /L1033 /v"/qr CONFIG_TYPE=Setup SETUP_TYPE=Basic SSO_DB_SERVER_TYPE=Bundled MASTER_PASSWORD=${sso_pwd} RSA_DBA_PASSWORD=${sso_pwd} RSA_USER_PASSWORD=${sso_pwd} COMPUTER_FQDN=test"'
In a windows bat file to define a variable you have to use SET and then to use these variable you have to put them between %
You'll better try like this :
#echo off
cd "Single Sign On"
start /wait VMware-SSO-Server.exe /S /L1033 /v /qr CONFIG_TYPE=Setup SETUP_TYPE=Basic SSO_DB_SERVER_TYPE=Bundled MASTER_PASSWORD=%${sso_pwd}% RSA_DBA_PASSWORD=%${sso_pwd}% RSA_USER_PASSWORD=%${sso_pwd}% COMPUTER_FQDN=test"
Assuming the paste is from your manifest, it doesn't work because you are using single quotes. Therefor variables are not expanded during manifest evaluation.
You will need to use
$cmd = "cd \"Single Sign On\" & start /wait ..."
Be careful to escape all double quotes, but other than that I see no special characters you need to worry about.

How do I run two commands in one line in Windows CMD?

I want to run two commands in a Windows CMD console.
In Linux I would do it like this
touch thisfile ; ls -lstrh
How is it done on Windows?
Like this on all Microsoft OSes since 2000, and still good today:
dir & echo foo
If you want the second command to execute only if the first exited successfully:
dir && echo foo
The single ampersand (&) syntax to execute multiple commands on one line goes back to Windows XP, Windows 2000, and some earlier NT versions. (4.0 at least, according to one commenter here.)
There are quite a few other points about this that you'll find scrolling down this page.
Historical data follows, for those who may find it educational.
Prior to that, the && syntax was only a feature of the shell replacement 4DOS before that feature was added to the Microsoft command interpreter.
In Windows 95, 98 and ME, you'd use the pipe character instead:
dir | echo foo
In MS-DOS 5.0 and later, through some earlier Windows and NT versions of the command interpreter, the (undocumented) command separator was character 20 (Ctrl+T) which I'll represent with ^T here.
dir ^T echo foo
A quote from the documentation:
Source: Microsoft, Windows XP Professional Product Documentation, Command shell overview
Also: An A-Z Index of Windows CMD commands
Using multiple commands and conditional processing symbols
You can run multiple commands from a single command line or script using conditional processing symbols. When you run multiple commands with conditional processing symbols, the commands to the right of the conditional processing symbol act based upon the results of the command to the left of the conditional processing symbol.
For example, you might want to run a command only if the previous command fails. Or, you might want to run a command only if the previous command is successful.
You can use the special characters listed in the following table to pass multiple commands.
& [...]
command1 & command2
Use to separate multiple commands on one command line. Cmd.exe runs the first command, and then the second command.
&& [...]
command1 && command2
Use to run the command following && only if the command preceding the symbol is successful. Cmd.exe runs the first command, and then runs the second command only if the first command completed successfully.
|| [...]
command1 || command2
Use to run the command following || only if the command preceding || fails. Cmd.exe runs the first command, and then runs the second command only if the first command did not complete successfully (receives an error code greater than zero).
( ) [...]
(command1 & command2)
Use to group or nest multiple commands.
; or ,
command1 parameter1;parameter2
Use to separate command parameters.
& is the Bash equivalent for ; ( run commands) and && is the Bash equivalent of && (run commands only when the previous has not caused an error).
If you want to create a cmd shortcut (for example on your desktop) add /k parameter (/k means keep, /c will close window):
cmd /k echo hello && cd c:\ && cd Windows
You can use & to run commands one after another. Example: c:\dir & vim myFile.txt
You can use call to overcome the problem of environment variables being evaluated too soon - e.g.
set A=Hello & call echo %A%
A number of processing symbols can be used when running several commands on the same line, and may lead to processing redirection in some cases, altering output in other case, or just fail. One important case is placing on the same line commands that manipulate variables.
#echo off
setlocal enabledelayedexpansion
set count=0
set "count=1" & echo %count% !count!
0 1
As you see in the above example, when commands using variables are placed on the same line, you must use delayed expansion to update your variable values. If your variable is indexed, use CALL command with %% modifiers to update its value on the same line:
set "i=5" & set "arg!i!=MyFile!i!" & call echo path!i!=%temp%\%%arg!i!%%
path5=C:\Users\UserName\AppData\Local\Temp\MyFile5
cmd /c ipconfig /all & Output.txt
This command execute command and open Output.txt file in a single command
So, I was trying to enable the specific task of running RegAsm (register assembly) from a context menu. The issue I had was that the result would flash up and go away before I could read it. So I tried piping to Pause, which does not work when the command fails (as mentioned here Pause command not working in .bat script and here Batch file command PAUSE does not work). So I tried cmd /k but that leaves the window open for more commands (I just want to read the result). So I added a pause followed by exit to the chain, resulting in the following:
cmd /k C:\Windows\Microsoft.NET\Framework\v4.0.30319\regasm.exe "%1" /codebase \"%1\" & pause & exit
This works like a charm -- RegAsm runs on the file and shows its results, then a "Press any key to continue..." prompt is shown, then the command prompt window closes when a key is pressed.
P.S. For others who might be interested, you can use the following .reg file entries to add a dllfile association to .dll files and then a RegAsm command extension to that (notice the escaped quotes and backslashes):
[HKEY_CLASSES_ROOT\.dll]
"Content Type"="application/x-msdownload"
#="dllfile"
[HKEY_CLASSES_ROOT\dllfile]
#="Application Extension"
[HKEY_CLASSES_ROOT\dllfile\Shell\RegAsm]
#="Register Assembly"
[HKEY_CLASSES_ROOT\dllfile\Shell\RegAsm\command]
#="cmd /k C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\regasm.exe \"%1\" /codebase \"%1\" & pause & exit"
Now I have a nice right-click menu to register an assembly.
In windows, I used all the above solutions &, && but nothing worked
Finally ';' symbol worked for me
npm install; npm start
Well, you have two options: Piping, or just &:
DIR /S & START FILE.TXT
Or,
tasklist | find "notepad.exe"
Piping (|) is more for taking the output of one command, and putting it into another. And (&) is just saying run this, and that.
In order to execute two commands at the same time, you must put an & (ampersand) symbol between the two commands. Like so:
color 0a & start chrome.exe
Cheers!
I try to have two pings in the same window, and it is a serial command on the same line. After finishing the first, run the second command.
The solution was to combine with start /b on a Windows 7 command prompt.
Start as usual, without /b, and launch in a separate window.
The command used to launch in the same line is:
start /b command1 parameters & command2 parameters
Any way, if you wish to parse the output, I don't recommend to use this.
I noticed the output is scrambled between the output of the commands.
Use & symbol in windows to use command in one line
C:\Users\Arshdeep Singh>cd Desktop\PROJECTS\PYTHON\programiz & jupyter notebook
like in linux
we use,
touch thisfile ; ls -lstrh
I was trying to create batch file to start elevated cmd and to make it run 2 separate commands.
When I used & or && characters, I got a problem. For instance, this is the text in my batch file:
powershell.exe -Command "Start-Process cmd \"/k echo hello && call cd C:\ \" -Verb RunAs"
I get parse error:
After several guesses I found out, that if you surround && with quotes like "&&" it works:
powershell.exe -Command "Start-Process cmd \"/k echo hello "&&" call cd C:\ \" -Verb RunAs"
And here's the result:
May be this'll help someone :)
No, cd / && tree && echo %time%. The time echoed is at when the first command is executed.
The piping has some issue, but it is not critical as long as people know how it works.
One more example: For example, when we use the gulp build system, instead of
gulp - default > build
gulp build - build build-folder
gulp watch - start file-watch
gulp dist - build dist-folder
We can do that with one line:
cd c:\xampp\htdocs\project & gulp & gulp watch
Yes there is. It's &.
&& will execute command 2 when command 1 is complete providing it didn't fail.
& will execute regardless.
With windows 10 you can also use scriptrunner:
ScriptRunner.exe -appvscript demoA.cmd arg1 arg2 -appvscriptrunnerparameters -wait -timeout=30 -rollbackonerror -appvscript demoB.ps1 arg3 arg4 -appvscriptrunnerparameters -wait -timeout=30 -rollbackonerror
it allows you to start few commands on one line you want you can run them consecutive or without waiting each other, you can put timeouts and rollback on error.
Try to create a .bat ot .cmd file with those lines using doskey key and $T which is equivalent to & to do several command line in just one line :
touch=echo off $T echo. ^> $* $T dir /B $T echo on
It'll create an empty file.
Example:
touch myfile
In cmd you'll get something like this:
But as mentioned previously by others, it is really advised to use & operator to do many command line in one line from CMD prompt.
Enjoy =)
When you try to use or manipulate variables in one line beware of their content! E.g. a variable like the following
PATH=C:\Program Files (x86)\somewhere;"C:\Company\Cool Tool";%USERPROFILE%\AppData\Local\Microsoft\WindowsApps;
may lead to a lot of unhand-able trouble if you use it as %PATH%
The closing parentheses terminate your group statement
The double quotes don't allow you to use %PATH% to handle the parentheses problem
And what will a referenced variable like %USERPROFILE% contain?
It's simple: just differentiate them with && signs.
Example:
echo "Hello World" && echo "GoodBye World".
"Goodbye World" will be printed after "Hello World".

How to run a PowerShell script within a Windows batch file

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.
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
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.
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.
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"
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
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
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"
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.
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.)
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%
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'" %*
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
}
bringing a few ideas together
<# :
#powershell -<%~f0&goto:eof
#>
Write-Output "Hello World"
Write-Output "Hello World again"

Resources